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 ollama-serverlocal. 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 porestrue, o Pi enviaAuthorization: Bearer localem cada request.input: ["text", "image"]: Crucial para suporte multimodal. Sem"image", o Pi não envia imagens para o modelo: mesmo que o--mmprojesteja 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.
Comentários (0)
Nenhum comentário ainda. Seja o primeiro!
Deixar comentário