Triển khai vLLM trên Linux với Docker: Phục vụ LLM Hiệu Năng Cao với PagedAttention và API Tương thích OpenAI

AI tutorial - IT technology blog
AI tutorial - IT technology blog

Vấn Đề Khiến Tôi Thức Giấc Lúc 2 Giờ Sáng

Lúc 2 giờ 17 sáng, dashboard giám sát của tôi bắt đầu sáng lên. Server inference LLM tự host mà chúng tôi đang chạy cho chatbot hướng khách hàng đã dừng lại hoàn toàn. Độ trễ phản hồi tăng vọt từ 800ms lên 45 giây. Mức sử dụng bộ nhớ đứng im ở mức 100%. Dịch vụ không bị crash — nó chỉ đứng đó, bão hòa hoàn toàn, không thể xử lý hàng đợi 200 request đang chờ.

Chúng tôi đang chạy một setup HuggingFace Transformers đơn giản với wrapper Flask API tiêu chuẩn. Nó hoạt động tốt trong môi trường staging với 5 người dùng đồng thời. Production có tới 300. Tôi đã mắc một sai lầm kinh điển: cho rằng việc load model và gọi model.generate() trong một vòng lặp là kiến trúc sẵn sàng cho production.

Thực tế không phải vậy. Đêm đó đã thuyết phục tôi rằng vLLM tồn tại có lý do — và nếu bạn đang chạy bất kỳ LLM nào trong production, hiểu nó ngay bây giờ sẽ ít tốn kém hơn nhiều so với việc debug lúc 2 giờ sáng.

Nguyên Nhân Gốc Rễ: Tại Sao Cách Phục vụ LLM Đơn Giản Thất Bại Dưới Tải Nặng

Inference transformer tiêu chuẩn có một vấn đề bộ nhớ cơ bản. Mỗi request inference đều cần một KV (key-value) cache lưu giữ các trạng thái attention qua tất cả các bước sinh. Với cách triển khai đơn giản, cache này được cấp phát tĩnh. Trường hợp xấu nhất: bạn pre-allocate bộ nhớ cho độ dài sequence tối đa cho mọi request — dù đầu ra thực tế chỉ là 5 token hay 500.

Dưới tải đồng thời, điều này có nghĩa là:

  • Bộ nhớ GPU bị lấp đầy bởi các allocation KV cache phần lớn trống rỗng
  • Request mới bị xếp hàng hoặc bị từ chối vì không còn bộ nhớ
  • Throughput sụp đổ khi GPU ngồi chờ bộ nhớ được giải phóng

Wrapper Flask của tôi xử lý từng request một (đồng bộ). Mỗi request giữ bộ nhớ GPU trong suốt thời gian tồn tại của nó, chặn tất cả mọi thứ còn lại. 300 người dùng × xử lý tuần tự = thảm họa.

Các Giải Pháp Tôi Đã Xem Xét

Phương án 1: TGI (Text Generation Inference của HuggingFace)

Giải pháp serving riêng của HuggingFace. Ổn định, tài liệu tốt, dùng Rust cho tầng HTTP. Hạn chế: nó sử dụng continuous batching nhưng quản lý bộ nhớ không tích cực như vLLM. Với các model lớn trên GPU cấp consumer, tôi thấy nó gặp lỗi OOM sớm hơn dưới tải burst.

Phương án 2: Triton Inference Server (NVIDIA)

Cực kỳ mạnh mẽ, được dùng trong các setup doanh nghiệp. Nhưng độ phức tạp khi cài đặt là có thật — bạn cần các backend dành riêng cho từng model, file cấu hình và kiến thức sâu về tooling NVIDIA. Quá mức cần thiết cho một team chỉ cần phục vụ một hoặc hai model một cách đáng tin cậy.

Phương án 3: vLLM

Thuần Python, tương thích OpenAI API, và được xây dựng đặc biệt xung quanh một kỹ thuật gọi là PagedAttention. Đây là thứ tôi đã chọn dùng, và tôi sẽ khuyến nghị nó cho 90% các team đang chạy LLM trên phần cứng của riêng mình.

