O blog de segurança da FireAI

Por FireAI Security & Research Team · Publicado

Rodando um LLM local em Apple silicon para triagem de segurança

Rodando um LLM local em Apple silicon para triagem de segurança

Um modelo que classifica uma conexão de rede como digna de atenção ou não, rodando no mesmo Mac que fez a conexão, muda três coisas ao mesmo tempo: o que sai da máquina, quanto custa rodar e se continua funcionando quando a própria rede é o que está sob suspeita. As três coisas 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 log de conexão para um modelo na nuvem para classificação significa transmitir, a cada decisão, qual aplicativo no seu Mac está falando com qual host, em qual porta, agora mesmo. Nada disso é secreto da forma como uma senha é, mas é exatamente o tipo de metadado que uma configuração preocupada com segurança tenta minimizar compartilhar, e uma ferramenta cujo propósito inteiro é decidir o que o seu Mac pode enviar para fora tem um motivo óbvio para não depender, ela mesma, de enviar algo para fora a cada decisão que toma. Rodar o modelo localmente elimina essa dependência por completo: a linha de log nunca sai do dispositivo, porque não existe nenhuma chamada de rede no caminho da decisão.

O segundo motivo é a continuidade. Uma etapa de triagem baseada em nuvem só está disponível na medida da sua conexão de internet e da API do fornecedor, e essas são exatamente as coisas que podem ficar degradadas ou ser deliberadamente cortadas durante um incidente real. Um modelo rodando na memória local continua respondendo com a rede fora do ar, o Wi-Fi desligado, ou um comprometimento suspeito em andamento no mesmo link que a chamada de API teria usado.

MLX: o próprio framework de arrays da Apple

O MLX é um framework de arrays construído pela equipe de pesquisa em aprendizado de máquina da Apple especificamente para Apple silicon, com APIs em Python, C++, C e Swift. Sua própria documentação descreve um modelo de memória unificada como sua escolha de design definidora: os arrays no MLX vivem em memória compartilhada, e operações sobre eles podem rodar em qualquer dispositivo suportado — CPU ou GPU — sem que os pesos do modelo precisem ser copiados entre pools de memória separados antes. Isso importa concretamente para um laptop: uma máquina com GPU dedicada precisa copiar os pesos de um modelo por um barramento até a memória da GPU antes de conseguir computar qualquer coisa, o que custa tempo e dobra o consumo de memória; na arquitetura de memória unificada de um Mac, a CPU e a GPU já compartilham a mesma memória física, então não há nada para copiar.

O mlx-lm, o pacote complementar para rodar modelos de linguagem no MLX, se instala com um único comando e já vem com um modelo padrão pronto para uso:

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 exigir um runtime Python no momento da inferência. Seu próprio README declara diretamente a posição do projeto sobre esse hardware: o Apple silicon é tratado, nas palavras do projeto, como "um cidadão de primeira classe — otimizado via ARM NEON, Accelerate e frameworks Metal", e sua documentação de build confirma que, no macOS, o backend de GPU Metal vem ativado por padrão, com uma flag em tempo de build para desativá-lo caso você especificamente queira inferência somente por CPU.

Build 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 arquivos GGUF, um formato de modelo em arquivo único que reúne os pesos quantizados e tudo o que é necessário para rodá-los; o comando acima puxa um diretamente de um repositório do Hugging Face pelo nome. O MLX, por outro lado, mantém-se mais próximo de um fluxo de trabalho nativo em Python, convertendo e quantizando modelos para seu próprio formato com antecedência. Nenhum dos dois é estritamente melhor: o llama.cpp é a opção para quem quer um único binário compilado sem dependência de Python; o MLX é a opção para quem já está construindo o restante do pipeline em Python e quer acesso de primeira classe ao modelo de memória unificada da Apple a partir dali.

Quantização: o que você de fato troca por um modelo menor

A quantização armazena cada peso do modelo em menos bits do que os 16 ou 32 em que o modelo foi treinado, reduzindo tanto o arquivo em disco quanto a memória necessária para rodá-lo, a algum custo na qualidade da saída. A própria documentação de quantização do llama.cpp lista os números exatos de bits por peso para cada esquema que suporta, uma forma mais precisa de pensar sobre essa troca do que o costumeiro atalho de "4 bits" ou "8 bits":

