- Published on
Day 29: Local LLM - Ollama, llama.cpp, vLLM
- Authors

- Name
- Trần Mạnh Thắng
- @TranManhThang96
Mục Tiêu
Sau bài này, bạn cần làm được các việc sau:
- Hiểu vì sao team chọn local/self-host LLM thay vì luôn gọi cloud LLM API.
- Phân biệt đúng vai trò của Ollama, llama.cpp, vLLM, TGI và LM Studio.
- Hiểu các biến ảnh hưởng serving: model size, tokenizer, context window, quantization, KV cache, batching, concurrency, RAM/VRAM.
- Gọi được local model qua HTTP API và OpenAI-compatible client.
- Thiết kế được LLM gateway để business code không phụ thuộc trực tiếp vào một runtime.
- Biết benchmark latency, throughput, memory và chất lượng ở mức đủ để ra production decision.
- Trả lời rõ: dùng được trong production không, nếu có thì cần điều kiện gì.
TL;DR
Local LLM không chỉ là "tải model về máy". Nó là một serving stack gồm model weights, tokenizer, runtime inference, quantization kernels, API server, scheduler, hardware, observability, security policy và deployment process.
Ollama phù hợp nhất cho local development, prototype và internal tool vì setup nhanh. llama.cpp mạnh ở GGUF, CPU, Apple Silicon, edge và môi trường hạn chế memory. vLLM phù hợp GPU serving có concurrency cao nhờ scheduler, batching và OpenAI-compatible server. TGI phù hợp team đã dùng Hugging Face ecosystem và muốn containerized serving. LM Studio rất tốt cho học, demo, manual evaluation nhưng không nên là production backend mặc định.
Production answer ngắn: local LLM dùng được trong production, nhưng chỉ khi bạn kiểm soát được model license, eval quality, hardware capacity, API gateway, timeout/retry, observability, security, fallback, rollout và vận hành GPU/CPU như một service thật.
1. Bài Này Nằm Ở Đâu Trong Phase 4
Phase 4 đi từ quyết định fine-tuning tới local deployment:
Day 25: Khi nào dùng RAG, fine-tuning, distillation
Day 26: Chuẩn bị dataset instruction tuning
Day 27: LoRA/QLoRA hands-on
Day 28: Evaluation trước/sau fine-tune
Day 29: Local LLM runtime và model serving API
Day 30: Quantization và deploy local model API
Day 29 không tập trung vào train model. Trọng tâm là inference/runtime: sau khi có base model hoặc fine-tuned model, bạn phục vụ nó cho app như thế nào, đo hiệu năng ra sao, và chọn runtime nào theo context.
2. Vì Sao Dùng Local LLM
Privacy
Local LLM giúp dữ liệu không phải rời khỏi network bạn kiểm soát. Điều này quan trọng khi prompt chứa source code private, tài liệu nội bộ, dữ liệu khách hàng, thông tin pháp lý, dữ liệu y tế, dữ liệu tài chính hoặc policy nội bộ.
Nhưng local không tự động an toàn. Nếu bạn log prompt thô chứa PII, mount volume sai quyền, expose endpoint không auth, hoặc lưu KV cache/debug dump không kiểm soát, dữ liệu vẫn có thể leak. Privacy là property của toàn bộ system, không phải chỉ của model.
Cost
Cloud API tính tiền theo token. Local LLM tốn GPU/CPU, RAM/VRAM, điện, storage, ops, monitoring và thời gian engineer. Local có thể rẻ hơn khi workload ổn định, traffic lớn, model vừa đủ nhỏ, hardware được tận dụng tốt và team có năng lực vận hành.
Nếu traffic thấp, yêu cầu chất lượng cao, hoặc team chưa có GPU ops, cloud API thường rẻ hơn về total cost of ownership.
Latency
Local LLM có thể giảm network latency nếu service nằm gần app hoặc user. Với internal app trong cùng datacenter, request không phải đi qua internet. Tuy nhiên inference latency vẫn phụ thuộc model size, context length, quantization, hardware, batching và số request đồng thời.
Local nhỏ hơn không luôn nhanh hơn theo cách hữu ích. Model quá nhỏ có thể trả lời sai, khiến phải retry, fallback hoặc human review, làm latency end-to-end tăng.
Offline
Local LLM hữu ích cho air-gapped environment, factory, lab, field device, laptop dev khi mất mạng, hoặc nơi không được gọi external API. Use case offline thường cần artifact management chặt: model file, tokenizer, config, container image, eval set và rollback bundle phải đi cùng nhau.
Control và Compliance
Local giúp bạn pin model version, pin runtime version, chọn quantization, giới hạn log retention, enforce data residency, audit request path và thử nghiệm model nội bộ. Đổi lại, bạn sở hữu trách nhiệm security patch, model upgrade, incident response và capacity planning.
3. Mental Model Của Local Serving
Client/App
-> LLM Gateway hoặc Provider Adapter
-> Runtime API Server
-> Request validation
-> Tokenizer
-> Model weights
-> Quantization kernels
-> KV cache
-> Scheduler / batching
-> Stream hoặc JSON response
Map về tư duy Senior Software Engineer:
| AI serving concept | SE analogy | Cần quan tâm |
|---|---|---|
| Model weights | Build artifact | Version, checksum, license, storage |
| Tokenizer | Parser/input contract | Token count, special tokens, chat template |
| Runtime | Application server | Performance, compatibility, observability |
| Quantization | Compression/optimized binary | Quality loss, memory, kernel support |
| KV cache | Runtime memory state | VRAM tăng theo context và concurrency |
| Context window | Request payload budget | Prompt dài làm chậm và tốn memory |
| Chat template | Serialization contract | Sai template làm model trả lời kém |
| Model serving API | Internal service contract | Timeout, retry, streaming, auth, schema |
4. Các Biến Quyết Định Hiệu Năng
Model size
Model càng nhiều parameters thì thường chất lượng tốt hơn nhưng cần nhiều RAM/VRAM hơn và decode chậm hơn. Model 7B quantized có thể chạy trên laptop mạnh; model 70B cần GPU server nghiêm túc nếu muốn latency tốt.
Dtype và quantization
FP16/BF16 thường dùng cho GPU serving chất lượng cao. INT8/INT4, GGUF, AWQ, GPTQ giúp giảm memory. Trade-off là chất lượng có thể giảm, kernel có thể không tối ưu cho mọi runtime, và một số quantization phù hợp runtime này nhưng không phù hợp runtime khác.
Context window và KV cache
Prompt dài làm prefill chậm. Mỗi token trong context sinh thêm KV cache. Khi batch/concurrency tăng, KV cache có thể trở thành nguyên nhân hết VRAM trước cả model weights.
Rule thực tế: luôn benchmark với prompt dài đúng production, không chỉ prompt "hello".
Throughput và latency
- Latency: thời gian một request hoàn thành.
- Time-to-first-token: thời gian tới token đầu tiên khi streaming.
- Throughput: token/second tổng hoặc request/second.
- p95/p99: tail latency, quan trọng hơn average với user-facing app.
Batching tăng throughput nhưng có thể tăng latency cho request đơn lẻ. Runtime phục vụ chatbot realtime và runtime phục vụ batch summarization có tuning khác nhau.
5. Runtime Options
Ollama
Ollama là lựa chọn nhanh nhất để bắt đầu local LLM. Nó quản lý pull model, run model, local API và có OpenAI-compatible endpoint.
Ví dụ:
ollama pull llama3.2
ollama run llama3.2
Native API:
curl http://localhost:11434/api/chat -d '{
"model": "llama3.2",
"messages": [
{"role": "user", "content": "Giải thích local LLM trong 3 bullet"}
],
"stream": false
}'
OpenAI-compatible endpoint thường dùng dạng:
http://localhost:11434/v1
Dùng Ollama khi:
- Cần setup nhanh trên laptop.
- Cần test prompt, RAG, tool calling local.
- Cần internal demo hoặc dev offline.
- Muốn app dùng OpenAI-compatible client để giảm lock-in.
Không nên mặc định dùng Ollama khi:
- Cần throughput GPU cao với nhiều user đồng thời.
- Cần autoscaling, multi-replica, metrics production đầy đủ.
- Cần tuning sâu scheduler/batching.
llama.cpp
llama.cpp là runtime C/C++ inference, nổi bật với GGUF, CPU inference, Apple Silicon, GPU offload và edge deployment.
Ví dụ server:
./build/bin/llama-server \
-m ./models/model-q4_k_m.gguf \
--host 0.0.0.0 \
--port 8080 \
-c 4096 \
-ngl 99
Dùng llama.cpp khi:
- Cần chạy CPU hoặc edge.
- Cần artifact GGUF nhỏ, dễ ship.
- Cần kiểm soát memory tốt.
- Cần runtime ít dependency.
- Cần Apple Silicon hoặc máy dev không có NVIDIA GPU.
Trade-off:
- Throughput scale lớn cần tự thiết kế thêm gateway, queue, metrics, replica và load balancing.
- Chọn GGUF quantization sai có thể làm chất lượng giảm rõ.
- Với model lớn, CPU latency có thể không đáp ứng realtime chat.
vLLM
vLLM là inference engine/server tối ưu cho GPU throughput, OpenAI-compatible serving, scheduler và batching.
Ví dụ:
vllm serve Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 \
--port 8000
Dùng vLLM khi:
- Có GPU server và traffic đồng thời.
- Cần throughput cao.
- Cần OpenAI-compatible endpoint cho app.
- Cần serving controls, metrics và production deployment rõ hơn dev tool.
- Cần phục vụ chat/completions nhiều request.
Trade-off:
- Cần hiểu GPU memory, tensor parallel, max model len, batch sizing và rollout.
- Setup phức tạp hơn Ollama.
- Không phải model/quantization nào cũng chạy tốt như nhau.
TGI
TGI, Text Generation Inference, phù hợp khi team đã dùng Hugging Face ecosystem, model registry, container deployment và muốn serving stack có production orientation.
Dùng TGI khi:
- Model nằm trong Hugging Face workflow.
- Team quen Docker/Kubernetes.
- Cần containerized serving và monitoring.
- Muốn tiêu chuẩn hóa deployment nhiều model HF.
Trade-off:
- Ops phức tạp hơn Ollama.
- Cần kiểm tra compatibility từng model, dtype, quantization và GPU.
LM Studio
LM Studio là desktop UX tốt để tải model, chat thử, so sánh model và expose local API trong lúc học hoặc demo.
Dùng LM Studio khi:
- Người học muốn nhìn thấy model chạy ngay.
- Product/PM/engineer cần manual evaluation nhanh.
- Cần thử nhiều GGUF model trên desktop.
Không nên coi LM Studio là production backend mặc định vì deployment, security, observability, scaling và automation không phải trọng tâm chính của nó.
6. Runtime Matrix
| Runtime | Mạnh ở đâu | Hạn chế | Hardware thường gặp | API | Production fit |
|---|---|---|---|---|---|
| Ollama | Setup nhanh, dev UX tốt, pull/run đơn giản | Throughput cao và ops production cần bổ sung | Laptop, workstation, single server | Native + OpenAI-compatible | Internal tool nhỏ, dev, prototype; production nhỏ nếu bọc gateway/monitoring |
| llama.cpp | GGUF, CPU, Apple Silicon, edge, memory thấp | Scale lớn cần tự xây nhiều phần | CPU, Apple Silicon, GPU offload | Server API/OpenAI-compatible tùy build | Edge, offline, embedded, workload nhỏ/vừa |
| vLLM | GPU throughput, batching, OpenAI-compatible serving | Cần GPU ops và tuning | NVIDIA GPU server | OpenAI-compatible | Production GPU serving mạnh |
| TGI | Hugging Face ecosystem, container serving | Setup/ops nặng hơn dev tools | NVIDIA GPU server | HTTP/generation API | Production nếu team dùng HF stack |
| LM Studio | Desktop UX, manual eval, học nhanh | Không phải backend production chuẩn | Laptop/desktop | Local API | Dev/manual evaluation |
| Cloud LLM API | Quality/SLA/time-to-market | Cost/token, data boundary, provider lock-in | Provider-managed | Provider API | Production nhanh nếu compliance cho phép |
7. Best Solution Theo Context
| Context | Lựa chọn nên bắt đầu | Vì sao |
|---|---|---|
| Học local LLM trong 1 giờ | Ollama hoặc LM Studio | Setup nhanh, ít ops |
| Dev RAG app offline | Ollama | Dễ chạy OpenAI-compatible endpoint |
| Laptop Apple Silicon | llama.cpp hoặc Ollama | GGUF/Metal/local UX tốt |
| Edge device không có GPU mạnh | llama.cpp + GGUF quantized | Memory thấp, artifact dễ đóng gói |
| Internal assistant traffic thấp | Ollama sau LLM gateway | Đơn giản, đủ dùng nếu benchmark đạt |
| Chat API nhiều user đồng thời | vLLM | Scheduler/batching tốt cho GPU throughput |
| Team đã chuẩn HF/K8s | TGI hoặc vLLM | Dễ đưa vào platform hiện có |
| Batch summarization traffic ổn định | vLLM/TGI với batching | Tối ưu cost/token và throughput |
| Data không được rời khỏi VPC | Self-host vLLM/TGI/llama.cpp | Kiểm soát network và data residency |
| Cần model chất lượng cao nhất ngay | Cloud LLM API hoặc hybrid | Local model nhỏ có thể chưa đạt quality |
8. Model Serving API Nên Thiết Kế Thế Nào
Không hardcode Ollama, vLLM hoặc llama.cpp vào business logic. App nên gọi một interface nội bộ:
Product App
-> LLM Gateway
-> policy: model, runtime, timeout, max_tokens, fallback
-> ProviderAdapter: OpenAI cloud
-> ProviderAdapter: Ollama
-> ProviderAdapter: vLLM
-> ProviderAdapter: llama.cpp
-> Response + trace
Contract tối thiểu:
class ChatRequest(BaseModel):
messages: list[dict[str, str]]
model_policy: str = "default"
temperature: float = 0.2
max_tokens: int = 512
timeout_s: float = 30.0
class ChatResponse(BaseModel):
text: str
model: str
runtime: str
latency_ms: float
input_tokens: int | None = None
output_tokens: int | None = None
trace_id: str
Gateway chịu trách nhiệm:
- Validate input và giới hạn max prompt size.
- Chọn runtime/model theo policy.
- Timeout và retry có kiểm soát.
- Streaming nếu UX cần.
- Logging không leak PII.
- Rate limit, auth, tenant isolation.
- Fallback sang model khác hoặc cloud provider.
- Chuẩn hóa response schema.
- Ghi metric latency, token, error, memory signal.
9. Performance, Throughput, Latency Và VRAM Concern
Những metric cần đo
| Metric | Ý nghĩa |
|---|---|
| Time-to-first-token | User cảm thấy model bắt đầu trả lời nhanh hay chậm |
| Total latency | Request hoàn thành mất bao lâu |
| Output tokens/second | Decode speed |
| Requests/second | Service capacity |
| p50/p95/p99 latency | Tail latency |
| RAM/VRAM used | Có còn headroom không |
| Error rate | Timeout, OOM, 5xx, invalid response |
| Quality score | Golden eval, human rating, task success |
Các nguyên nhân latency cao
- Model quá lớn so với hardware.
- Prompt quá dài làm prefill chậm.
max_tokensquá rộng khiến decode dài.- Concurrency vượt capacity, queue tăng.
- KV cache đầy VRAM.
- Quantization format không tối ưu cho runtime.
- Cold start hoặc model unload/reload.
- Không streaming nên user phải chờ toàn bộ output.
VRAM estimation ở mức trực giác
VRAM không chỉ chứa model weights. Nó còn chứa KV cache, activation workspace, runtime overhead và fragmentation. Với production, đừng dùng 100% VRAM lý thuyết. Cần headroom để tránh OOM khi prompt dài hoặc concurrency tăng.
Checklist capacity:
- Model weights fit vào VRAM với dtype/quantization đã chọn.
- Context length production không vượt ngân sách KV cache.
- Batch/concurrency benchmark đúng traffic thật.
- Có guardrail
max_tokens,max_context, request timeout. - Có canary và rollback khi đổi model/runtime.
10. Production Answer
Dùng được trong production không?
Có, local LLM dùng được trong production. Nhiều use case production hợp lý gồm internal assistant, private RAG, code assistant nội bộ, batch summarization, document extraction, offline assistant và domain workflow có data boundary nghiêm ngặt.
Nếu có thì cần điều kiện gì?
Cần tối thiểu các điều kiện sau:
- Model license cho phép use case hiện tại, đặc biệt commercial/internal distribution.
- Có golden eval để chứng minh chất lượng đủ tốt so với cloud baseline hoặc human baseline.
- Có runtime phù hợp context: Ollama cho internal nhỏ/dev, llama.cpp cho edge/CPU/GGUF, vLLM/TGI cho GPU production.
- Có LLM gateway với auth, timeout, retry, max token, logging, policy routing và fallback.
- Có observability: latency p50/p95/p99, throughput, token count, error rate, RAM/VRAM, OOM, queue time, quality sample.
- Có security policy: không log PII thô, tenant isolation, network boundary, secret management, audit log.
- Có deployment process: pin model revision, runtime version, container image, config, canary, rollback.
- Có capacity planning: benchmark theo prompt thật, concurrency thật, context thật và output length thật.
- Có response validation nếu output đi vào downstream automation.
Không nên production nếu:
- Chỉ mới chạy được demo trên laptop.
- Chưa đọc license model.
- Chưa có eval dataset.
- Chưa đo p95 latency và memory.
- Endpoint không auth hoặc log prompt nhạy cảm.
- Không có fallback khi model timeout/OOM.
- Team không có năng lực vận hành hardware/runtime đã chọn.
11. Checklist Học Xong
- Giải thích được local LLM khác gì cloud LLM API.
- Nêu được privacy benefit và privacy risk còn lại.
- Phân biệt được Ollama, llama.cpp, vLLM, TGI, LM Studio.
- Biết vì sao OpenAI-compatible API giúp giảm lock-in.
- Biết KV cache ảnh hưởng memory và latency như thế nào.
- Biết đo latency, throughput, p95 và RAM/VRAM.
- Có decision note chọn runtime theo context.
- Trả lời được điều kiện để dùng local LLM trong production.
12. Câu Hỏi Tự Kiểm Tra
- Local LLM giải quyết privacy theo cách nào, và không giải quyết phần nào?
- Vì sao traffic thấp thường chưa nên self-host LLM?
- Ollama khác vLLM ở mục tiêu thiết kế nào?
- GGUF liên quan gì tới llama.cpp?
- Vì sao prompt dài làm prefill chậm và KV cache lớn?
- Khi nào chọn TGI thay vì vLLM?
- Vì sao LM Studio tốt cho learning nhưng không phải production backend mặc định?
- OpenAI-compatible API giúp app architecture như thế nào?
- Nếu local model trả lời kém hơn cloud model, bạn đo và quyết định ra sao?
- Điều kiện tối thiểu để đưa local LLM vào production là gì?
13. Tài Liệu Liên Quan Trong Folder
document.md: code và cấu hình triển khai.exercise.md: bài tập 60-120 phút, benchmark và decision note.
Tài liệu
Tài liệu này đưa phần lý thuyết Day 29 thành một skeleton thực tế: config, OpenAI-compatible client abstraction, FastAPI proxy, health check, logging, timeout/retry và benchmark.
Các ví dụ dùng OpenAI Python client với base_url, timeout và max_retries theo API hiện hành, phù hợp khi runtime local expose OpenAI-compatible endpoint như Ollama hoặc vLLM.
1. Cấu Hình Runtime
Ollama
ollama pull llama3.2
ollama serve
Endpoint tham khảo:
Native: http://localhost:11434/api/chat
OpenAI-compatible: http://localhost:11434/v1
vLLM
vllm serve Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 \
--port 8000 \
--served-model-name local-chat
Endpoint tham khảo:
OpenAI-compatible: http://localhost:8000/v1
llama.cpp
./build/bin/llama-server \
-m ./models/model-q4_k_m.gguf \
--host 0.0.0.0 \
--port 8080 \
-c 4096 \
-ngl 99
Tùy version/build, API có thể khác nhau. Trước khi viết adapter production, luôn kiểm tra endpoint /health, /v1/models, /v1/chat/completions hoặc endpoint native mà runtime cung cấp.
2. Environment Config
Ví dụ .env:
LOCAL_LLM_BASE_URL=http://localhost:11434/v1
LOCAL_LLM_API_KEY=local
LOCAL_LLM_MODEL=llama3.2
LOCAL_LLM_RUNTIME=ollama
LOCAL_LLM_TIMEOUT_S=30
LOCAL_LLM_MAX_RETRIES=2
LOCAL_LLM_MAX_TOKENS=512
LOCAL_LLM_TEMPERATURE=0.2
LOG_LEVEL=INFO
Production nên pin thêm:
MODEL_ID=llama3.2
MODEL_REVISION=exact-revision-or-digest
MODEL_LICENSE=check-model-card
RUNTIME_VERSION=ollama-or-vllm-version
QUANTIZATION=q4_k_m-or-fp16
MAX_CONTEXT_TOKENS=4096
3. OpenAI-compatible Client Abstraction
File ví dụ local_llm_client.py:
from __future__ import annotations
import logging
import os
import time
import uuid
from dataclasses import dataclass
from typing import Any
from openai import OpenAI
from openai import APIConnectionError, APITimeoutError, RateLimitError, APIStatusError
logger = logging.getLogger("local_llm")
@dataclass(frozen=True)
class LocalLLMConfig:
base_url: str
api_key: str
model: str
runtime: str
timeout_s: float = 30.0
max_retries: int = 2
temperature: float = 0.2
max_tokens: int = 512
@classmethod
def from_env(cls) -> "LocalLLMConfig":
return cls(
base_url=os.getenv("LOCAL_LLM_BASE_URL", "http://localhost:11434/v1"),
api_key=os.getenv("LOCAL_LLM_API_KEY", "local"),
model=os.getenv("LOCAL_LLM_MODEL", "llama3.2"),
runtime=os.getenv("LOCAL_LLM_RUNTIME", "ollama"),
timeout_s=float(os.getenv("LOCAL_LLM_TIMEOUT_S", "30")),
max_retries=int(os.getenv("LOCAL_LLM_MAX_RETRIES", "2")),
temperature=float(os.getenv("LOCAL_LLM_TEMPERATURE", "0.2")),
max_tokens=int(os.getenv("LOCAL_LLM_MAX_TOKENS", "512")),
)
class LocalLLMClient:
def __init__(self, config: LocalLLMConfig) -> None:
self.config = config
self.client = OpenAI(
base_url=config.base_url,
api_key=config.api_key,
timeout=config.timeout_s,
max_retries=config.max_retries,
)
def chat(self, messages: list[dict[str, str]], trace_id: str | None = None) -> dict[str, Any]:
trace_id = trace_id or str(uuid.uuid4())
start = time.perf_counter()
try:
response = self.client.chat.completions.create(
model=self.config.model,
messages=messages,
temperature=self.config.temperature,
max_tokens=self.config.max_tokens,
)
latency_ms = (time.perf_counter() - start) * 1000
text = response.choices[0].message.content or ""
usage = getattr(response, "usage", None)
logger.info(
"llm_request_success",
extra={
"trace_id": trace_id,
"runtime": self.config.runtime,
"model": self.config.model,
"latency_ms": round(latency_ms, 2),
"input_tokens": getattr(usage, "prompt_tokens", None),
"output_tokens": getattr(usage, "completion_tokens", None),
},
)
return {
"trace_id": trace_id,
"runtime": self.config.runtime,
"model": self.config.model,
"text": text,
"latency_ms": latency_ms,
"input_tokens": getattr(usage, "prompt_tokens", None),
"output_tokens": getattr(usage, "completion_tokens", None),
}
except (APITimeoutError, APIConnectionError, RateLimitError, APIStatusError) as exc:
latency_ms = (time.perf_counter() - start) * 1000
logger.warning(
"llm_request_failed",
extra={
"trace_id": trace_id,
"runtime": self.config.runtime,
"model": self.config.model,
"latency_ms": round(latency_ms, 2),
"error_type": type(exc).__name__,
},
)
raise
Điểm production quan trọng:
base_urlgiúp trỏ cùng client tới Ollama, vLLM, llama.cpp hoặc cloud-compatible endpoint.timeouttránh request treo vô hạn.max_retriesxử lý lỗi tạm thời, nhưng không nên retry quá nhiều với request sinh text dài.- Log có
trace_id, runtime, model và latency; không log full prompt nếu có dữ liệu nhạy cảm.
4. FastAPI Proxy/Gateway
File ví dụ app.py:
from __future__ import annotations
import logging
import time
import uuid
from fastapi import FastAPI, HTTPException, Request
from openai import APIConnectionError, APITimeoutError, RateLimitError, APIStatusError
from pydantic import BaseModel, Field
from local_llm_client import LocalLLMClient, LocalLLMConfig
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("llm_gateway")
config = LocalLLMConfig.from_env()
llm = LocalLLMClient(config)
app = FastAPI(title="Local LLM Gateway", version="0.1.0")
class ChatMessage(BaseModel):
role: str = Field(pattern="^(system|user|assistant)$")
content: str = Field(min_length=1, max_length=20_000)
class ChatRequest(BaseModel):
messages: list[ChatMessage] = Field(min_length=1, max_length=30)
model_policy: str = "default"
class ChatResponse(BaseModel):
trace_id: str
runtime: str
model: str
text: str
latency_ms: float
input_tokens: int | None = None
output_tokens: int | None = None
@app.get("/health")
def health() -> dict[str, str]:
return {
"status": "ok",
"runtime": config.runtime,
"model": config.model,
}
@app.post("/chat", response_model=ChatResponse)
def chat(payload: ChatRequest, request: Request) -> dict[str, object]:
trace_id = request.headers.get("x-trace-id") or str(uuid.uuid4())
start = time.perf_counter()
try:
messages = [message.model_dump() for message in payload.messages]
result = llm.chat(messages=messages, trace_id=trace_id)
return result
except APITimeoutError as exc:
raise HTTPException(status_code=504, detail="LLM timeout") from exc
except (APIConnectionError, RateLimitError) as exc:
raise HTTPException(status_code=503, detail="LLM unavailable") from exc
except APIStatusError as exc:
raise HTTPException(status_code=502, detail=f"LLM upstream error: {exc.status_code}") from exc
finally:
logger.info(
"gateway_request_finished",
extra={
"trace_id": trace_id,
"path": str(request.url.path),
"latency_ms": round((time.perf_counter() - start) * 1000, 2),
},
)
Chạy local:
pip install -U fastapi uvicorn openai
uvicorn app:app --host 0.0.0.0 --port 9000
Gọi thử:
curl http://localhost:9000/chat \
-H 'content-type: application/json' \
-H 'x-trace-id: demo-001' \
-d '{
"messages": [
{"role": "user", "content": "Tóm tắt local LLM trong 3 bullet."}
]
}'
Production cần thêm:
- AuthN/AuthZ.
- Rate limit theo user/tenant.
- Request body size limit.
- Prompt redaction trước logging.
- Streaming endpoint nếu UX cần.
- Fallback policy.
- Circuit breaker nếu runtime lỗi liên tục.
- Metrics endpoint Prometheus hoặc OpenTelemetry.
5. Health Check Thật Hơn
/health chỉ kiểm tra process gateway sống. Với LLM serving, nên có thêm /ready hoặc scheduled probe:
@app.get("/ready")
def ready() -> dict[str, object]:
started = time.perf_counter()
try:
result = llm.chat(
messages=[{"role": "user", "content": "Reply with exactly: ok"}],
trace_id="readiness-probe",
)
return {
"status": "ready",
"runtime": config.runtime,
"model": config.model,
"latency_ms": round((time.perf_counter() - started) * 1000, 2),
"sample_ok": "ok" in result["text"].lower(),
}
except Exception:
logger.exception("llm_readiness_failed")
raise HTTPException(status_code=503, detail="LLM not ready")
Không nên gọi generation nặng trong health check quá thường xuyên. Với Kubernetes, tách:
- Liveness: process còn sống.
- Readiness: model đã load và endpoint nhận request.
- Synthetic probe: chạy định kỳ ngoài request path để kiểm tra chất lượng/latency.
6. Benchmark Script
File ví dụ benchmark_local_llm.py:
from __future__ import annotations
import concurrent.futures
import os
import statistics
import time
from dataclasses import dataclass
from openai import OpenAI
@dataclass(frozen=True)
class Sample:
name: str
prompt: str
samples = [
Sample("short", "Tóm tắt local LLM trong 3 bullet."),
Sample("json", "Trả lời JSON với keys: runtime, strengths, risks."),
Sample("long_context", "Giải thích KV cache cho senior software engineer. " * 80),
]
client = OpenAI(
base_url=os.getenv("LOCAL_LLM_BASE_URL", "http://localhost:11434/v1"),
api_key=os.getenv("LOCAL_LLM_API_KEY", "local"),
timeout=float(os.getenv("LOCAL_LLM_TIMEOUT_S", "60")),
max_retries=int(os.getenv("LOCAL_LLM_MAX_RETRIES", "1")),
)
model = os.getenv("LOCAL_LLM_MODEL", "llama3.2")
def run_once(sample: Sample) -> dict[str, object]:
started = time.perf_counter()
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": sample.prompt}],
temperature=0.2,
max_tokens=256,
)
latency_ms = (time.perf_counter() - started) * 1000
text = response.choices[0].message.content or ""
usage = getattr(response, "usage", None)
return {
"sample": sample.name,
"latency_ms": latency_ms,
"chars": len(text),
"input_tokens": getattr(usage, "prompt_tokens", None),
"output_tokens": getattr(usage, "completion_tokens", None),
}
def percentile(values: list[float], pct: float) -> float:
if not values:
return 0.0
values = sorted(values)
index = min(len(values) - 1, int(round((pct / 100) * (len(values) - 1))))
return values[index]
def main() -> None:
iterations = int(os.getenv("BENCH_ITERATIONS", "10"))
concurrency = int(os.getenv("BENCH_CONCURRENCY", "1"))
jobs = [sample for sample in samples for _ in range(iterations)]
started = time.perf_counter()
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
results = list(executor.map(run_once, jobs))
total_s = time.perf_counter() - started
latencies = [float(item["latency_ms"]) for item in results]
print(
{
"model": model,
"requests": len(results),
"concurrency": concurrency,
"total_s": round(total_s, 2),
"rps": round(len(results) / total_s, 2),
"latency_avg_ms": round(statistics.mean(latencies), 2),
"latency_p50_ms": round(percentile(latencies, 50), 2),
"latency_p95_ms": round(percentile(latencies, 95), 2),
"latency_p99_ms": round(percentile(latencies, 99), 2),
}
)
if __name__ == "__main__":
main()
Chạy:
BENCH_ITERATIONS=5 BENCH_CONCURRENCY=1 python benchmark_local_llm.py
BENCH_ITERATIONS=10 BENCH_CONCURRENCY=4 python benchmark_local_llm.py
Khi benchmark production candidate, ghi thêm bằng tool hệ thống:
nvidia-smi
docker stats
top
7. Decision Record Template
# Local LLM Decision Record
## Use case
- Task:
- User:
- Data sensitivity:
- Expected traffic:
- Latency SLO:
## Runtime tested
- Ollama:
- llama.cpp:
- vLLM:
- TGI:
- Cloud baseline:
## Model
- model_id:
- revision/digest:
- license:
- quantization:
- context window:
- runtime version:
## Benchmark
- prompt short p50/p95:
- prompt long p50/p95:
- concurrency tested:
- RAM/VRAM observed:
- error rate:
- quality score:
## Decision
- Dev runtime:
- Production candidate:
- Fallback:
- Rollout plan:
- Rollback plan:
## Risks
- Quality:
- Latency:
- Security/privacy:
- License:
- Ops:
8. Common Pitfalls
- Chọn model theo leaderboard nhưng không chạy golden eval của use case.
- Benchmark prompt ngắn rồi production lại dùng RAG context dài.
- Quên kiểm tra license model.
- Log full prompt chứa PII.
- Không giới hạn
max_tokens, dẫn tới latency và cost compute khó kiểm soát. - Retry request generation dài quá nhiều lần, làm tải tăng khi runtime đang lỗi.
- Không tách gateway khỏi runtime, khiến đổi Ollama sang vLLM phải sửa business code.
- Không có fallback khi GPU OOM hoặc runtime restart.
- Nhầm average latency với user experience; p95/p99 mới phản ánh sự khó chịu của user.
Bài tập
Thời lượng gợi ý: 60-120 phút.
Mục tiêu: chạy ít nhất một local model, gọi bằng OpenAI-compatible client, đo latency cơ bản, ghi decision note và trả lời có dùng production được không.
Phần 1: Setup Runtime
Chọn một trong hai hướng.
Option A: Ollama
ollama pull llama3.2
ollama run llama3.2
Kiểm tra API:
curl http://localhost:11434/api/chat -d '{
"model": "llama3.2",
"messages": [
{"role": "user", "content": "Trả lời một câu: local LLM là gì?"}
],
"stream": false
}'
Option B: vLLM
Chỉ chọn nếu có GPU và môi trường phù hợp.
vllm serve Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 \
--port 8000 \
--served-model-name local-chat
Kiểm tra OpenAI-compatible endpoint:
curl http://localhost:8000/v1/models
Phần 2: Gọi Bằng OpenAI-compatible Client
Cài dependency:
pip install -U openai
Tạo file local_llm_probe.py trong workspace tạm hoặc thư mục thực hành của bạn:
from __future__ import annotations
import os
import time
from openai import OpenAI
base_url = os.getenv("LOCAL_LLM_BASE_URL", "http://localhost:11434/v1")
api_key = os.getenv("LOCAL_LLM_API_KEY", "local")
model = os.getenv("LOCAL_LLM_MODEL", "llama3.2")
client = OpenAI(
base_url=base_url,
api_key=api_key,
timeout=30.0,
max_retries=2,
)
prompts = [
"Tóm tắt local LLM trong 3 bullet.",
"Trả lời JSON với keys: runtime, strengths, risks.",
"Giải thích KV cache cho senior software engineer.",
]
for prompt in prompts:
started = time.perf_counter()
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
temperature=0.2,
max_tokens=300,
)
latency_ms = (time.perf_counter() - started) * 1000
text = response.choices[0].message.content or ""
print({"model": model, "latency_ms": round(latency_ms, 2), "chars": len(text)})
print(text[:500])
Chạy:
LOCAL_LLM_BASE_URL=http://localhost:11434/v1 \
LOCAL_LLM_MODEL=llama3.2 \
python local_llm_probe.py
Với vLLM:
LOCAL_LLM_BASE_URL=http://localhost:8000/v1 \
LOCAL_LLM_MODEL=local-chat \
python local_llm_probe.py
Phần 3: Benchmark Nhỏ
Chạy mỗi prompt ít nhất 5 lần và ghi lại:
| Prompt | p50 latency | p95 latency | Output có đúng không | Ghi chú |
|---|---|---|---|---|
| Short summary | ||||
| JSON output | ||||
| KV cache explanation | ||||
| Long context |
Thêm một prompt dài:
Bạn là AI engineer. Hãy đọc context sau và trả lời ngắn gọn...
Sau đó lặp lại cùng nội dung 30-50 lần để mô phỏng RAG context dài. So sánh latency với prompt ngắn.
Phần 4: So Sánh Runtime Theo Context
Điền bảng sau:
| Context | Runtime chọn | Vì sao | Rủi ro |
|---|---|---|---|
| Dev laptop offline | |||
| Internal assistant 20 user | |||
| Batch summarize 100k docs/ngày | |||
| Edge device không internet | |||
| Chatbot public traffic cao |
Gợi ý:
- Dev laptop thường bắt đầu bằng Ollama hoặc LM Studio.
- Edge thường cân nhắc llama.cpp + GGUF.
- Throughput GPU cao thường cân nhắc vLLM hoặc TGI.
- Public traffic cần auth, rate limit, monitoring, fallback và security review.
Phần 5: Production Readiness Checklist
- Đã ghi model id, revision/digest và quantization.
- Đã đọc license/model card.
- Đã đo prompt ngắn và prompt dài.
- Đã đo p50/p95, không chỉ một lần chạy.
- Đã ghi RAM/VRAM observed.
- Đã thử concurrency tối thiểu 2-4 request song song.
- Đã giới hạn
max_tokens. - Đã có timeout.
- Đã có retry có kiểm soát.
- Đã không log full prompt nhạy cảm.
- Đã có health/readiness check.
- Đã có fallback plan.
- Đã có golden eval hoặc ít nhất sample set cho chất lượng.
- Đã trả lời được: production được không, điều kiện gì.
Phần 6: Quiz
- Local LLM có đảm bảo dữ liệu không leak không? Vì sao?
- Vì sao cùng một model nhưng runtime khác nhau có latency khác nhau?
- Quantization giảm VRAM bằng cách nào và rủi ro chính là gì?
- Tại sao prompt dài ảnh hưởng prefill nhiều hơn decode?
- KV cache tăng theo những yếu tố nào?
- Khi nào Ollama đủ dùng, khi nào nên chuyển sang vLLM?
- Khi nào llama.cpp là lựa chọn tốt hơn GPU serving?
- Vì sao OpenAI-compatible API không đủ để production nếu thiếu gateway?
- Những metric nào cần xem trước khi mở traffic thật?
- Nếu p95 latency vượt SLO, bạn sẽ thử 5 hướng tối ưu nào?
Phần 7: Bài Tập Thiết Kế
Thiết kế một local LLM service cho internal policy Q&A:
- 200 nhân viên.
- Tài liệu nội bộ không được gửi ra cloud.
- 80% request là hỏi đáp ngắn.
- 20% request có RAG context dài.
- SLO: p95 dưới 5 giây cho câu hỏi ngắn, dưới 12 giây cho câu hỏi dài.
- Cần audit log nhưng không được lưu PII thô.
Bạn cần nộp:
- Runtime chọn cho dev.
- Runtime chọn cho production.
- Model candidate.
- API contract.
- Logging fields.
- Benchmark plan.
- Fallback plan.
- Rủi ro còn lại.
Phần 8: Decision Note
# Day 29 Local LLM Decision Note
## Use case
- Task:
- Data sensitivity:
- Users/concurrency:
- Latency SLO:
## Runtime candidates
- Ollama:
- llama.cpp:
- vLLM:
- TGI:
- LM Studio:
## Model
- model_id:
- revision/digest:
- license:
- quantization:
- context window:
## Benchmark result
- short prompt p50/p95:
- long prompt p50/p95:
- concurrency:
- RAM/VRAM:
- quality notes:
## Production decision
- Dùng được trong production không:
- Điều kiện bắt buộc:
- Runtime chọn:
- Fallback:
- Rollback:
- Rủi ro còn lại: