Configurações de Engenharia para Salvar LLMs Locais que Morrem no Docker do Mac
TuBrief 편집팀
2026년 8월 14일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
Ao baixar o código de um espaço de trabalho de IA de código aberto e executá-lo no Docker do Mac, você esbarra em um obstáculo logo no primeiro prompt. Embora você certamente esteja usando um chip da série M, apenas os ventiladores giram barulhentamente e uma velocidade desastrosa de pouco mais de 10 tokens por segundo é registrada. Isso acontece porque a camada de virtualização do Docker Desktop falha em passar a GPU Apple Silicon (Metal) para dentro do contêiner, fazendo com que o sistema recaia sobre o processamento bruto da CPU.
Se você rodar isso combinando um backend FastAPI e um banco de vetor local sem resolver esse gargalo, o contêiner vai cair devido a vazamentos de memória e esgotamento de conexões. Aqui estão quatro configurações que você precisa ajustar para transformar o código que você pegou apenas olhando o número de estrelas do GitHub em um nível de produção rodando no seu ambiente local.
O Docker Desktop no macOS roda em cima de uma VM Linux leve. Não existe uma função para repassar diretamente a API da GPU Metal do host para dentro do contêiner do SO convidado Linux. O Ollama ou llama.cpp dentro do contêiner falham na alocação de VRAM e silenciosamente alternam para o processamento de CPU. É por isso que um modelo Llama 3 8B que atingia de 40 a 80 t/s de forma nativa despenca para níveis de 21,45 t/s na avaliação de prompts e 12,17 t/s na geração de tokens assim que entra no Docker.
`
[Mecanismo de Ocorrência de Fallback de CPU dentro do Contêiner de Virtualização]
+-----------------------------------------------------------------------+
| Docker Container (Linux Guest OS) |
| +---------------------+ |
| | Local LLM Engine | --(VRAM Allocation Req)--> [VirtGPU Missing] |
| +---------------------+ | |
| | v |
| +<--(Fallback to CPU Execution)--- [Silent slog.Debug] |
+-------------|---------------------------------------------------------+
v
[Apple Silicon Host CPU (ARM Neon/DotProd)] -> Ocorre queda de velocidade
`
A configuração padrão de memória compartilhada (shm) também é um problema. Com a cota padrão de 64MB, ocorre um Erro de Barramento (Bus Error) imediato durante cálculos de tensores em grande escala. Abra o arquivo ~/.docker/daemon.json para expandir a memória compartilhada e liberar os limites de recursos.
`json
{
"builder": {
"gc": {
"defaultKeepStorage": "20GB",
"enabled": true
}
},
"experimental": false,
"default-shm-size": "8g"
}
`
`yaml
version: '3.8'
services:
llm-inference-engine:
image: ollama/ollama:latest
container_name: local_llm_engine
shm_size: '16gb'
ipc: host
deploy:
resources:
limits:
cpus: '8.0'
memory: 24G
reservations:
cpus: '4.0'
memory: 12G
ports:
- "11434:11434"
volumes:
- ollama_storage:/root/.ollama
volumes:
ollama_storage:
`
Para extrair a aceleração de GPU de dentro do contêiner, você precisa migrar para o Podman, utilizando o monitor de máquina virtual libkrun e o driver krunkit. Trata-se de um método que encaminha as requisições de computação Vulkan de dentro do contêiner para a API Metal do macOS host (Virtio-GPU Venus). Isso eleva a velocidade de processamento para até 75% em comparação com o Metal nativo.
brew tap slp/krunkit && brew install krunkit podmanexport CONTAINERS_MACHINE_PROVIDER="libkrun" && podman machine init --cpus 8 --memory 32768 && podman machine startpodman machine ssh "ls -la /dev/dri"`dockerfile
FROM fedora:40
RUN dnf -y install dnf-plugins-core &&
dnf -y copr enable slp/mesa-krunkit fedora-40-aarch64 &&
dnf -y install mesa-vulkan-drivers vulkan-loader vulkan-tools &&
dnf -y downgrade mesa-vulkan-drivers.aarch64 --repo copr:copr.fedorainfracloud.org:slp:mesa-krunkit &&
dnf clean all
ENV GGML_VULKAN=1
`
Se você precisar usar apenas o cálculo de CPU devido a regulamentações de infraestrutura, utilize o formato de quantização Q4_0_4_4 ajustado para as instruções vetoriais ARMv8.4-A DotProduct e Neon. Isso protege a velocidade de avaliação de prompts em até 50,63 t/s, reduzindo o tempo de processamento para menos da metade em comparação com a execução padrão de CPU no Docker.
| Método de Configuração de Runtime | Avaliação de Prompt (t/s) | Geração de Tokens (t/s) | Dificuldade de Construção | Características |
|---|---|---|---|---|
| Docker Desktop (CPU Padrão) | ~21,45 | ~12,17 | Baixa | Fallback de CPU devido a restrições de virtualização |
| Docker Container (ARM Q4_0_4_4) | ~50,63 | ~14,01 | Média | Utilização de instruções vetoriais ARM Neon |
| Podman + libkrun (Vulkan Venus) | ~75% do Nativo | ~75% do Nativo | Alta | Aceleração Virtio-GPU dentro do contêiner |
| Host Native Metal + Container | 100% Nativo (40~80) | 100% Nativo | Média | Estrutura de chamada direta ao motor do host |
Backends de código aberto frequentemente apresentam um gerenciamento de estado de tarefas assíncronas ou um tratamento do coletor de lixo (GC) do CPython bastante precários. À medida que as solicitações se acumulam, a memória incha até que o processo morre. Utilize o tracemalloc para identificar os pontos que consomem memória.
`python
import gc
import tracemalloc
tracemalloc.start()
def log_memory_snapshot():
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("[Memory Debug] Top 5 Allocations:")
for stat in top_stats[:5]:
print(stat)
gc.collect()
`
É necessário purgar a memória à força na etapa de gerenciamento de processos. Defina um ciclo de vida para que o worker do Gunicorn devolva a memória automaticamente e reinicie a cada 1.000 requisições processadas.
`bash
gunicorn
--workers 4
--worker-class uvicorn.workers.UvicornWorker
--bind 0.0.0.0:8000
--max-requests 1000
--max-requests-jitter 100
--timeout 120
--keep-alive 5
--preload-app
app.main:app
`
Adicione --max-requests-jitter 100 para evitar que todos os 4 workers morram e revivam simultaneamente causando perda de solicitações, e encerre trabalhos sem resposta com um timeout de 120 segundos.
Se um gerador assíncrono continuar rodando em loop quando o usuário fechar o navegador no meio de uma resposta, os recursos computacionais serão desperdiçados. Verifique request.is_disconnected() para sair do loop imediatamente caso a conexão seja interrompida.
`python
import asyncio
import gc
import logging
from typing import AsyncGenerator
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import StreamingResponse
app = FastAPI(title="Production AI Workspace Backend")
logger = logging.getLogger("stream_logger")
async def robust_llm_token_stream(prompt: str, request: Request) -> AsyncGenerator[str, None]:
try:
for token_idx in range(2000):
if await request.is_disconnected():
logger.warning(f"[Stream Aborted] Client disconnected at step {token_idx}.")
break
await asyncio.sleep(0.01)
yield f"event: message\ndata: {{"id": {token_idx}, "text": "chunk_{token_idx} "}}\n\n"
except asyncio.CancelledError:
logger.info("[Stream Cancelled] Task cancelled by ASGI server.")
raise
except Exception as err:
logger.error(f"[Stream Error] Error during streaming: {str(err)}")
raise
finally:
logger.info("[Stream Cleanup] Releasing context and triggering GC.")
gc.collect()
@app.post("/api/v1/chat/stream")
async def chat_stream_endpoint(request: Request, body: dict):
prompt = body.get("prompt", "")
if not prompt:
raise HTTPException(status_code=400, detail="Prompt string is missing.")
return StreamingResponse(
robust_llm_token_stream(prompt, request),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
)
`
É comum que uma única atualização secundária no upstream quebre a construção do contêiner local. Previna adulterações de pacotes e conflitos de versão usando um requirements.txt com hashes SHA-256 embutidos.
`plaintext
fastapi==0.110.0 --hash=sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
uvicorn==0.28.0 --hash=sha256:2c6a81387e0037a34ca6a7f8087796030c79eb299f116515822ee483c6604e43
pydantic==2.6.4 --hash=sha256:d82e212f451f2d6c19f5a5e3a89369322e70e1781297587786411516279f64a5
sqlalchemy==2.0.28 --hash=sha256:a611116c2bb45f8f3077e6822ec3a37b384ff6b9a84d4dd88a0e8eb876b5cf12
`
Para evitar falhas no download do pip ao reconstruir o Docker em ambientes offline (como redes corporativas de segurança ou dentro de aviões), baixe previamente os binários Wheel em um diretório local.
`bash
pip wheel --wheel-dir=./wheels_repository -r requirements.txt
`
`dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY ./wheels_repository /app/wheels_repository
COPY requirements.txt .
RUN pip install --no-cache-dir --no-index --find-links=/app/wheels_repository -r requirements.txt
COPY . .
CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-c", "gunicorn.conf.py", "app.main:app"]
`
Dar um pull completo nas alterações do repositório Git do upstream fará com que suas configurações locais cuidadosamente tunadas sejam perdidas. Separe uma branch de otimização e traga apenas os commits necessários via cherry-pick.
`bash
git remote add upstream https://github.com/opensource-ai-workspace/workspace.git
git fetch upstream
git checkout -b feature/local-mac-optimization
git cherry-pick
`
Se múltiplos agentes dispararem busca por embeddings, consulta de memória e registro de conversas de uma só vez, o pool de conexões do PostgreSQL se esgotará rapidamente. Com base na fórmula de cálculo de conexões () para um ambiente de disco único Apple Silicon de 8 núcleos, limite o pool do motor para baixo de 17.
`python
from sqlalchemy.ext.asyncio import create_async_engine
DATABASE_URL = "postgresql+asyncpg://postgres:password@localhost:6432/ai_workspace"
engine = create_async_engine(
DATABASE_URL,
pool_size=15,
max_overflow=5,
pool_timeout=10,
pool_recycle=300,
pool_pre_ping=True
)
`
O fenômeno em que agentes ficam segurando a sessão do DB enquanto aguardam os resultados da inferência da LLM é cortado com o uso do Transaction Pooling do PgBouncer.
`ini
[databases]
ai_workspace = host=postgres_db port=5432 dbname=ai_workspace auth_user=postgres
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = plain
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
idle_transaction_timeout = 60
max_client_conn = 1000
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
`
`
+-------------------------------------------------------------------------------+
| Docker Internal Isolated Network (internal: true) |
| |
| +--------------------+ +--------------------+ +-----------------+ |
| | AI Workspace App | ---> | PgBouncer Proxy | ---> | PostgreSQL DB | |
| | (FastAPI Backend) | | (Port 6432) | | (Port 5432) | |
| +--------------------+ +--------------------+ +-----------------+ |
| | | |
| | [pool_mode = transaction] |
| | [idle_tx_timeout = 60s] |
| v |
| +--------------------+ |
| | Local Vector DB | (Bloqueio de comunicação com a internet externa) |
| | (Qdrant) | |
| +--------------------+ |
+-------------------------------------------------------------------------------+
`
Para eliminar riscos de vazamento de dados, adicione internal: true à rede do contêiner para bloquear comunicações com nuvens externas. A conexão com o runtime Metal Ollama de alta velocidade rodando diretamente na máquina host é feita apenas pelo gateway host.docker.internal.
`yaml
version: '3.8'
services:
backend-app:
build: .
environment:
- DB_HOST=pgbouncer
- DB_PORT=6432
- VECTOR_DB_HOST=vector-db
- OLLAMA_HOST=http://host.docker.internal:11434
networks:
- isolated_local_net
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "8000:8000"
pgbouncer:
image: edoburu/pgbouncer:latest
environment:
- DB_HOST=postgres_db
- DB_PORT=5432
- DB_USER=postgres
- DB_PASSWORD=secret
- POOL_MODE=transaction
networks:
- isolated_local_net
depends_on:
- postgres_db
postgres_db:
image: postgres:16-alpine
environment:
- POSTGRES_DB=ai_workspace
- POSTGRES_PASSWORD=secret
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- isolated_local_net
vector-db:
image: qdrant/qdrant:v1.9.0
volumes:
- qdrant_data:/qdrant/storage
networks:
- isolated_local_net
networks:
isolated_local_net:
driver: bridge
internal: true
volumes:
pgdata:
qdrant_data:
`
Ao contornar o gargalo de virtualização da GPU com o Podman ou bridge do host, e controlando o ciclo de vida por meio de reciclagem de workers e proxy de DB, cria-se um ambiente de desenvolvimento independente que não cai mesmo em laptops equipados com chips da série M.