Da documentação da ferramenta de quantização do llama.cpp.
Família de esquemaExemplos de esquemasBits por peso
Precisão total (referência)F1616,0
Quase sem perdasQ8_08,50
Faixa intermediária de qualidade mais altaQ5_K_S / Q5_K_M5,57 - 5,70
Equilíbrio entre qualidade e tamanhoQ4_K_S / Q4_K_M4,67 - 4,89
Menor, com mais perdasQ3_K_S / Q3_K_M / Q3_K_L3,64 - 4,30
Compressão extremaIQ2_XXS ... IQ2_M2,00 - 2,93

Multiplicar a contagem de parâmetros de um modelo pelo seu número de bits por peso dá uma estimativa aproximada de memória só para os pesos: um modelo de 7 bilhões de parâmetros nos 4,89 bits por peso do Q4_K_M 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 intermediárias, que somam mais em cima dependendo de quanto texto você fornece. Esse é um cálculo a partir do número de bits por peso citado, não um número que qualquer um dos dois projetos publica diretamente, e o uso real de memória vai ficar mais alto assim que um prompt real e seu contexto forem carregados. A orientação prática que decorre da tabela é simples: para uma tarefa como classificar uma linha de log de conexão por vez, em que a entrada é curta e a saída exigida é um pequeno veredito estruturado, um modelo da classe Q4_K_M costuma ser a troca certa — perceptivelmente menor e mais rápido do que o Q8_0 ou o F16, sem cair no nível de compressão extrema, em que a qualidade da saída se degrada de forma mais acentuada.

Nenhum dos dois projetos publica um requisito de memória por configuração de Mac, então a abordagem prática é trabalhar de trás para frente a partir da estimativa acima e deixar uma folga real: o próprio macOS, seu navegador e o que mais estiver rodando também precisam de memória unificada, e um modelo que mal cabe sem nada mais aberto vai começar a trocar memória com o disco ou travar assim que você mudar para outro aplicativo. Em um Mac com uma quantidade modesta de memória unificada, isso favorece ficar mais perto da ponta menor da tabela acima — alguns bilhões de parâmetros no Q4_K_M em vez de um modelo bem maior na mesma quantização — e reservar as opções maiores e de maior precisão para máquinas com memória sobrando. Para uma tarefa de classificação restrita como a deste artigo, um modelo menor recebendo uma pergunta precisa tende a ser tanto mais rápido quanto, na prática, mais consistente do que um modelo maior recebendo uma pergunta vaga.

Os dois projetos estão em desenvolvimento ativo, e a comparação honesta é menos "qual é melhor" e mais "qual se encaixa no seu pipeline". O llama.cpp compila para um único binário sem exigir runtime Python no momento da inferência, o que importa se você quer embutir isso dentro de outro software sem distribuir um interpretador Python junto. O MLX assume que você já está trabalhando em Python (ou Swift, para o qual também oferece bindings) e recompensa isso com integração mais estreita à própria pilha de aprendizado de máquina da Apple e ao modelo de memória unificada descrito acima. Uma ferramenta de segurança construída como um aplicativo macOS independente, que é exatamente a situação do próprio FireAI, fica mais próxima do lado do llama.cpp desse espectro; um notebook de pesquisa explorando qual padrão de prompt funciona melhor fica mais próximo do lado do MLX.

Um padrão de prompt para triar uma conexão

Quanto mais restrita a pergunta que você faz a um modelo local pequeno, mais confiável é a resposta. Para uma única conexão, isso significa fornecer exatamente os campos que um revisor humano olharia — nada mais, nada inferido — e pedir um veredito estruturado em vez de um texto livre:

modelo de prompt 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 exatamente os que uma checagem de assinatura de código e uma lista de ameaças conseguem de fato produzir sem chutar: se o binário está assinado e por quem, para onde está tentando se conectar e em qual porta, se esse destino aparece em uma lista de ameaças, e se essa é a primeira vez que esse aplicativo tenta se conectar. Pedir uma saída fixa de duas linhas, em vez de uma explicação aberta, torna o resultado mais fácil de registrar, mais fácil de comparar entre milhares de conexões, e muito mais difícil de o modelo encher com linguagem evasiva que soa autoritativa sem dizer nada verificável.