Điều Gì Làm vLLM Khác Biệt: Giải Thích PagedAttention

PagedAttention là đổi mới cốt lõi. Được vay mượn từ khái niệm virtual memory trong hệ điều hành, nó chia KV cache thành các block có kích thước cố định (“trang”) được cấp phát theo nhu cầu, không pre-allocate theo từng request.

Thực tế, điều này có nghĩa:

  • Phản hồi ngắn không lãng phí bộ nhớ dành cho các phản hồi dài
  • Bộ nhớ từ các request đã hoàn thành được thu hồi ngay lập tức
  • GPU có thể phục vụ nhiều request đồng thời hơn nhiều với cùng lượng VRAM

Ngoài ra, vLLM triển khai continuous batching. Thay vì chờ để tập hợp một batch đầy, nó chèn các request mới vào batch đang chạy khi có dung lượng trống. GPU utilization duy trì cao ngay cả trong điều kiện traffic không đều, bùng phát.

Kết quả: trong các benchmark của tôi, vLLM xử lý throughput cao hơn 8–12 lần so với setup HuggingFace đơn giản trên cùng phần cứng.

Triển Khai vLLM với Docker trên Linux

Yêu Cầu Trước

  • Host Linux (khuyến nghị Ubuntu 20.04+)
  • GPU NVIDIA với ít nhất 16GB VRAM (24GB+ cho các model 7B ở độ chính xác đầy đủ)
  • NVIDIA drivers 525+ đã cài đặt
  • Docker Engine với nvidia-container-toolkit
  • Tài khoản HuggingFace với quyền truy cập model (nếu dùng các model có kiểm soát)

Bước 1: Cài Đặt NVIDIA Container Toolkit

Nếu bạn chưa thiết lập quyền truy cập GPU trong Docker:

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
  sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
  sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

Xác minh quyền truy cập GPU trong container:

docker run --rm --gpus all nvidia/cuda:12.1.0-base-ubuntu22.04 nvidia-smi

Bước 2: Pull và Chạy vLLM

vLLM duy trì một Docker image chính thức. Để kiểm tra nhanh với meta-llama/Llama-3.2-3B-Instruct (yêu cầu token HuggingFace):

docker run --gpus all \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  -e HUGGING_FACE_HUB_TOKEN=your_token_here \
  -p 8000:8000 \
  --ipc=host \
  vllm/vllm-openai:latest \
  --model meta-llama/Llama-3.2-3B-Instruct \
  --dtype bfloat16

Các flag quan trọng ở đây:

  • --ipc=host: Bắt buộc để chia sẻ bộ nhớ giữa các tiến trình (quan trọng cho hiệu năng)
  • -v ~/.cache/huggingface: Mount cache để model không bị tải lại mỗi lần restart
  • --dtype bfloat16: Inference half-precision, giảm VRAM ~50% với tổn thất chất lượng tối thiểu

Bước 3: Docker Compose cho Production

Cho production, hãy dùng file Compose phù hợp:

version: '3.8'
services:
  vllm:
    image: vllm/vllm-openai:latest
    restart: unless-stopped
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    environment:
      - HUGGING_FACE_HUB_TOKEN=${HF_TOKEN}
    ports:
      - "8000:8000"
    ipc: host
    command: >
      --model meta-llama/Llama-3.2-3B-Instruct
      --dtype bfloat16
      --max-model-len 4096
      --gpu-memory-utilization 0.90
      --tensor-parallel-size 1

Flag --gpu-memory-utilization 0.90 cho vLLM biết sử dụng 90% VRAM cho pool KV cache. Tôi để lại 10% dự phòng để tránh lỗi OOM. Nếu bạn thấy crash OOM, hãy giảm xuống 0.85.

Bước 4: Kiểm Tra API Tương thích OpenAI

API này là sự thay thế hoàn toàn cho OpenAI. Sau khi server khởi động — chờ dòng log Uvicorn running on http://0.0.0.0:8000 — gọi nó bằng curl:

curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "meta-llama/Llama-3.2-3B-Instruct",
    "messages": [{"role": "user", "content": "Giải thích KV cache trong một câu."}],
    "max_tokens": 100
  }'

Hoặc dùng OpenAI SDK, trỏ vào server local của bạn:

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed",  # vLLM không yêu cầu xác thực theo mặc định
)

response = client.chat.completions.create(
    model="meta-llama/Llama-3.2-3B-Instruct",
    messages=[{"role": "user", "content": "PagedAttention là gì?"}],
    max_tokens=200,
    temperature=0.7,
)
print(response.choices[0].message.content)

Bất kỳ code nào hoạt động với OpenAI SDK đều hoạt động với vLLM — không cần thay đổi ứng dụng.

Tối Ưu cho Tải Thực Tế

Tăng Concurrency

Thêm các flag này dựa trên phần cứng của bạn:

# Cho các setup đa GPU (ví dụ: 2× A100)
--tensor-parallel-size 2

# Giới hạn số sequence đồng thời tối đa để ngăn memory spike
--max-num-seqs 128

# Bật prefix caching cho các system prompt lặp lại (lợi thế lớn cho chatbot)
--enable-prefix-caching

Flag cuối đó tạo ra sự khác biệt lớn nhất cho trường hợp chatbot của tôi. Nếu mọi request đều chia sẻ một system prompt dài, vLLM cache trạng thái KV của nó sau request đầu tiên và tái sử dụng cho tất cả các request tiếp theo. Phần prompt tiêu tốn gần như không có compute. Với system prompt 1.000 token xử lý 10.000 request mỗi ngày, đó là sự giảm đáng kể thời gian GPU.

Quantization để Giảm VRAM

Đang chạy model 13B trên GPU 16GB? Hãy dùng AWQ hoặc GPTQ quantization:

docker run --gpus all -p 8000:8000 --ipc=host \
  vllm/vllm-openai:latest \
  --model TheBloke/Llama-2-13B-chat-AWQ \
  --quantization awq \
  --dtype float16

AWQ 4-bit cắt giảm VRAM khoảng một nửa. Throughput giảm nhẹ, nhưng với workload chatbot sự khác biệt chất lượng đầu ra hầu như không đáng kể. Nó không lý tưởng cho mọi thứ — nếu bạn cần đầu ra chính xác hoặc xác định, hãy benchmark cẩn thận trước khi cam kết.

Điều Tôi Sẽ Làm Khác Vào Đêm Đó

Nếu có thể quay lại, tôi sẽ triển khai vLLM ngay từ đầu thay vì cách làm tạm bợ với HuggingFace Transformers. Khả năng tương thích API nghĩa là không cần nỗ lực migration — chỉ cần cập nhật base URL trong config ứng dụng của bạn. Docker image xử lý toàn bộ setup CUDA. Và PagedAttention làm cho hành vi bộ nhớ đủ dự đoán được để bạn thực sự có thể lập kế hoạch dung lượng dựa trên VRAM thay vì đoán mò.

Một điều khiến tôi bất ngờ: lần khởi động đầu tiên mất 10–20 phút trong khi vLLM load và tối ưu hóa model. Hãy thêm health check phù hợp trong Docker Compose hoặc load balancer — chờ endpoint /health trả về 200 trước khi định tuyến traffic.

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 300s

Giá trị start_period: 300s cho vLLM 5 phút để khởi tạo trước khi Docker bắt đầu đếm lỗi health check. Nếu không có nó, Docker sẽ restart container vì nghĩ nó không khỏe trong giai đoạn khởi động bình thường.

Sự cố lúc 2 giờ sáng khiến chúng tôi mất 40 phút dịch vụ bị suy giảm. Sau khi chuyển sang vLLM, cùng server đó xử lý 300 người dùng đồng thời với độ trễ trở lại dưới một giây. Nguyên nhân gốc rễ chưa bao giờ là phần cứng. Đó là quản lý bộ nhớ — và đó là phần mà việc thay đổi framework thực sự giải quyết được.

Share: