Um coding agent local, offline, com modelo de 26B parâmetros a correr a 72 tokens por segundo. Sem cloud, sem latência, sem dependências externas.

Ficaste sem internet a meio de uma sessão de trabalho e perdeste o acesso ao teu coding agent favorito? Aconteceu-me mais vezes do que gostava. A alternativa é montar um setup que corre tudo na máquina: modelo, servidor e agente: sem precisar de ligação externa.

Neste guia vou mostrar-te como construir um coding agent local completo no macOS usando o llama.cpp como runtime de inferência, o Gemma 4 26B-A4B (em GGUF quantizado) com speculative decoding via MTP, e o Pi como terminal coding agent. O resultado final é um servidor compatível com a API OpenAI que qualquer ferramenta consegue usar.

Requisitos de Hardware

Este setup foi testado num Apple M1 Max com 64 GB de memória unificada a correr macOS 15.7.7. Mas é adaptável:

  • M1/M2/M3/M4 Pro ou Max com pelo menos 32 GB de RAM: consegues correr o modelo Q4 com contexto até 64K
  • 16 GB de RAM: possível com contexto mais reduzido (16K-32K) ou modelos mais pequenos (Gemma 4 12B)
  • Apple Silicon (qualquer): o Metal acceleration funciona em todos os M-series

O modelo principal em Q4_K_XL ocupa ~16 GB. Com o MTP draft model e o projetor, a diretoria de modelos fica com ~17 GB. Precisas de ~20-24 GB de memória livre para correr tudo confortavelmente com contexto de 64K.

Stack Final

Camada Escolha
Inference runtime llama.cpp
Aceleração macOS Metal + Accelerate
Modelo principal gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf
Draft model (MTP) gemma-4-26B-A4B-it-Q8_0-MTP.gguf
MTP setting --spec-draft-n-max 3
Projetor multimodal mmproj-BF16.gguf
Servidor llama-server em 127.0.0.1:8080
API Compatível OpenAI /v1
Coding agent Pi
Input do modelo ["text", "image"]

Passo 1: Instalar o llama.cpp

O llama.cpp é o runtime de inferência mais otimizado para execução local de modelos GGUF. A versão com aceleração Metal aproveita ao máximo os Neural Engine e GPU dos Apple Silicon.

Começa por instalar as dependências:

brew install cmake git tmux [email protected]

O tmux é opcional mas útil para manter o servidor a correr em segundo plano numa sessão destacada.

Cria a estrutura de diretorias e clona o repositório:

mkdir -p ~/Developer/ML-Models/Gemma4/repos
cd ~/Developer/ML-Models/Gemma4

git clone https://github.com/ggml-org/llama.cpp repos/llama.cpp

cd repos/llama.cpp

Compila com suporte Metal e Accelerate. O GGML_METAL=ON ativa a computação na GPU via Metal Performance Shaders. O GGML_ACCELERATE=ON usa o framework Accelerate da Apple para operações BLAS otimizadas.

cmake -B build \
  -DCMAKE_BUILD_TYPE=Release \
  -DGGML_METAL=ON \
  -DGGML_ACCELERATE=ON \
  -DGGML_BLAS=ON \
  -DGGML_BLAS_VENDOR=Apple

cmake --build build --config Release -j

O flag -j usa todos os cores disponíveis para acelerar a compilação. O processo demora 2-3 minutos num M1 Max.

No final, os binários estão em repos/llama.cpp/build/bin/. Vais precisar especialmente do llama-server e do llama-cli.


Passo 2: Descarregar os Modelos

Os modelos estão no HuggingFace, publicados pela Unsloth. Precisamos de três ficheiros: o modelo principal (quantizado Q4_K_XL), o draft model MTP (Q8_0), e o projetor multimodal.

Cria um ambiente Python e instala as ferramentas de download:

cd ~/Developer/ML-Models/Gemma4
python3.11 -m venv .venv
source .venv/bin/activate
pip install -U huggingface_hub hf_xet

