Aiko-chan / backend /llm.py
OppaAI's picture
feat: add Nemotron ASR backend for speech transcription and update persona skills to include Japanese teaching
e4860d2
Raw
History Blame Contribute Delete
5.08 kB
"""
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 + volume ──────────────────────────────────────────────────────────────
app = modal.App("aiko-llm")
volume = modal.Volume.from_name("aiko-models", create_if_missing=True)
# ── image ─────────────────────────────────────────────────────────────────────
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]")
)
# ── constants ─────────────────────────────────────────────────────────────────
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}"
#HF_REPO = "unsloth/Ministral-3-8B-Instruct-2512-GGUF"
#HF_FILE = "Ministral-3-8B-Instruct-2512-UD-Q4_K_XL.gguf"
#MODEL_PATH = f"/models/{HF_FILE}"
LLAMA_PORT = 8080
# ── inference class ───────────────────────────────────────────────────────────
@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
# ── pull model once, cache in volume ──────────────────────────────────
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() # persist to volume after download
print("[aiko] model cached βœ“")
else:
print("[aiko] model already cached, skipping download")
# ── start llama.cpp server ────────────────────────────────────────────
# --jinja enables the model's chat template (incl. tool-calling format
# for Ministral) so `tools` / `tool_choice` in requests are honored and
# the response can contain `tool_calls`.
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",
])
# ── wait for server ready ─────────────────────────────────────────────
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}