TungDaDev's Blog

vllm & paged attention

Vllm paged attention.jpg
Published on
/9 mins read/

Trong thế giới của LLM Inference, kẻ chiến thắng không phải là kẻ có GPU mạnh nhất, mà là kẻ quản lý từng byte bộ nhớ VRAM thông minh nhất.

Nếu bạn đã từng thử tự host (self-host) một mô hình ngôn ngữ lớn như Llama-3-70B, Qwen-2.5-32B hay DeepSeek trên cụm máy chủ GPU nội bộ của doanh nghiệp, bạn chắc chắn đã đâm sầm vào bức tường mang tên: CUDA Out of Memory (OOM).

Một nghịch lý cay đắng xảy ra:

  • Bạn mua 4 card NVIDIA A100 (80GB VRAM mỗi chiếc) — tổng cộng 320GB VRAM.
  • Mô hình 70B sau khi lượng tử hóa (Quantization) chỉ chiếm khoảng 35 - 40GB VRAM tĩnh.
  • Bạn nghĩ rằng mình có thể phục vụ hàng trăm kỹ sư đồng thời? Hoàn toàn không. Chỉ cần 20 - 30 request gửi đến với context dài (ví dụ: prompt 8k tokens), GPU lập tức báo tràn bộ nhớ và crash toàn bộ service.

Tại sao lại như vậy? Thủ phạm giấu mặt đứng sau 80% sự lãng phí VRAM trong suy luận LLM chính là: Sự phân mảnh và phình to mất kiểm soát của KV Cache (Key-Value Cache).

Và đó chính là lý do vì sao dự án vLLM cùng phát minh PagedAttention (ra đời từ nhóm nghiên cứu tại UC Berkeley) đã làm thay đổi hoàn toàn cục diện của hạ tầng LLM Serving toàn cầu, đưa thông lượng (throughput) tăng từ 2 đến 24 lần so với các thư viện truyền thống như HuggingFace Transformers.

Bài viết này sẽ đi sâu vào tầng vật lý của GPU, mổ xẻ cơ chế tính toán Attention, giải phẫu thuật toán PagedAttention và hướng dẫn bạn thiết lập một cụm inference server chuẩn Enterprise.


# gốc rễ vấn đề: sự bùng nổ của kv cache

Để hiểu tại sao vLLM là một bước đột phá, ta cần hiểu cách một mô hình Transformer sinh từ (Autoregressive Generation):

Mỗi khi sinh ra một token mới, mô hình phải tính toán sự liên hệ của token đó với toàn bộ các token đứng trước nó thông qua phép tính Self-Attention:

\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

Nếu không có cơ chế lưu đệm, để sinh ra token thứ 1000, bạn phải tính toán lại Key (K) và Value (V) của 999 token trước đó. Để tránh lãng phí tính toán, kỹ thuật KV Caching được áp dụng: Lưu toàn bộ vector K và V của các token đã qua vào VRAM.

Vấn đề 1: Bộ nhớ động không thể đoán trước

Độ dài của câu trả lời không bao giờ biết trước. Một câu trả lời có thể kết thúc ở 50 tokens, nhưng cũng có thể kéo dài tới 4.000 tokens.

Vấn đề 2: Sự lãng phí khủng khiếp của Memory Fragmentation

Trong các hệ thống inference truyền thống (như HuggingFace):

  1. Internal Fragmentation (Phân mảnh nội vi): Vì không biết trước request dài bao nhiêu, hệ thống buộc phải cấp phát trước một mảng bộ nhớ liên tục (contiguous memory) cho độ dài tối đa (ví dụ 4.096 tokens). Nếu request chỉ sinh 200 tokens, 95% vùng nhớ đã cấp phát bị bỏ hoang!
  2. External Fragmentation (Phân mảnh ngoại vi): Các request vào và ra ngẫu nhiên làm bộ nhớ GPU bị xé nhỏ thành các lỗ trống lắt nhắt, không đủ khoảng trống liên tục để cấp phát cho request mới dù tổng dung lượng trống còn rất nhiều.

Thống kê từ UC Berkeley: Trong các hệ thống LLM truyền thống, có tới 60% - 80% bộ nhớ VRAM bị lãng phí chỉ vì phân mảnh bộ nhớ của KV Cache!


# giải pháp pagedattention: khi os hội ngộ cùng ai

Đứng trước bài toán này, các nhà nghiên cứu nhận ra một sự tương đồng kỳ lạ: Bài toán này giống hệt bài toán phân mảnh RAM mà các hệ điều hành (Operating Systems) đã giải quyết từ 50 năm trước bằng Bộ nhớ ảo (Virtual Memory & Paging)!

Tại sao cứ phải bắt KV Cache nằm trên một dải ô nhớ vật lý liên tục (contiguous memory)?