Um modelo pequeno vai, de vez em quando, ignorar o formato pedido mesmo assim — três linhas em vez de duas, uma ressalva extra, uma palavra de veredito que não é uma das três pedidas. Trate isso como um problema de engenharia, não de modelagem: valide a saída contra o formato exato esperado e, se não corresponder, ou peça de novo ou recorra ao veredito mais seguro (ask, ou seja, mostrar a um humano) em vez de tentar interpretar uma resposta mais solta. Uma etapa de triagem que falha com segurança diante de uma saída malformada é muito mais útil do que uma que ocasionalmente produz uma linha de aparência confiante mas impossível de interpretar e a descarta silenciosamente.

Rode esse padrão ao longo de um dia inteiro de tentativas de conexão, em vez de uma de cada vez, e o formato da carga de trabalho muda: a maioria das conexões vem de aplicativos com uma regra já existente e nunca chega a alcançar o modelo, um número menor é genuinamente inédito e recebe um veredito, e só uma fração desses vereditos é algo diferente de uma liberação rotineira. O trabalho real do modelo, nesse ponto, não é ser um especialista em segurança; é reduzir uma longa lista de conexões inéditas ao pequeno subconjunto que uma pessoa de fato precisa examinar, uma meta muito mais alcançável para um modelo de alguns bilhões de parâmetros do que um julgamento de segurança aberto seria.

Onde isso dá errado

  • Um modelo local pequeno pode produzir um motivo confiante, bem escrito e inteiramente errado. Rodar localmente não muda nada nesse risco; muda apenas onde o erro acontece, não se ele pode acontecer.
  • Janelas de contexto são finitas, e um log longo e ruidoso não cabe. Resumir ou pré-filtrar antes de o modelo ver os dados introduz seu próprio ponto de falha — você pode perder exatamente a linha que importava antes de o modelo ter a chance de olhá-la.
  • O modelo só vê o que a linha de log contém. Ele não consegue ver dentro de tráfego criptografado, não consegue verificar se uma lista de ameaças está atualizada, e não consegue saber sua intenção — uma conexão de uma ferramenta que você acabou de instalar de propósito parece idêntica a uma de uma ferramenta da qual você nunca ouviu falar.
  • Um veredito não é uma ação. Nada aqui deve bloquear, apagar ou permitir silenciosamente uma conexão por conta própria; uma pessoa ainda precisa ver o motivo e confirmá-lo, e conseguir reverter a decisão se o modelo tiver errado.

Esse último ponto não é uma limitação específica de um modelo pequeno e quantizado rodando em um laptop — é verdade para toda decisão de segurança automatizada, local ou na nuvem, modelo pequeno ou grande. O valor de rodá-lo localmente está no que ele remove da equação (uma dependência de rede, uma cobrança recorrente, um terceiro recebendo os metadados da sua conexão), não em uma alegação de que remove a necessidade de um humano no ciclo.

Onde o FireAI se encaixa nesse padrão

O FireAI, o firewall para Mac da HisnLabs, traz algo construído sobre a mesma ideia, com um escopo mais restrito do que um modelo de chat de propósito geral: um modelo local opcional, um download adicional de aproximadamente 1,5 GB, que roda inteiramente no Mac e analisa conexões de aplicativos que ainda não têm regra. Ele exige o macOS 14 ou posterior em Apple silicon, aplica listas públicas de ameaças localmente em vez de consultá-las em um serviço remoto, e mostra o motivo da sua decisão no mesmo aviso de permissão que usa para perguntar se você permite ou nega a conexão — cada uma dessas decisões se torna uma regra visível e editável, e qualquer uma delas pode ser desfeita. Vale ser preciso sobre o que isso é e o que não é: não é o modelo de chat aberto, de alguns bilhões de parâmetros, que este artigo descreveu, e não é um antivírus nem uma VPN — ele faz um trabalho de classificação restrito, localmente, e deixa a decisão final para quem lê o aviso.

Onde FireAI e HisnLabs entram nessa história

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: um firewall com IA que roda direto no seu Mac. Ele mostra, em linguagem simples, cada conexão que seus aplicativos fazem e deixa você decidir o que sai do seu Mac — a IA roda localmente, então seu tráfego nunca é enviado para nós nem para ninguém. A equipe de pesquisa em segurança da HisnLabs é quem mantém essas decisões confiáveis: cataloga quais domínios são telemetria comum e quais são um serviço de verdade, rastreia o país e a rede por trás de uma conexão e treina o modelo local (o recurso Autopilot) com padrões reais de tráfego, sem que nada disso saia do seu Mac.

Você pode ler as decisões técnicas por trás dele, ou testar o FireAI por 17 dias, em FireAI, da HisnLabs.

Fontes