O hf_xet é o novo backend de transferência do HuggingFace que acelera downloads de ficheiros grandes usando content-addressable storage e compressão delta. Sem ele, downloads de ficheiros com 16 GB+ podem ser instáveis.

Descarrega os três ficheiros:

mkdir -p models/unsloth-gemma-4-26B-A4B-it-GGUF

huggingface-cli download unsloth/gemma-4-26B-A4B-it-GGUF \
  gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \
  mmproj-BF16.gguf \
  MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf \
  --local-dir models/unsloth-gemma-4-26B-A4B-it-GGUF

Estrutura final esperada:

models/unsloth-gemma-4-26B-A4B-it-GGUF/
├── gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf   # ~16 GB
├── mmproj-BF16.gguf                       # ~1.2 GB
└── MTP/
    └── gemma-4-26B-A4B-it-Q8_0-MTP.gguf  # ~1 GB

Porquê estes ficheiros?

  • UD-Q4_K_XL: Quantização dinâmica 4-bit da Unsloth que usa Importance Matrix para distribuir os bits de forma inteligente entre os parâmetros. Qualidade muito próxima do FP16 mas com 1/4 do tamanho.
  • Q8_0-MTP: O draft model em 8-bit. É mais pequeno e rápido que o modelo principal, usado para gerar candidatos a tokens que o modelo principal depois valida. O MTP (Multi-Token Prediction) é uma forma de speculative decoding específica do Gemma 4.
  • mmproj-BF16.gguf: O projetor multimodal que permite ao modelo processar imagens. Sem ele, o modelo só aceita texto. O Gemma 4 26B não é nativamente multimodal: ao contrário do 12B: por isso precisa deste projetor extra.

Passo 3: Benchmark: Modelo Base vs MTP

O que é MTP (Multi-Token Prediction)?

Isto é diferente do speculative decoding tradicional (que usa um modelo mais pequeno como draft). No MTP, o draft head faz parte da arquitetura do modelo: foi treinado em conjunto: o que resulta em propostas de maior qualidade e menos rejeições.

Benchmark base (sem MTP)

repos/llama.cpp/build/bin/llama-cli \
  -m models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \
  -ngl 999 \
  -fa on \
  -c 4096 \
  -n 128 \
  -p "Write a compact Python function that parses a unified diff and returns the changed file paths. Then explain two edge cases."

Resultado:

Setup Prompt tok/s Generation tok/s
Gemma 4 26B-A4B Q4, llama.cpp Metal 298.0 58.2

58 tokens por segundo é utilizável, mas para coding agent work: onde o modelo faz dezenas de tool calls por tarefa: cada décima de segundo conta.

Benchmark com MTP

repos/llama.cpp/build/bin/llama-cli \
  -m models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \
  --model-draft models/unsloth-gemma-4-26B-A4B-it-GGUF/MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf \
  --spec-type draft-mtp \
  --spec-draft-n-max 3 \
  -ngl 999 \
  -fa on \
  -c 4096 \
  -n 128 \
  -p "Write a compact Python function that parses a unified diff and returns the changed file paths. Then explain two edge cases."

Resultado:

Setup Prompt tok/s Generation tok/s Speedup
Modelo base 298.0 58.2 1.00x
Modelo + Q8 MTP draft 295.6 72.2 1.24x

O prompt processing manteve-se praticamente igual (~296 tok/s vs 298 tok/s), mas a geração melhorou 24%. Num cenário real de coding agent onde o modelo gera centenas de tokens por tool call, isto traduz-se em segundos poupados por iteração.

Ajustar o --spec-draft-n-max

A Unsloth recomenda testar valores de 1 a 6 porque o valor ótimo depende do hardware. No M1 Max com 64 GB, os resultados foram:

--spec-draft-n-max Prompt tok/s Generation tok/s
1 295.5 68.4
2 299.1 72.0
3 295.6 72.2
4 297.3 70.7
5 297.9 63.7
6 296.3 61.2

