Pengaturan Engineering untuk Menyelamatkan LLM Lokal yang Sering Mati di Docker Mac
Saat Anda mengunduh kode workspace AI open-source dan menjalankannya di Docker Mac, Anda akan langsung menemui kendala sejak prompt pertama. Meskipun menggunakan chip seri M, kipas hanya berputar kencang dengan kecepatan yang sangat lambat, yaitu sekitar 10 token per detik. Hal ini terjadi karena lapisan virtualisasi Docker Desktop gagal meneruskan GPU Apple Silicon (Metal) ke dalam container, sehingga jatuh ke komputasi CPU mentah.
Jika Anda menjalankan backend FastAPI dan vector DB lokal bersamaan tanpa mengatasi hambatan ini, container akan bertumbangan akibat kebocoran memori (memory leak) dan kehabisan koneksi (connection exhaustion). Berikut adalah empat pengaturan yang perlu diubah agar kode yang diambil hanya berdasarkan jumlah bintang GitHub dapat berjalan pada tingkat produksi di lingkungan lokal.
Penyebab Fallback CPU dan Jalur Bypass Akselerasi Perangkat Keras
Docker Desktop di macOS berjalan di atas VM Linux yang ringan. Tidak ada fungsi untuk meneruskan API Metal GPU host secara langsung ke dalam container OS guest Linux. Ollama atau llama.cpp di dalam container gagal mengalokasikan VRAM dan secara diam-diam beralih ke komputasi CPU. Inilah alasan mengapa model Llama 3 8B yang mencapai 40-80 t/s secara native pada M3 Max merosot tajam menjadi 21.45 t/s untuk evaluasi prompt dan 12.17 t/s untuk pembuatan token saat masuk ke dalam Docker.
`
[Mekanisme Terjadinya Fallback CPU di dalam Container Virtualisasi]
+-----------------------------------------------------------------------+
| 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)] -> Penurunan kecepatan terjadi
`
Pengaturan shared memory (shm) default juga menjadi masalah. Dengan kuota default 64MB, operasi tensor berskala besar akan langsung menghasilkan Bus Error. Buka ~/.docker/daemon.json untuk memperluas shared memory dan melonggarkan batas sumber daya.
`json
{
"builder": {
"gc": {
"defaultKeepStorage": "20GB",
"enabled": true
}
},
"experimental": false,
"default-shm-size": "8g"
}
`
`yaml
version: '3.8'
eservices:
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:
`
Untuk mendapatkan akselerasi GPU di dalam container, Anda harus beralih ke Podman yang menggunakan monitor mesin virtual libkrun dan driver krunkit. Metode ini meneruskan permintaan komputasi Vulkan di dalam container ke API Metal macOS host (Virtio-GPU Venus). Ini meningkatkan kecepatan pemrosesan hingga 75% dibandingkan Metal native.
- Instal krunkit dan Podman melalui Homebrew.
brew tap slp/krunkit && brew install krunkit podman
- Jalankan mesin dengan 8 core dan RAM 32GB berbasis provider libkrun.
export CONTAINERS_MACHINE_PROVIDER="libkrun" && podman machine init --cpus 8 --memory 32768 && podman machine start
- Verifikasi apakah driver terpasang di dalam VM.
podman 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
`
Jika peraturan infrastruktur mengharuskan penggunaan komputasi CPU saja, gunakan format kuantisasi Q4_0_4_4 yang disesuaikan dengan instruksi vektor ARMv8.4-A DotProduct dan Neon. Ini mempertahankan kecepatan evaluasi prompt hingga 50.63 t/s, mengurangi waktu pemrosesan menjadi kurang dari separuh dibandingkan eksekusi CPU Docker default.
| Metode Konfigurasi Runtime |
Evaluasi Prompt (t/s) |
Pembuatan Token (t/s) |
Tingkat Kesulitan |
Fitur |
| Docker Desktop (CPU Default) |
~21.45 |
~12.17 |
Rendah |
Fallback CPU karena batasan virtualisasi |
| Docker Container (ARM Q4_0_4_4) |
~50.63 |
~14.01 |
Sedang |
Memanfaatkan instruksi vektor ARM Neon |
| Podman + libkrun (Vulkan Venus) |
~75% dari Native |
~75% dari Native |
Tinggi |
Akselerasi Virtio-GPU di dalam container |
| Host Native Metal + Container |
Native 100% (40~80) |
Native 100% |
Sedang |
Struktur pemanggilan engine host langsung |
Pelacakan Memory Leak FastAPI dan Pemulihan Stream yang Terputus
Backend open-source seringkali memiliki manajemen status tugas asinkron atau penanganan garbage collector CPython yang buruk. Seiring bertumpuknya permintaan, memori membengkak hingga proses mati. Gunakan tracemalloc untuk mendeteksi titik yang menghabiskan memori.
`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()
`
Anda harus membersihkan memori secara paksa pada tahap manajemen proses. Atur siklus hidup agar worker Gunicorn secara otomatis mengembalikan memori dan dimulai ulang setiap kali selesai memproses 1.000 permintaan.
`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
`
Dengan menambahkan --max-requests-jitter 100, Anda mencegah 4 worker mati dan hidup secara bersamaan yang dapat menyebabkan permintaan gagal, serta memutus tugas yang tidak merespons dengan timeout 120 detik.
Jika pengguna menutup browser di tengah jawaban, generator asinkron yang terus melakukan looping akan membuang-buang sumber daya komputasi. Periksa request.is_disconnected() untuk segera keluar dari loop jika koneksi terputus.
`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"
}
)
`
Penatan Dependensi dan Pipeline Build Offline
Satu pembaruan minor dari upstream seringkali merusak proses build container lokal. Cegah manipulasi paket dan konflik versi menggunakan requirements.txt yang menyertakan hash SHA-256.
`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
`
Unduh binary Wheel ke direktori lokal terlebih dahulu agar tidak terjadi kegagalan unduhan pip saat membangun ulang Docker di lingkungan offline seperti jaringan keamanan perusahaan atau di dalam pesawat.
`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"]
`
Melakukan pull secara langsung pada perubahan repositori Git upstream dapat menghilangkan pengaturan lokal yang telah Anda sesuaikan. Pisahkan branch optimasi dan ambil commit yang diperlukan melalui 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
`
Kontrol Bottleneck Koneksi DB dan Routing Jaringan Terisolasi
Ketika multi-agent melakukan pencarian embedding, pencarian memori, dan pencatatan percakapan secara bersamaan, pool koneksi PostgreSQL akan langsung habis. Batasi pool engine di bawah 17 koneksi berdasarkan rumus penghitungan koneksi untuk lingkungan disk tunggal Apple Silicon 8-core (Nextconn=extCoreCPUimes2+extJumlahSpindle).
`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
)
`
Putus masalah agent yang menahan sesi DB saat menunggu hasil inferensi LLM menggunakan transaction pooling 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 | (Blokir komunikasi internet eksternal) |
| | (Qdrant) | |
| +--------------------+ |
+-------------------------------------------------------------------------------+
`
Untuk menghilangkan risiko kebocoran data, tetapkan internal: true pada jaringan container guna memblokir komunikasi cloud eksternal. Sambungkan ke runtime Metal Ollama berkecepatan tinggi yang berjalan langsung di mesin host hanya melalui 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:
`
Dengan mem-bypass bottleneck virtualisasi GPU menggunakan Podman atau host bridge, serta mengontrol siklus hidup melalui daur ulang worker dan proxy DB, lingkungan pengembangan mandiri yang stabil dan tidak mudah crash bahkan pada laptop dengan chip seri M kini telah selesai dibuat.