إعدادات هندسية لإنقاذ نماذج اللغات الكبيرة المحلية (Local LLM) التي تعاني على دوكر الماك
عند تنزيل كود مساحة عمل الذكاء الاصطناعي مفتوحة المصدر وتشغيله على دوكر الماك، ستصطدم بالحائط من أول موجه (Prompt). على الرغم من استخدام رقاقات سلسلة M، إلا أن المراوح تعمل بصوت عالٍ فقط، وتسجل سرعة مروعة بالكاد تصل إلى 10 رموز في الثانية. والسبب هو أن طبقة المحاكاة الافتراضية في Docker Desktop لا يمكنها تمرير وحدة معالجة الرسومات Apple Silicon (Metal) إلى داخل الحاوية، مما يتسبب في التراجع إلى الحساب العشوائي لوحدة المعالجة المركزية (CPU).
إذا قمت بربط الواجهة الخلفية FastAPI وقاعدة بيانات المتجهات المحلية وتشغيلها دون تجاوز هذا الاختناق، فسوف تتعطل الحاوية بسبب تسرب الذاكرة ونفاد الاتصالات. فيما يلي أربعة إعدادات يجب تعديلها لتحويل الكود الذي جلبته بناءً فقط على عدد نجوم جيت هب إلى مستوى جاهز للإنتاج في بيئتك المحلية.
سبب التراجع إلى وحدة المعالجة المركزية (CPU Fallback) ومسار تجاوز تسريع الأجهزة
يعمل Docker Desktop على نظام macOS فوق نظام تشغيل Linux افتراضي خفيف الوزن. ولا توجد ميزة لتمرير واجهة برمجة تطبيقات Metal GPU الخاصة بالضيف مباشرة إلى داخل حاوية نظام تشغيل Linux الضيف. تفشل محركات مثل Ollama أو llama.cpp داخل الحاوية في تخصيص VRAM، وتنتقل بهدوء إلى حسابات وحدة المعالجة المركزية. وهذا هو السبب في أن نموذج Llama 3 8B، الذي كان ينتج 40-80 t/s محلياً على جهاز M3 Max، ينخفض بشكل حاد بمجرد دخوله إلى Docker إلى حوالي 21.45 t/s لتقييم الموجه و12.17 t/s لتوليد الرموز.
`
[آلية حدوث التراجع إلى وحدة المعالجة المركزية داخل حاوية المحاكاة الافتراضية]
+-----------------------------------------------------------------------+
| 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)] -> انخفاض السرعة
`
إعداد الذاكرة المشتركة (shm) الافتراضية يمثل مشكلة أيضاً. فحصة التخصيص الافتراضية البالغة 64 ميجابايت تتسبب في ظهور خطأ Bus Error فوراً أثناء حسابات Tensor الكبيرة. افتح ملف ~/.docker/daemon.json لتوسيع الذاكرة المشتركة وإلغاء تقييد حدود الموارد.
`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:
`
لاستخراج تسريع وحدة معالجة الرسومات من داخل الحاوية، يجب الانتقال إلى Podman باستخدام مراقب الجهاز الظاهري libkrun وبرنامج التشغيل krunkit. وتتمثل الطريقة في تمرير طلبات حساب Vulkan داخل الحاوية إلى واجهة Metal API الخاصة بنظام التشغيل المضيف macOS (Virtio-GPU Venus). يؤدي هذا إلى رفع سرعة المعالجة إلى ما يصل إلى 75% مقارنة بـ Metal الأصلي.
- تثبيت krunkit و Podman عبر Homebrew.
brew tap slp/krunkit && brew install krunkit podman
- تشغيل جهاز بـ 8 نواة و 32 جيجابايت من ذاكرة الوصول العشوائي بناءً على مزود libkrun.
export CONTAINERS_MACHINE_PROVIDER="libkrun" && podman machine init --cpus 8 --memory 32768 && podman machine start
- التحقق مما إذا كان برنامج التشغيل متصلاً داخل الجهاز الظاهري.
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
`
إذا كان يجب استخدام حسابات وحدة المعالجة المركزية فقط بسبب لوائح البنية التحتية، فاستخدم تنسيق الكمي Q4_0_4_4 المصمم خصيصاً لتعليمات المتجهات ARMv8.4-A DotProduct و Neon. وهذا يحافظ على سرعة تقييم الموجه حتى 50.63 t/s، مما يقلل وقت المعالجة إلى النصف أو أقل مقارنة بتشغيل وحدة المعالجة المركزية الافتراضية في دوكر.
| تكوين وقت التشغيل |
تقييم الموجه (t/s) |
توليد الرموز (t/s) |
صعوبة البناء |
الميزات |
| Docker Desktop (وحدة المعالجة المركزية الافتراضية) |
~21.45 |
~12.17 |
منخفضة |
التراجع إلى وحدة المعالجة المركزية بسبب قيود المحاكاة الافتراضية |
| Docker Container (ARM Q4_0_4_4) |
~50.63 |
~14.01 |
متوسطة |
استخدام تعليمات متجهات ARM Neon |
| Podman + libkrun (Vulkan Venus) |
~75% من الأصلي |
~75% من الأصلي |
عالية |
تسريع Virtio-GPU داخل الحاوية |
| Host Native Metal + Container |
100% أصلي (40~80) |
100% أصلي |
متوسطة |
هيكل استدعاء محرك المضيف مباشرة |
تتبع تسرب ذاكرة FastAPI واستعادة التدفقات المنقطعة
غالباً ما تحتوي الواجهات الخلفية مفتوحة المصدر على إدارة رديئة لحالة المهام غير المتزامنة أو معالجة جامع النفايات في CPython. وكلما تراكمت الطلبات، تتضخم الذاكرة حتى يتعطل العملية. استخدم tracemalloc لتحديد النقاط التي تستهلك الذاكرة.
`python
import gc
import tracemalloc
tracemalloc.start()
def log_memory_snapshot():
snapshot = tracemalloc.snapshot()
top_stats = snapshot.statistics('lineno')
print("[Memory Debug] Top 5 Allocations:")
for stat in top_stats[:5]:
print(stat)
gc.collect()
`
يجب مسح الذاكرة قسراً في مرحلة إدارة العمليات. قم بتعيين دورة حياة لعمال Gunicorn بحيث يقومون تلقائياً بإرجاع الذاكرة والتشغيل من جديد كلما عالجوا 1000 طلب.
`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
`
يؤدي إضافة --max-requests-jitter 100 إلى منع حدوث ظاهرة موت وإحياء العمال الأربعة في نفس الوقت مما يؤدي إلى ضياع الطلبات، كما يتم قطع المهام التي لا تستجيب مهلة قدرها 120 ثانية.
إذا أغلق المستخدم المتصفح أثناء الإجابة واستمر المولد غير المتزامن في تكرار الحلقة، يتم إهدار موارد الحساب عبثاً. تحقق من request.is_disconnected() لخروج الحلقة فوراً إذا انقطع الاتصال.
`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"
}
)
`
تثبيت التبعيات وخط أنابيب البناء دون اتصال بالإنترنت
من الشائع أن يؤدي تحديث طفيف من المصدر إلى كسر بناء الحاوية المحلية. امنع تعديل الحزم وتضارب الإصدارات باستخدام ملف requirements.txt الذي يحتوي على تجزئات 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
`
قم بتنزيل ثنائيات Wheel في الدليل المحلي مقدماً لضمان عدم فشل تنزيلات pip عند إعادة بناء دوكر في بيئة غير متصلة بالإنترنت مثل شبكة أمان الشركة أو على متن طائرة.
`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"]
`
سحب تغييرات مستودع جيت بالكامل قد يؤدي إلى ضياع الإعدادات المحلية التي قمت بضبطها بعناية. افصل فرع التحسين واجلب الالتزامات اللازمة فقط عبر 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
`
التحكم في اختناق اتصالات قاعدة البيانات وتوجيه الشبكة المعزولة
إذا قام الوكلاء المتعددون (Multi-agents) بتنفيذ بحث التضمين، والبحث في الذاكرة، وتسجيل المحادثات دفعة واحدة، فستنفد تجمعات اتصالات PostgreSQL في لحظات. قم بتهيئة تجمع المحرك بحد أقصى أقل من 17 اتصالا بناءً على معادلة حساب الاتصالات الخاصة ببيئة قرص واحد بنواة Apple Silicon (Nextconn=extCPUCoresimes2+extSpindleCount).
`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
)
`
يمكن قطع ظاهرة احتفاظ الوكيل بجلسة قاعدة البيانات أثناء انتظار نتائج استدلال LLM باستخدام ميزة تجميع معاملات 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 | (حظر الاتصال بالإنترنت الخارجي) |
| | (Qdrant) | |
| +--------------------+ |
+-------------------------------------------------------------------------------+
`
لتخفيف مخاطر تسرب البيانات، قم بتطبيق internal: true على شبكة الحاوية لمنع الاتصال السحابي الخارجي. ويتم الاتصال فقط مع محرك Metal Ollama عالي السرعة قيد التشغيل مباشرة على الجهاز المضيف عبر بوابة 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:
`
من خلال تجاوز الاختناقات الافتراضية لـ GPU باستخدام Podman أو جسر المضيف، والتحكم في دورة الحياة عبر إعادة تدوير العمال ووكلاء قاعدة البيانات، يتم إكمال بيئة تطوير مستقلة لا تتعطل حتى على أجهزة الكمبيوتر المحمولة المزودة برقاقات سلسلة M.