O valor 3 foi o mais rápido, com o 2 muito próximo (72.0 vs 72.2). A partir de 4 a performance degrada-se porque o draft model começa a fazer demasiadas propostas que o modelo principal rejeita, desperdiçando ciclos.

No teu hardware, testa com 1, 2 e 3: um destes três valores será o ideal.


Passo 4: Iniciar o Servidor

Com os modelos descarregados e o llama.cpp compilado, vamos iniciar o servidor com todas as otimizações ligadas.

Comando final do servidor

repos/llama.cpp/build/bin/llama-server \
  -m models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \
  --model-draft models/unsloth-gemma-4-26B-A4B-it-GGUF/MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf \
  --mmproj models/unsloth-gemma-4-26B-A4B-it-GGUF/mmproj-BF16.gguf \
  --spec-type draft-mtp \
  --spec-draft-n-max 3 \
  -ngl 999 \
  -fa on \
  -c 65536 \
  --parallel 1 \
  --host 127.0.0.1 \
  --port 8080

Explicação de cada flag:

Flag O que faz
-m Caminho para o modelo principal (GGUF)
--model-draft Caminho para o draft model MTP
--mmproj Projetor multimodal para processar imagens
--spec-type draft-mtp Ativa speculative decoding com MTP (obrigatório para usar o draft model do Gemma 4)
--spec-draft-n-max 3 Número máximo de draft tokens por passo (3 = ideal neste hardware)
-ngl 999 Offload de todas as layers para a GPU (Metal). 999 = todas
-fa on Flash Attention: reduz uso de memória e acelera atenção para contextos longos
-c 65536 Context window de 64K tokens (equilíbrio entre capacidade e uso de RAM)
--parallel 1 Apenas um slot de processamento (para uso exclusivo do Pi)
--host 127.0.0.1 Apenas localhost: nada exposto à rede
--port 8080 Porta do servidor

O endpoint OpenAI-compatível fica em http://127.0.0.1:8080/v1.

Script com tmux

Para não ficares com o servidor preso ao terminal, cria um script start_server.sh que corre dentro de uma sessão tmux:

#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$HOME/Developer/ML-Models/Gemma4"
SESSION_NAME="${SESSION_NAME:-gemma4-server}"
HOST="${HOST:-127.0.0.1}"
PORT="${PORT:-8080}"
CTX_SIZE="${CTX_SIZE:-65536}"
PARALLEL="${PARALLEL:-1}"

LLAMA_SERVER="$ROOT_DIR/repos/llama.cpp/build/bin/llama-server"
MODEL="$ROOT_DIR/models/unsloth-gemma-4-26B-A4B-it-GGUF/gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf"
DRAFT_MODEL="$ROOT_DIR/models/unsloth-gemma-4-26B-A4B-it-GGUF/MTP/gemma-4-26B-A4B-it-Q8_0-MTP.gguf"
MMPROJ="$ROOT_DIR/models/unsloth-gemma-4-26B-A4B-it-GGUF/mmproj-BF16.gguf"
LOG_FILE="$ROOT_DIR/logs/llama-server-mtp.log"

mkdir -p "$ROOT_DIR/logs"

tmux new-session -d -s "$SESSION_NAME" -c "$ROOT_DIR" \
  "$LLAMA_SERVER \
    -m '$MODEL' \
    --model-draft '$DRAFT_MODEL' \
    --mmproj '$MMPROJ' \
    --spec-type draft-mtp \
    --spec-draft-n-max 3 \
    -ngl 999 \
    -fa on \
    -c '$CTX_SIZE' \
    --parallel '$PARALLEL' \
    --host '$HOST' \
    --port '$PORT' \
    2>&1 | tee -a '$LOG_FILE'"

Torna-o executável e corre:

chmod +x start_server.sh
./start_server.sh

Para ver o output do servidor:

tmux attach -t gemma4-server

Para desligar (Ctrl+B, depois d) sem parar o servidor.

Verificar que está online

curl http://127.0.0.1:8080/v1/models

