| """ |
| backend/llm.py |
| |
| Aiko's Modal LLM backend β runs llama.cpp server with Ministral 3B Q4 on GPU. |
| Deploy with: modal deploy backend/llm.py |
| OpenAI-compatible endpoint β think.py needs zero changes. |
| Set LLAMA_BASE_URL to the Modal chat URL in your HF Space secrets. |
| """ |
|
|
| import os |
| import modal |
|
|
| |
|
|
| app = modal.App("aiko-llm") |
| volume = modal.Volume.from_name("aiko-models", create_if_missing=True) |
|
|
| |
|
|
| image = ( |
| modal.Image.from_registry( |
| "nvidia/cuda:12.1.0-devel-ubuntu22.04", |
| add_python="3.11" |
| ) |
| .apt_install( |
| "curl", "ca-certificates", "git", |
| "build-essential", "cmake", "libcurl4-openssl-dev" |
| ) |
| .run_commands( |
| "git clone https://github.com/ggerganov/llama.cpp /llama.cpp", |
| "cd /llama.cpp && cmake -B build -DLLAMA_CURL=ON -DGGML_CUDA=ON " |
| "-DCMAKE_CUDA_ARCHITECTURES=75 " |
| "-DCMAKE_EXE_LINKER_FLAGS='-L/usr/local/cuda/lib64/stubs -lcuda' " |
| "-DCMAKE_SHARED_LINKER_FLAGS='-L/usr/local/cuda/lib64/stubs -lcuda' " |
| "&& cmake --build build --config Release -j$(nproc) -t llama-server", |
| "cp /llama.cpp/build/bin/llama-server /usr/local/bin/llama-server", |
| ) |
| .pip_install("huggingface_hub", "httpx", "fastapi[standard]") |
| ) |
|
|
| |
|
|
| HF_REPO = "unsloth/Ministral-3-3B-Instruct-2512-GGUF" |
| HF_FILE = "Ministral-3-3B-Instruct-2512-UD-Q4_K_XL.gguf" |
| MODEL_PATH = f"/models/{HF_FILE}" |
| |
| |
| |
| LLAMA_PORT = 8080 |
|
|
| |
|
|
| @app.cls( |
| image=image, |
| gpu="T4", |
| volumes={"/models": volume}, |
| timeout=300, |
| scaledown_window=300, |
| secrets=[modal.Secret.from_name("aiko-secrets")], |
| ) |
| class AikoLLM: |
|
|
| @modal.enter() |
| def startup(self): |
| import subprocess, time, httpx |
| from huggingface_hub import hf_hub_download |
|
|
| |
| if not os.path.exists(MODEL_PATH): |
| print(f"[aiko] downloading {HF_FILE} from HF...") |
| hf_hub_download( |
| repo_id=HF_REPO, |
| filename=HF_FILE, |
| local_dir="/models", |
| ) |
| volume.commit() |
| print("[aiko] model cached β") |
| else: |
| print("[aiko] model already cached, skipping download") |
|
|
| |
| |
| |
| |
| self._proc = subprocess.Popen([ |
| "/usr/local/bin/llama-server", |
| "--model", MODEL_PATH, |
| "--port", str(LLAMA_PORT), |
| "--host", "0.0.0.0", |
| "--n-gpu-layers", "99", |
| "--ctx-size", "4096", |
| "--threads", "4", |
| "--parallel", "1", |
| "--jinja", |
| ]) |
|
|
| |
| for i in range(60): |
| try: |
| r = httpx.get(f"http://localhost:{LLAMA_PORT}/health", timeout=2) |
| if r.status_code == 200: |
| print("[aiko] llama.cpp ready β") |
| break |
| except Exception: |
| pass |
| time.sleep(1) |
| else: |
| raise RuntimeError("llama.cpp server failed to start") |
|
|
| @modal.fastapi_endpoint(method="POST") |
| def chat(self, request: dict): |
| import httpx |
| resp = httpx.post( |
| f"http://localhost:{LLAMA_PORT}/v1/chat/completions", |
| json=request, |
| timeout=120.0, |
| ) |
| return resp.json() |
|
|
| @modal.fastapi_endpoint(method="GET") |
| def health(self): |
| return {"status": "ok", "model": HF_FILE} |