O blog de segurança da FireAI

Por FireAI Security & Research Team · Publicado

Correr um LLM Local em Silício da Apple para Triagem de Segurança

Correr um LLM Local em Silício da Apple para Triagem de Segurança

Um modelo que classifica uma ligação de rede como merecedora de atenção ou não, a correr no mesmo Mac que fez a ligação, muda três coisas ao mesmo tempo: o que sai da máquina, quanto custa correr, e se continua a funcionar quando a própria rede é aquilo sob suspeita. As três importam mais para uma ferramenta de segurança do que para a maioria dos outros usos de um modelo de linguagem, e é por isso que este artigo trata especificamente do caso no dispositivo, em vez de chamar uma API.

Por que no dispositivo, especificamente para triagem

Enviar uma linha de registo de ligação para um modelo na nuvem, para classificação, significa transmitir, para cada decisão, qual a aplicação no seu Mac a falar com que anfitrião, em que porta, neste momento. Nada disso é um segredo da forma como uma palavra-passe é, mas é exatamente o tipo de metadados que uma configuração consciente da segurança tenta minimizar partilhar, e uma ferramenta cujo propósito inteiro é decidir o que o seu Mac tem permissão para enviar para outro lado tem uma razão óbvia para não depender ela própria de enviar algo para outro lado em cada decisão que toma. Correr o modelo localmente elimina essa dependência por completo: a linha de registo nunca sai do dispositivo, porque não há nenhuma chamada de rede no percurso da decisão.

A segunda razão é a continuidade. Um passo de triagem baseado na nuvem só está tão disponível quanto a sua ligação à internet e a API do fornecedor, ambas exatamente as coisas que podem estar degradadas ou deliberadamente cortadas durante um incidente real. Um modelo a correr em memória local continua a responder com a rede em baixo, o Wi-Fi desligado, ou um compromisso suspeito em curso na mesma ligação que a chamada à API teria usado.

MLX: a própria estrutura de arrays da Apple

O MLX é uma estrutura de arrays construída pela equipa de investigação em aprendizagem automática da Apple especificamente para o silício da Apple, com APIs em Python, C++, C e Swift. A sua própria documentação descreve um modelo de memória unificada como a sua escolha de conceção definidora: os arrays no MLX vivem em memória partilhada, e as operações sobre eles podem correr em qualquer dispositivo suportado — CPU ou GPU — sem que os pesos do modelo tenham primeiro de ser copiados entre grupos de memória separados. Isso importa concretamente para um portátil: uma máquina com GPU dedicada tem de copiar os pesos de um modelo através de um barramento para a memória da GPU antes de conseguir computar seja o que for, o que custa tempo e duplica a pegada de memória; na arquitetura de memória unificada de um Mac, a CPU e a GPU já partilham a mesma memória física, pelo que não há nada para copiar.

O mlx-lm, o pacote complementar para correr modelos de linguagem sobre o MLX, instala-se com um único comando e traz um modelo predefinido já pronto a usar:

Configuração do MLX
pip install mlx-lm
# runs the default model (mlx-community/Llama-3.2-3B-Instruct-4bit)
mlx_lm.generate --prompt "How tall is Mt Everest?"
# or name a specific quantized model from the mlx-community hub
mlx_lm.generate --model mlx-community/Mistral-7B-Instruct-v0.3-4bit --prompt "..."
# interactive chat session instead of a single prompt
mlx_lm.chat
# convert and quantize a model yourself
mlx_lm.convert --model mistralai/Mistral-7B-Instruct-v0.3 -q

llama.cpp: a opção portátil

O llama.cpp é o mais antigo e mais amplamente portado dos dois, escrito em C/C++ sem necessidade de nenhum runtime Python em tempo de inferência. O seu próprio README declara diretamente a posição do projeto quanto a este hardware: o silício da Apple é tratado, nas palavras do projeto, como "um cidadão de primeira classe — otimizado através das estruturas ARM NEON, Accelerate e Metal", e a sua documentação de compilação confirma que, no macOS, o backend Metal da GPU vem ativado por defeito, com uma opção em tempo de compilação para o desativar caso queira especificamente inferência apenas em CPU.

