Bối cảnh lúc 2 giờ sáng: Khi server LLM gục ngã vì nghẽn request
Đúng 2 giờ sáng, chuông PagerDuty réo liên hồi. Toàn bộ cụm backend AI báo đỏ rực. Trên dashboard Datadog, p99 latency vọt thẳng từ 420ms lên 45 giây. Hàng trăm request ăn ngay lỗi HTTP 504 Gateway Timeout.
24GB VRAM trên con GPU NVIDIA A10G chạm trần 99%. Hệ thống lúc ấy vẫn chạy qua một wrapper FastAPI tự chế bọc quanh thư viện Transformers gốc. Khi traffic nhảy vọt từ 15 lên 120 req/s, server xử lý tuần tự từng request theo hàng đợi. KV cache phân mảnh nghiêm trọng khiến cả cụm crash đơ toàn tập.
Kinh nghiệm xương máu sau đêm đó: đừng bao giờ vác kiến trúc dev đi gánh tải production. Để phục vụ hàng trăm kết nối đồng thời trên một LLM, bạn bắt buộc phải dùng inference engine chuyên biệt.
So sánh 3 hướng tiếp cận LLM Production
Ngay trong đêm, mình đặt lên bàn cân 3 phương án để cứu cụm GPU:
- FastAPI + Transformers truyền thống: Viết API bọc pipeline của Hugging Face. Cách này dựng chỉ mất 15 phút, dễ sửa code, rất hợp để test prompt hay làm demo nội bộ.
- vLLM qua Docker container: Engine nguồn mở nổi tiếng với PagedAttention. Xử lý KV cache cực mượt, throughput rất cao khi chạy batch lớn và cộng đồng hỗ trợ đông đảo.
- Hugging Face Text Generation Inference (TGI) trên Docker: Giải pháp chuẩn production của chính Hugging Face, kết hợp Rust và C++/CUDA. Tích hợp sẵn Continuous Batching, PagedAttention, FlashAttention-2, Tensor Parallelism cùng endpoint tương thích chuẩn OpenAI và gRPC tốc độ cao.
Phân tích ưu nhược điểm từng phương án
1. FastAPI + Transformers
Điểm cộng lớn nhất là dễ viết và dễ gỡ lỗi. Tuy nhiên, điểm yếu chí mạng là thiếu continuous batching. Khi 10 user gửi request cùng lúc, người thứ hai vẫn phải đợi người thứ nhất sinh xong token cuối cùng. GPU utilization lúc vọt lên 100%, lúc tụt về 0%. VRAM phình to không kiểm soát, kéo theo lỗi CUDA Out-Of-Memory (OOM) triền miên.
2. vLLM
vLLM dẹp tan bài toán lãng phí RAM nhớ nhờ cơ chế chia trang PagedAttention. Tốc độ sinh token thuộc hàng top hiện nay. Điểm trừ nằm ở khâu tích hợp enterprise: nếu bạn cần kéo weights từ private repo trên Hugging Face Hub, dùng token authentication chặt chẽ hay đo metric streaming cấp token, vLLM cần thêm nhiều bước cấu hình runtime thủ công.
3. Hugging Face TGI
TGI sở hữu kiến trúc lai rất thực dụng. Router và scheduler viết bằng Rust giúp triệt tiêu độ trễ điều phối (zero-overhead scheduling). Trong khi đó, inference kernel chạy C++/CUDA tối ưu sâu. Image Docker hỗ trợ thẳng FlashAttention-2 cùng các chuẩn nén như AWQ, GPTQ hay BitsAndBytes mà không cần convert checkpoint. Nhược điểm: image Docker khá nặng (tầm 15-20GB) và đòi hỏi GPU kiến trúc Turing hoặc Ampere trở lên.
Vì sao mình chốt hạ TGI để cứu cụm Production?
Giữa cơn khủng hoảng cần kéo server sống lại ngay lập tức, TGI đáp ứng đúng 3 yêu cầu cấp bách:
- Image Docker chính thức đóng gói sẵn: Không mất thời gian sửa lỗi xung đột CUDA driver, PyTorch hay wheel. Chỉ cần host có NVIDIA Container Toolkit là gõ lệnh chạy ngay.
- Continuous Batching chuẩn chỉ: TGI không gom request thành batch tĩnh cố định. Ngay khi request A nhả xong token cuối, slot đó lập tức đón request B nhảy vào giữa chừng. Nhờ vậy, throughput tăng gấp 4 đến 5 lần so với script FastAPI cũ.
- Cơ chế bảo vệ VRAM chủ động: TGI chặn cứng số token vào/ra và hàng đợi concurrency. Nó tự động ngắt request timeout trước khi GPU kịp tràn bộ nhớ.
Hướng dẫn triển khai TGI trên Docker thực chiến
Bước 1: Kiểm tra môi trường host và NVIDIA Container Toolkit
Trước hết, hãy xác nhận host đã cài driver và Docker nhận card đồ họa. Chạy lệnh sau để kiểm tra:
docker run --rm --gpus all nvidia/cuda:12.1.1-base-ubuntu22.04 nvidia-smi
Bảng thông tin GPU hiện ra đầy đủ (A10G, A100 hay RTX 4090) là bạn đã sẵn sàng kéo image TGI.
Bước 2: Tạo thư mục cache và khởi chạy container
Đừng để container tải lại hàng chục GB weights mỗi lần khởi động lại. Hãy mount thư mục cache từ host sang container. Đây là lệnh triển khai mô hình Qwen/Qwen2.5-7B-Instruct đã cứu cánh cho team đêm đó:
mkdir -p /data/hf_cache
docker run -d \
--name tgi-server \
--gpus all \
--shm-size 1g \
-p 8080:80 \
-v /data/hf_cache:/data \
-e MODEL_ID=Qwen/Qwen2.5-7B-Instruct \
-e MAX_CONCURRENT_REQUESTS=128 \
-e MAX_INPUT_TOKENS=2048 \
-e MAX_TOTAL_TOKENS=4096 \
-e MAX_BATCH_PREFILL_TOKENS=4096 \
ghcr.io/huggingface/text-generation-inference:2.4.0
Bước 3: Tối ưu các tham số chống sập server
Tùy theo dung lượng VRAM thực tế, bạn cần tinh chỉnh các biến môi trường sau:
--shm-size 1g: Bắt buộc phải có. Worker Rust và Python dùng bộ nhớ chia sẻ này để IPC tốc độ cao. Thiếu nó, container sẽ crash vì cạn POSIX shared memory.MAX_INPUT_TOKENS&MAX_TOTAL_TOKENS: Chặn đứng các payload gửi prompt dài hàng nghìn token làm tràn KV cache.MAX_BATCH_PREFILL_TOKENS: Giới hạn token tính toán đồng thời ở bước prefill prompt. Với GPU A100 80GB, bạn có thể đẩy lên 8192 để tăng tốc độ phản hồi.MAX_CONCURRENT_REQUESTS: Ngưỡng trần hàng đợi. Vượt ngưỡng này, TGI trả về ngay mã HTTP 429 để bảo vệ GPU.
Bước 4: Theo dõi log khởi động
Container cần vài chục giây để nạp model vào VRAM và warm-up kernel. Kiểm tra tiến trình bằng lệnh:
docker logs -f tgi-server
Khi thấy log hiện Connected kèm thông báo Router started at 0.0.0.0:80, server đã sẵn sàng nhận request.
Bước 5: Test endpoint bằng cURL
TGI cung cấp sẵn /v1/chat/completions chuẩn OpenAI. Bạn có thể đo Time-To-First-Token (TTFT) bằng một lệnh cURL đơn giản:
curl -X POST http://localhost:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "tgi",
"messages": [
{"role": "system", "content": "Bạn là chuyên gia DevOps hỗ trợ xử lý sự cố."},
{"role": "user", "content": "Làm thế nào để kiểm tra GPU memory leak?"}
],
"max_tokens": 256,
"temperature": 0.7
}'
Bước 6: Nhận streaming token từ Python client
Để giao diện người dùng hiển thị chữ chạy mượt mà theo thời gian thực, hãy kết nối qua endpoint /generate_stream:
import requests
import json
url = "http://localhost:8080/generate_stream"
payload = {
"inputs": "Giải thích ngắn gọn cơ chế Continuous Batching:",
"parameters": {
"max_new_tokens": 200,
"temperature": 0.2
}
}
response = requests.post(url, json=payload, stream=True)
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith("data:"):
data = json.loads(decoded_line[5:])
print(data.get("token", {}).get("text", ""), end="", flush=True)
print()
Xử lý các ca lỗi thực chiến
- OOM ngay khi vừa khởi động: Nếu model 7B nặng quá 15GB VRAM (chuẩn FP16), hãy bật thêm cờ
-e QUANTIZE=awqhoặc-e QUANTIZE=bitsandbytes-nf4. Dung lượng VRAM cần thiết sẽ giảm xuống chỉ còn khoảng 5.5GB đến 6GB. - Độ trễ p99 đột ngột tăng vọt: Thường do client bắn prompt vượt độ dài quy định. Hãy siết chặt kích thước body request ngay tại Nginx hoặc Traefik phía trước để drop sớm.
- Cần chạy mô hình 70B trên nhiều GPU: Thêm biến
-e NUM_SHARD=4(tương ứng 4 GPU). TGI sẽ tự động chia tách tensor parallelism qua NCCL mà không cần cấu hình torchrun thủ công.
Sau 40 phút chuyển đổi toàn bộ sang TGI, kết quả đo lại lúc 3 giờ sáng: p99 latency tụt sâu từ 45 giây xuống còn 1.15 giây. Mức tiêu thụ VRAM giữ vững ở mức 81% trên 120 req/s. Đến 9 giờ sáng hôm sau, hệ thống vượt qua đợt cao điểm mà không dính một lỗi OOM nào.

