TuBrief
구독 채널
비디오
커뮤니티

Configuración de ingeniería para rescatar un LLM local que se congela en Docker en Mac

TuBrief 편집팀
2026년 8월 14일
0
Computing/Software

원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.

Español한국어English中文العربيةDeutschFrançaisPortuguêsहिन्दीРусскийBahasa Indonesia日本語

관련 영상

PewDiePie ahora es ingeniero de software...6:29

PewDiePie ahora es ingeniero de software...

Better Stack

커뮤니티의 다른 글

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

2026년 9월 13일

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

2026년 9월 13일

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

2026년 9월 13일

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

2026년 9월 13일

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

2026년 9월 12일

Apple Won the AI Race

2026년 9월 12일

댓글 (0)

Log in to leave a comment

아직 작성된 글이 없습니다

© 2026 . All rights reserved.

TuBrief
구독 채널
비디오
커뮤니티
로그인

Configuración de ingeniería para rescatar un LLM local que se congela en Docker en Mac

Al descargar el código de un espacio de trabajo de IA de código abierto y ejecutarlo en Docker en una Mac, te encuentras con un obstáculo desde el primer mensaje (prompt). A pesar de estar usando claramente un chip de la serie M, los ventiladores solo giran ruidosamente y se registra una velocidad desastrosa de apenas unos 10 tokens por segundo. Esto ocurre porque la capa de virtualización de Docker Desktop no logra pasar la GPU de Apple Silicon (Metal) al interior del contenedor, cayendo en un procesamiento burdo de CPU.

Si ejecutas esto combinando un backend de FastAPI y una base de datos vectorial local sin resolver este cuello de botella, el contenedor terminará cayendo debido a fugas de memoria y agotamiento de conexiones. Estas son cuatro configuraciones que debes modificar para llevar un código obtenido simplemente por su cantidad de estrellas en GitHub a un nivel de producción en tu entorno local.

Causa del respaldo de CPU (CPU Fallback) y ruta de desvío de aceleración de hardware

Docker Desktop en macOS se ejecuta sobre una máquina virtual de Linux ligera. No existe una función para transferir directamente la API de Metal GPU del host al interior del contenedor del sistema operativo invitado Linux. Ollama o llama.cpp dentro del contenedor fallan en la asignación de VRAM y cambian silenciosamente al procesamiento de CPU. Esta es la razón por la cual el modelo Llama 3 8B, que alcanzaba de 40 a 80 t/s de forma nativa en un M3 Max, se desploma a niveles de 21.45 t/s en la evaluación de prompts y 12.17 t/s en la generación de tokens en el momento en que entra en Docker.

`
[Mecanismo de aparición del respaldo de CPU dentro del contenedor de virtualización]
+-----------------------------------------------------------------------+
| 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)] -> Ocurre degradación de velocidad

`

La configuración de memoria compartida predeterminada (shm) también es un problema. Con la asignación predeterminada de 64 MB, se producen errores de bus (Bus Error) de inmediato durante el cálculo de tensores a gran escala. Abre ~/.docker/daemon.json para ampliar la memoria compartida y liberar los límites 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 extraer la aceleración de GPU dentro del contenedor, debes cambiar a Podman utilizando el monitor de máquina virtual libkrun y el controlador krunkit. Es un método que transfiere las solicitudes de computación Vulkan dentro del contenedor a la API Metal del macOS host (Virtio-GPU Venus). Eleva la velocidad de procesamiento hasta un 75% en comparación con Metal nativo.

  1. Instala krunkit y Podman mediante Homebrew.
    brew tap slp/krunkit && brew install krunkit podman
  2. Inicia una máquina de 8 núcleos y 32 GB de RAM basada en el proveedor libkrun.
    export CONTAINERS_MACHINE_PROVIDER="libkrun" && podman machine init --cpus 8 --memory 32768 && podman machine start
  3. Verifica si el controlador se ha adjuntado dentro de la 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

`

Si las normas de infraestructura exigen el uso exclusivo de computación por CPU, utiliza el formato de cuantización Q4_0_4_4 adaptado a las instrucciones vectoriales ARMv8.4-A DotProduct y Neon. Esto protege la velocidad de evaluación de prompts hasta 50.63 t/s, reduciendo el tiempo de procesamiento a menos de la mitad en comparación con la ejecución básica de la CPU en Docker.

Método de configuración del entorno de ejecución Evaluación de prompts (t/s) Generación de tokens (t/s) Dificultad de construcción Características
Docker Desktop (CPU predeterminada) ~21.45 ~12.17 Baja Respaldo a CPU por restricciones de virtualización
Docker Container (ARM Q4_0_4_4) ~50.63 ~14.01 Media Uso de instrucciones vectoriales ARM Neon
Podman + libkrun (Vulkan Venus) ~75% vs Nativo ~75% vs Nativo Alta Aceleración Virtio-GPU dentro del contenedor
Host Native Metal + Container Nativo 100% (40~80) Nativo 100% Media Estructura de llamada directa al motor del host

Seguimiento de fugas de memoria en FastAPI y recuperación de streams interrumpidos

Los backends de código abierto a menudo tienen un manejo deficiente de la gestión de estado de tareas asíncronas o del recolector de basura de CPython. A medida que se acumulan las solicitudes, la memoria seinfla hasta que el proceso se congela. Utiliza tracemalloc para identificar los puntos que consumen memoria.

`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()

`

Debes vaciar la memoria de forma forzada a nivel de gestión de procesos. Establece un ciclo de vida para que los workers de Gunicorn liberen la memoria automáticamente y se reinicien cada vez que procesen 1000 solicitudes.

`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

`

Al agregar --max-requests-jitter 100, se evita el fenómeno en el que los 4 workers mueren y reviven simultáneamente bloqueando las peticiones, y las tareas sin respuesta se cortan con un tiempo de espera (timeout) de 120 segundos.

Si un usuario cierra el navegador en medio de una respuesta y el generador asíncrono continúa el bucle, los recursos de computación se desperdician por completo. Verifica request.is_disconnected() para salir del bucle inmediatamente si se corta la conexión.

`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"
    }
)

`

Fijación de dependencias y pipeline de compilación offline

Es común que una sola actualización menor en el upstream rompa la compilación del contenedor local. Se evitan alteraciones de paquetes y conflictos de versiones mediante un requirements.txt que incluye hashes 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

`

Descarga previamente los binarios Wheel en un directorio local para evitar fallos de descarga de pip al reconstruir Docker en entornos offline, como redes corporativas de seguridad o durante un vuelo.

`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"]

`

Al hacer pull completo de los cambios del repositorio Git upstream, la configuración local que tanto costó afinar se pierde. Separa una rama de optimización y trae únicamente los commits necesarios mediante 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

`

Control de cuellos de botella en conexiones de BD y enrutamiento de red aislado

Si múltiples agentes ejecutan búsquedas de incrustación (embedding), consultas de memoria y registro de chat al mismo tiempo, el pool de conexiones de PostgreSQL se agota en un instante. Limita el pool del motor a menos de 17 conexiones según la fórmula de cálculo de conexiones para un entorno de disco único con Apple Silicon de 8 núcleos (Nextconn=extnuˊcleosdeCPUimes2+extnuˊmerodeejesN_{ ext{conn}} = ext{núcleos de CPU} imes 2 + ext{número de ejes}Nextconn​=extnuˊcleosdeCPUimes2+extnuˊmerodeejes). 채 (Nota: traducido como la fórmula estándar de la industria).

`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
)

`

El fenómeno en el que los agentes retienen la sesión de la BD mientras esperan los resultados de inferencia del LLM se corta mediante el agrupamiento de transacciones (transaction pooling) de 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 | (Bloqueo de comunicación con internet externo) |
| | (Qdrant) | |
| +--------------------+ |
+-------------------------------------------------------------------------------+

`

Para eliminar el riesgo de fugas de datos, se aplica internal: true a la red del contenedor para bloquear la comunicación con la nube externa. La conexión con el motor Metal Ollama de alta velocidad que se ejecuta directamente en la máquina host se realiza únicamente a través de la puerta de enlace 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

etworks:
isolated_local_net:
driver: bridge
internal: true

volumes:
pgdata:
qdrant_data:

`

Al evitar los cuellos de botella de la virtualización de la GPU con Podman o un puente del host, y controlar el ciclo de vida mediante la reutilización de workers y un proxy de BD, se completa un entorno de desarrollo independiente que no se cae ni siquiera en computadoras portátiles equipadas con chips de la serie M.