Bản chất của PagedAttention:

  1. Chia nhỏ thành các Block (Trang nhớ): Thay vì cấp phát một dải ô nhớ khổng lồ, vLLM chia nhỏ KV Cache thành các khối cố định (thường là 16 hoặc 32 tokens mỗi block).
  2. Không cần liên tục về mặt vật lý: Block 0 có thể nằm ở đầu VRAM, Block 1 nằm ở cuối VRAM, Block 2 nằm ở giữa.
  3. Quản lý qua Bảng phân trang (Block Table): Tương tự như Page Table trong CPU, GPU chỉ cần duy trì một bảng tra cứu để ánh xạ từ Logical Token Index sang Physical Memory Address.
  4. Cấp phát theo nhu cầu (On-Demand Allocation): Khi nào token thực sự được sinh ra và lấp đầy block hiện tại, hệ thống mới xin thêm một block vật lý mới. Tỷ lệ lãng phí bộ nhớ giảm từ 80% xuống dưới 4%!

# 2 tính năng sát thủ khác của vllm

1. Copy-on-Write & Shared Prefix Caching (Tái sử dụng Prompt chung)

Trong các tác vụ như Multi-turn Chat hay RAG, System Prompt hoặc đoạn tài liệu tham khảo thường giống hệt nhau giữa nhiều lượt tương tác.

  • Hệ thống cũ: Mỗi request copy lại toàn bộ KV Cache của prompt \to nhân đôi, nhân ba dung lượng VRAM.
  • vLLM: Nhiều request cùng trỏ tới chung các physical blocks của System Prompt. Chỉ khi nào có request ghi đè nội dung mới, vLLM mới tạo bản copy riêng (Copy-on-Write).

2. Continuous Batching (Batching động liên tục)

Batching truyền thống (Static Batching) gặp hiện tượng "con sâu làm rầu nồi canh": Trong 1 batch 4 requests, nếu 3 request đã xong ở token 50 nhưng 1 request dài 1000 tokens, toàn bộ batch phải đứng chờ request cuối cùng xong mới giải phóng GPU.

  • Continuous Batching trong vLLM: Ngay khi một request hoàn thành ở bất kỳ bước nào, vLLM lập tức đẩy request mới trong hàng đợi vào thế chỗ trống ngay tại bước tiếp theo (iteration-level scheduling). GPU hoạt động 100% công suất liên tục không có thời gian chết.

# hướng dẫn triển khai vllm chuẩn production với docker

vLLM cung cấp sẵn interface tương thích hoàn toàn 100% với OpenAI API Standard (/v1/chat/completions), nghĩa là bất kỳ thư viện nào (LangChain, Spring AI, Cursor, n8n) đều có thể cắm vào vLLM như thể đang gọi OpenAI.

File docker-compose.yml tối ưu hóa GPU:

version: '3.8'
 
services:
  vllm-server:
    image: vllm/vllm-openai:latest
    container_name: vllm-llama3
    runtime: nvidia
    environment:
      - HUGGING_FACE_HUB_TOKEN=hf_your_token_here
    ports:
      - '8000:8000'
    volumes:
      - ~/.cache/huggingface:/root/.cache/huggingface
    ipc: host
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    command: >
      --model meta-llama/Meta-Llama-3-8B-Instruct
      --tensor-parallel-size 1
      --gpu-memory-utilization 0.90
      --max-model-len 8192
      --swap-space 4
      --disable-log-requests

Các tham số sống còn cần lưu ý:

  • --gpu-memory-utilization 0.90: Yêu cầu vLLM dành 90% dung lượng VRAM cho mô hình và KV Cache (chừa 10% cho PyTorch overhead).
  • --max-model-len 8192: Giới hạn context length tối đa. Đặt con số này sát với nhu cầu thực tế giúp vLLM tối ưu hóa số lượng concurrent blocks.
  • --tensor-parallel-size: Nếu bạn có 2 hoặc 4 GPU (ví dụ 2x RTX 4090 hoặc 4x A100), đặt giá trị này tương ứng để chia đều mô hình qua các card.

# tổng kết

Thành công của vLLM là một minh chứng tuyệt đẹp cho việc: Những nguyên lý cơ bản của khoa học máy tính cổ điển (như Paging trong Hệ điều hành) khi được áp dụng sáng tạo vào lĩnh vực AI hiện đại có thể tạo ra những bước nhảy vọt phi thường.

Khi đưa AI vào hệ sinh thái ứng dụng của doanh nghiệp, thay vì phụ thuộc hoàn toàn vào API đóng của các Big Tech, việc làm chủ hạ tầng Local Inference với vLLM không chỉ giúp công ty bạn tiết kiệm hàng ngàn USD chi phí hàng tháng mà còn bảo vệ an toàn tuyệt đối tài sản dữ liệu nội bộ.


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.