Deverás receber um JSON com o modelo disponível.


Passo 5: Configurar o Pi (Coding Agent)

O Pi é um terminal coding agent minimalista do Mario Zechner. Dá ao modelo quatro ferramentas: read, write, edit, bash: e gasta poucos tokens. O system prompt é pequeno, o que é importante quando estás a correr um modelo local.

Instalar o Pi

npm install -g @mariozechner/pi-coding-agent

Configurar o provider local

O Pi lê os providers de modelos de ~/.pi/agent/models.json. Cria ou edita esse ficheiro:

{
  "providers": {
    "gemma4-local": {
      "name": "Gemma 4 Local",
      "baseUrl": "http://127.0.0.1:8080/v1",
      "api": "openai-completions",
      "apiKey": "local",
      "authHeader": false,
      "compat": {
        "supportsDeveloperRole": false,
        "supportsReasoningEffort": false
      },
      "models": [
        {
          "id": "gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf",
          "name": "Gemma 4 26B-A4B Q4 + MTP",
          "reasoning": false,
          "input": ["text", "image"],
          "contextWindow": 65536,
          "maxTokens": 8192,
          "cost": {
            "input": 0,
            "output": 0,
            "cacheRead": 0,
            "cacheWrite": 0
          }
        }
      ]
    }
  }
}

Explicação dos campos:

  • baseUrl: Aponta para o llama-server local. O /v1 é o prefixo da API compatível com OpenAI.
  • api: "openai-completions": Diz ao Pi para usar o formato de chat completions da OpenAI.
  • authHeader: false: O servidor local não precisa de autenticação. Se pores true, o Pi envia Authorization: Bearer local em cada request.
  • input: ["text", "image"]: Crucial para suporte multimodal. Sem "image", o Pi não envia imagens para o modelo: mesmo que o --mmproj esteja carregado no servidor.
  • reasoning: false: O Gemma 4 26B não tem modo “thinking” como o Qwen3.6.

Definir como provider padrão (opcional)

Em ~/.pi/agent/settings.json:

{
  "defaultProvider": "gemma4-local",
  "defaultModel": "gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf",
  "defaultThinkingLevel": "minimal"
}

Verificar que o Pi reconhece o modelo

pi --offline --list-models gemma

Deverás ver:

provider      model                               context  max-out  thinking  images
gemma4-local  gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf  65.5K    8.2K     no        yes

Usar o Pi interativamente

pi --provider gemma4-local --model gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf

Usar o Pi em modo non-interactive

pi -p --provider gemma4-local --model gemma-4-26B-A4B-it-UD-Q4_K_XL.gguf \
  "Explain what this repository does"

Enviar screenshots

pi -p @"/path/to/screenshot.png" "Describe this image and point out anything relevant to the UI"

O prefixo @ antes do path indica ao Pi que é um ficheiro para anexar. O modelo precisa do projetor multimodal carregado para processar a imagem.


Passo 6 (Alternativa): Qwen3.6 35B-A3B

O Qwen3.6 35B-A3B é a alternativa natural ao Gemma 4 para coding agents. Segundo os benchmarks, o Qwen é significativamente melhor a gerar código. A contrapartida é a velocidade: 55 tok/s vs 72 tok/s.

Descarregar os modelos

mkdir -p models/unsloth-Qwen3.6-35B-A3B-MTP-GGUF

huggingface-cli download unsloth/Qwen3.6-35B-A3B-MTP-GGUF \
  Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf \
  mmproj-BF16.gguf \
  --local-dir models/unsloth-Qwen3.6-35B-A3B-MTP-GGUF

Nota: ao contrário do Gemma 4, o Qwen3.6 já inclui o MTP embutido no próprio modelo GGUF. Não precisas de um ficheiro MTP separado.

Servidor (porta 8081 para não conflituar)