Compilação e execução do llama.cpp
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
cmake -B build
cmake --build build --config Release
# Metal is on by default on macOS; add -DGGML_METAL=OFF to the first
# command above if you need to force CPU-only inference
llama cli -hf ggml-org/Qwen3.5-0.8B-GGUF
# or run it as a local server instead of a one-shot command
llama serve -hf ggml-org/Qwen3.5-0.8B-GGUF

O llama.cpp trabalha a partir de ficheiros GGUF, um formato de modelo num único ficheiro que junta os pesos quantizados e tudo o que é necessário para os correr; o comando acima obtém um diretamente de um repositório do Hugging Face pelo nome. O MLX, por contraste, mantém-se mais próximo de um fluxo de trabalho nativo em Python, convertendo e quantizando modelos para o seu próprio formato antecipadamente. Nenhum é estritamente melhor: o llama.cpp é a escolha se quiser um único binário compilado sem dependência de Python, o MLX é a escolha se já estiver a construir o resto do seu pipeline em Python e quiser acesso de primeira classe ao modelo de memória unificada da Apple a partir daí.

Quantização: o que realmente se troca por um modelo mais pequeno

A quantização guarda cada peso do modelo em menos bits do que os 16 ou 32 em que o modelo foi treinado, encolhendo tanto o ficheiro em disco como a memória necessária para o correr, a algum custo na qualidade da saída. A própria documentação de quantização do llama.cpp lista valores exatos de bits por peso para cada esquema que suporta, o que é uma forma mais precisa de pensar sobre a troca do que os habituais atalhos de "4 bits" ou "8 bits":

Da documentação da ferramenta de quantização do llama.cpp.
Família de esquemaEsquemas de exemploBits por peso
Precisão total (referência)F1616,0
Quase sem perdasQ8_08,50
Meio-termo de maior qualidadeQ5_K_S / Q5_K_M5,57 - 5,70
Qualidade e tamanho equilibradosQ4_K_S / Q4_K_M4,67 - 4,89
Mais pequeno, mais perdasQ3_K_S / Q3_K_M / Q3_K_L3,64 - 4,30
Compressão extremaIQ2_XXS ... IQ2_M2,00 - 2,93

Multiplicar o número de parâmetros de um modelo pelo seu valor de bits por peso dá uma estimativa aproximada de memória apenas para os pesos: um modelo de 7 mil milhões de parâmetros a Q4_K_M, com 4,89 bits por peso, precisa de aproximadamente 7 000 000 000 × 4,89 ÷ 8 bytes, ou cerca de 4,3 GB, antes de contar a janela de contexto e as ativações intermédias, que acrescentam mais consoante a quantidade de texto que lhe der. Este é um cálculo a partir do valor de bits por peso citado, não um número que qualquer um dos projetos publique diretamente, e o uso real de memória será maior assim que um pedido real e o seu contexto forem carregados. A orientação prática que resulta da tabela é simples: para uma tarefa como classificar uma linha de registo de ligação de cada vez, em que a entrada é curta e a saída necessária é um pequeno veredito estruturado, um modelo da classe Q4_K_M é muitas vezes a troca certa — visivelmente mais pequeno e mais rápido do que Q8_0 ou F16, sem descer ao nível de compressão extrema, onde a qualidade da saída se degrada de forma mais acentuada.

Nenhum dos projetos publica um requisito de memória por configuração de Mac, por isso a abordagem prática é trabalhar a partir da estimativa acima, deixando uma margem real: o próprio macOS, o seu navegador e o que mais estiver a correr também precisam de memória unificada, e um modelo que cabe à justa, sem mais nada aberto, vai trocar (swap) ou parar assim que mudar para outra aplicação. Num Mac com uma quantidade modesta de memória unificada, isso aconselha a manter-se mais próximo do extremo mais pequeno da tabela acima — alguns milhares de milhões de parâmetros a Q4_K_M em vez de um modelo muito maior à mesma quantização — e a reservar as opções maiores e de maior precisão para máquinas com memória de sobra. Para uma tarefa de classificação restrita como a de que trata este artigo, um modelo mais pequeno a quem se faz uma pergunta precisa tende a ser tanto mais rápido como, na prática, mais consistente do que um modelo maior a quem se dá uma pergunta vaga.

Ambos os projetos estão em desenvolvimento ativo, e a comparação honesta é menos "qual é melhor" do que "qual se encaixa no seu pipeline." O llama.cpp compila para um único binário sem necessidade de runtime Python em tempo de inferência, o que importa se quiser incorporá-lo dentro de outra peça de software sem enviar também um interpretador Python. O MLX assume que já está a trabalhar em Python (ou em Swift, para o qual também disponibiliza bindings) e recompensa isso com uma integração mais estreita na própria pilha de aprendizagem automática da Apple e no modelo de memória unificada descrito acima. Uma ferramenta de segurança construída como uma aplicação macOS autónoma, que é a situação em que o próprio FireAI se encontra, fica mais próxima do extremo do llama.cpp desse espetro; um caderno de investigação a explorar qual o padrão de instrução que funciona melhor fica mais próximo do extremo do MLX.

Um padrão de instrução para triar uma ligação

Quanto mais estreita a pergunta que se faz a um pequeno modelo local, mais fiavelmente ele responde. Para uma única ligação, isso significa dar-lhe exatamente os campos que um revisor humano observaria — nada mais, nada inferido — e pedir um veredito estruturado, em vez de texto corrido livre:

modelo de instrução de triagem (ilustrativo, não testado contra nenhum conjunto de dados)
System: You review one outbound network connection at a time. You are
given only the fields listed below. Do not assume anything not stated.
Respond with exactly two lines: a verdict (allow, ask, or block) and a
one-sentence reason a non-expert could understand.

Connection:
  app: UpdaterHelper.app
  code_signature: unsigned
  destination: 91.203.xxx.xxx:4444
  protocol: TCP
  threat_feed_hit: none
  first_seen: yes (no prior rule for this app)

Verdict:

Os campos que importam são os que uma verificação de assinatura de código e um feed de ameaças conseguem de facto produzir sem adivinhar: se o binário está assinado e por quem, para onde está a tentar ligar-se e em que porta, se esse destino aparece num feed de ameaças, e se é a primeira vez que esta aplicação tenta ligar-se. Pedir uma saída fixa de duas linhas, em vez de uma explicação aberta, torna o resultado mais fácil de registar, mais fácil de comparar entre milhares de ligações, e muito mais difícil de o modelo encher com linguagem evasiva que soa autorizada sem dizer nada verificável.

Um modelo pequeno vai, ocasionalmente, ignorar o formato pedido de qualquer forma — três linhas em vez de duas, uma ressalva extra, uma palavra de veredito que não é nenhuma das três que pediu. Trate isso como um problema de engenharia, não de modelação: valide a saída contra a forma exata que espera, e se não corresponder, ou volte a perguntar ou recorra ao veredito mais seguro (ask, ou seja, mostrar a um humano), em vez de tentar analisar uma resposta mais solta. Um passo de triagem que falha em segurança perante uma saída malformada é muito mais útil do que um que ocasionalmente produz uma linha de aparência confiante mas impossível de analisar, e a descarta em silêncio.

Corra este padrão ao longo de um dia inteiro de tentativas de ligação, em vez de uma de cada vez, e a forma da carga de trabalho muda: a maioria das ligações vêm de aplicações com uma regra já existente e nunca chegam a alcançar o modelo, um número menor são genuinamente vistas pela primeira vez e recebem um veredito, e apenas uma fração desses veredictos é outra coisa que não uma permissão de rotina. O verdadeiro trabalho do modelo, nesse ponto, não é ser um perito em segurança; é reduzir uma longa lista de ligações vistas pela primeira vez ao pequeno subconjunto que uma pessoa realmente precisa de ver, o que é um objetivo muito mais alcançável para um modelo de poucos milhares de milhões de parâmetros do que seria um juízo de segurança aberto.

Onde isto corre mal

  • Um pequeno modelo local pode produzir uma razão confiante, bem escrita e inteiramente errada. Nada em correr localmente muda esse risco; só muda onde o erro acontece, não se pode acontecer.
  • As janelas de contexto são finitas, e um registo longo e ruidoso não cabe. Resumir ou pré-filtrar antes de o modelo ver os dados introduz o seu próprio ponto de falha — pode perder a única linha que importava antes de o modelo sequer ter oportunidade de a ver.
  • O modelo só vê o que a linha de registo contém. Não consegue ver dentro de tráfego cifrado, não consegue verificar se um feed de ameaças está atualizado, e não consegue conhecer a sua intenção — uma ligação de uma ferramenta que acabou de instalar de propósito parece idêntica a uma de uma ferramenta de que nunca ouviu falar.
  • Um veredito não é uma ação. Nada aqui deve bloquear, apagar ou permitir silenciosamente uma ligação por si só; uma pessoa continua a precisar de ver a razão e confirmá-la, e de poder reverter a decisão se o modelo se enganou.

Esse último ponto não é uma limitação específica de um pequeno modelo quantizado a correr num portátil — é verdade para toda a decisão de segurança automatizada, local ou na nuvem, modelo pequeno ou grande. O valor de o correr localmente está no que remove da equação (uma dependência de rede, uma fatura recorrente, um terceiro a receber os seus metadados de ligação), não numa alegação de que remove a necessidade de um humano no ciclo.

Onde o FireAI encaixa neste padrão

O FireAI, a firewall da HisnLabs para Mac, inclui algo construído sobre a mesma ideia, mas com um âmbito mais estreito do que um modelo de chat de uso geral: um modelo local opcional, uma transferência adicional de cerca de 1,5 GB, que corre inteiramente no Mac e revê ligações de aplicações que ainda não têm nenhuma regra. Exige macOS 14 ou posterior em silício da Apple, aplica feeds de ameaças públicos localmente em vez de os verificar contra um serviço remoto, e mostra a razão da sua decisão no mesmo pedido de permissão que usa para perguntar se deve permitir ou negar a ligação — cada uma dessas decisões torna-se numa regra visível e editável, e qualquer uma delas pode ser desfeita. Vale a pena ser preciso sobre o que isto é e não é: não é o modelo de chat aberto, de poucos milhares de milhões de parâmetros, que este artigo tem vindo a descrever, e não é um antivírus nem uma VPN — faz uma única tarefa restrita de classificação, localmente, e deixa a decisão final a quem lê o pedido.

O papel do FireAI e da HisnLabs

FireAI’s own connection reviewer is this exact bet, made narrower still: an optional, roughly 1.5 GB on-device model, macOS 14 and up, Apple silicon only, reviewing one thing (should this unknown app reach this destination) with a visible reason and an undo button — and, like the triage pattern in this article, it still needs a person to confirm anything consequential.

O FireAI é o produto da HisnLabs: uma firewall com IA que corre diretamente no seu Mac. Mostra, em linguagem clara, cada ligação que as suas aplicações fazem e deixa-o decidir o que sai do seu Mac — a IA funciona localmente, pelo que o seu tráfego nunca é enviado para nós nem para mais ninguém. A equipa de investigação em segurança da HisnLabs é quem mantém essas decisões fiáveis: cataloga que domínios são simples telemetria e quais são um serviço real, acompanha o país e a rede por detrás de uma ligação e treina o modelo local (a funcionalidade Autopilot) com padrões de tráfego reais, sem que nada disso saia do seu Mac.

Pode ler as decisões técnicas por detrás dele, ou experimentar o FireAI durante 17 dias, em FireAI, da HisnLabs.

Fontes