repos/llama.cpp/build/bin/llama-server \
  -m models/unsloth-Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf \
  --mmproj models/unsloth-Qwen3.6-35B-A3B-MTP-GGUF/mmproj-BF16.gguf \
  --spec-type draft-mtp \
  --spec-draft-n-max 3 \
  -ngl 999 \
  -fa on \
  -c 65536 \
  --parallel 1 \
  --host 127.0.0.1 \
  --port 8081

Config do Pi para Qwen3.6

Adiciona outro provider em ~/.pi/agent/models.json:

{
  "providers": {
    "qwen36-local": {
      "name": "Qwen3.6 Local",
      "baseUrl": "http://127.0.0.1:8081/v1",
      "api": "openai-completions",
      "apiKey": "local",
      "authHeader": false,
      "compat": {
        "supportsDeveloperRole": false,
        "supportsReasoningEffort": false
      },
      "models": [
        {
          "id": "Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf",
          "name": "Qwen3.6 35B-A3B Q4 + MTP",
          "reasoning": true,
          "input": ["text", "image"],
          "contextWindow": 65536,
          "maxTokens": 8192,
          "cost": {
            "input": 0,
            "output": 0,
            "cacheRead": 0,
            "cacheWrite": 0
          }
        }
      ]
    },
    "gemma4-local": {
      "...": "..."
    }
  }
}

Podes ter ambos os servidores a correr em portas diferentes e alternar entre eles no Pi com /model.


Benchmarks Comparativos

Testes no M1 Max 64 GB, prompt de 128 tokens, medições com llama-cli e mlx-lm:

Runtime Modelo Generation tok/s
llama.cpp Metal + MTP Unsloth GGUF Q4 + Q8 MTP 72.2
llama.cpp Metal Unsloth GGUF Q4 58.2
MLX-LM Unsloth UD MLX 4-bit 45.8
MLX-LM mlx-community 4-bit 43.9
MLX-LM mlx-community OptiQ 4-bit 38.1

Notas sobre os resultados:

  • llama.cpp com MTP é claramente a opção mais rápida no macOS. A diferença de 58.2 para 72.2 tok/s (24% de speedup) faz diferença em sessões longas de coding agent.
  • MLX é mais lento que o esperado. Apesar de ser um framework nativo da Apple para Apple Silicon, o llama.cpp (cross-platform) está mais maduro e otimizado para este tipo de carga.
  • MLX com OptiQ (optimized quantization) é o mais lento: a otimização de quantização não compensa a perda de velocidade de inferência.

Suporte a Imagens

O projetor multimodal (mmproj-BF16.gguf) é carregado com --mmproj no llama-server. Isto permite ao modelo processar imagens: úteis para dares screenshots de UIs que o agente está a construir ou depurar.

Para o Pi enviar imagens, a configuração do modelo tem de declarar "input": ["text", "image"]. Sem isso, o Pi trata o modelo como text-only e nunca envia o campo image_url no payload.

O Gemma 4 26B não é nativamente multimodal. O modelo 12B é que tem capacidades multimodais embutidas. Para o 26B, o projetor é um adaptador treinado pela Unsloth que converte embeddings visuais em espaço de texto que o modelo consegue interpretar.

Benchmark com e sem projetor:

Setup Projetor Prompt tok/s Generation tok/s
llama.cpp Metal + MTP nenhum 120.3 71.4
llama.cpp Metal + MTP mmproj-BF16.gguf 297.4 72.2

O projetor não degrada a performance de geração de texto: aliás, o prompt processing até apareceu mais rápido (provavelmente variação de medição). Podes deixá-lo carregado permanentemente sem preocupações.


Conclusão

Este guia mostrou como montar um coding agent local completo no macOS com llama.cpp, Gemma 4 26B-A4B, MTP, projetor multimodal e Pi. O resultado é um servidor compatível com API OpenAI que corre offline.

A API compatível com OpenAI significa que podes usar o mesmo servidor com outras ferramentas: OpenCode, continue.dev, ou editors com suporte OpenAI-compatible.

Recursos

Comentários (0)

Nenhum comentário ainda. Seja o primeiro!

Deixar comentário