Tăng tốc suy luận LLM với Speculative Decoding: Hướng dẫn thực hành trên vLLM và llama.cpp

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

Khởi động nhanh: Tăng gấp đôi tốc độ sinh token chỉ trong 5 phút

Việc serving các mô hình ngôn ngữ lớn (LLM) trong các ứng dụng tương tác luôn vấp phải cùng một rào cản: băng thông bộ nhớ (memory bandwidth). Trong quá trình autoregressive decoding, việc sinh mỗi token đòi hỏi phải đọc toàn bộ trọng số từ bộ nhớ băng thông cao (HBM). Trên một mô hình 70B tham số chạy trên hai card A100, bước nghẽn bộ nhớ (memory-bound) này khiến tốc độ sinh token chậm như rùa bò—thường chỉ đạt đỉnh khoảng 18 đến 22 token mỗi giây với batch size 1.

Speculative decoding giải quyết triệt để nút thắt cổ chai này. Một draft model nhỏ gọn, nhẹ nhàng sẽ nhanh chóng sinh ra một số token ứng viên. Sau đó, target model cỡ lớn của bạn sẽ xác thực đồng thời tất cả các ứng viên đó chỉ trong một lượt forward pass duy nhất. Chi phí tính toán gần như tương đương với việc sinh một token, nhưng bạn lại thu về từ 3 đến 5 token cùng lúc.

Dưới đây là cách thiết lập pipeline hoàn chỉnh trên cả vLLM lẫn llama.cpp.

Lựa chọn A: Thiết lập nhanh với vLLM

Nếu bạn đã chạy vLLM trên cụm GPU NVIDIA, việc kích hoạt speculative decoding chỉ cần thêm ba cờ dòng lệnh trỏ đến một draft model nhỏ hơn tương thích.

pip install vllm

# Khởi chạy Llama-3.1-70B-Instruct với Llama-3.1-8B-Instruct làm draft model
vllm serve meta-llama/Meta-Llama-3.1-70B-Instruct \
    --speculative-model meta-llama/Meta-Llama-3.1-8B-Instruct \
    --num-speculative-tokens 5 \
    --tensor-parallel-size 4 \
    --port 8000

Sau khi nạp xong trọng số, hãy gửi một yêu cầu completion thử nghiệm để kiểm tra engine:

curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "meta-llama/Meta-Llama-3.1-70B-Instruct",
    "messages": [{"role": "user", "content": "Viết một hàm Python để đọc và xử lý các file JSONL một cách an toàn."}],
    "temperature": 0.1
  }'

Lựa chọn B: Thiết lập tiết kiệm tài nguyên với llama.cpp

Trên Apple Silicon, máy trạm cục bộ hoặc các edge node, llama.cpp hỗ trợ sẵn speculative sampling thông qua cờ -md (model draft).

# Build llama.cpp với hỗ trợ Metal / CUDA
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp && cmake -B build && cmake --build build --config Release -j

# Chạy Qwen2.5-14B với Qwen2.5-1.5B làm draft model
./build/bin/llama-speculative \
    -m models/qwen2.5-14b-instruct-q4_k_m.gguf \
    -md models/qwen2.5-1.5b-instruct-q4_k_m.gguf \
    -p "Giải thích cơ chế TCP flow control trong ba gạch đầu dòng:" \
    -ngl 99 -ngld 99 \
    --draft-max 6

Trên máy Mac M2 Pro (32GB unified memory), cấu hình này đã đưa tốc độ sinh token của chúng tôi từ 17,8 token/giây lên thẳng 33,4 token/giây—tăng ngay 87% tốc độ mà không cần sửa đổi mã nguồn.

Nguyên lý hoạt động thực sự của Speculative Decoding

Tại sao kỹ thuật này hoạt động hiệu quả mà không làm giảm chất lượng đầu ra? Tất cả bắt nguồn từ cường độ số học (arithmetic intensity).

Nút thắt cổ chai về băng thông bộ nhớ

Quá trình sinh token tiêu chuẩn bị nghẽn bộ nhớ cực kỳ nghiêm trọng. Cứ mỗi token được sinh ra, GPU phải nạp hàng chục gigabyte trọng số vào SRAM trên chip, thực hiện một phép nhân ma trận – vector rất nhỏ, rồi loại bỏ các trọng số đó. Các nhân tính toán thuần túy phải chịu cảnh nhàn rỗi lên đến 80% thời gian.

Xử lý theo batch nhiều token sẽ thay đổi hoàn toàn cục diện. Việc xử lý một prompt chứa 6 token trên GPU hiện đại mất khoảng thời gian gần như tương đương với xử lý một token đơn lẻ. Speculative decoding tận dụng chính năng lực tính toán nhàn rỗi này.

Vòng lặp thực thi hai tầng

Chu trình giải mã diễn ra qua ba giai đoạn chặt chẽ:

  1. Bước Draft (Dự đoán sơ bộ): Một mô hình cùng họ nhỏ gọn (ví dụ 1.5B hoặc 8B) chạy autoregressively trong K vòng lặp (thường là 4 đến 6 token). Vì draft model rất nhỏ nên việc truyền dữ liệu bộ nhớ hoàn tất chỉ trong vài mili-giây.
  2. Xác thực song song (Parallel Verification): Target model cỡ lớn sẽ xử lý prompt cùng với tất cả K token ứng viên trong một lượt forward pass song song duy nhất.
  3. Chấp nhận & Hiệu chỉnh (Acceptance & Correction): Target model so sánh xác suất của draft token với xác suất của chính nó. Nó sẽ chấp nhận tuần tự các token hợp lệ cho đến khi phát hiện sai lệch. Tại vị trí token sai lệch đầu tiên đó, target model sẽ lấy mẫu (sample) token thay thế trực tiếp từ phân phối xác suất của nó.

Liệu điều này có làm thay đổi kết quả đầu ra? Hoàn toàn không. Speculative decoding sử dụng phương pháp modified rejection sampling. Về mặt toán học, phân phối đầu ra khớp chính xác từng bit với phân phối xác suất thực tế của target model. Bạn giữ nguyên được chiều sâu suy luận y hệt trong khi giảm được một nửa độ trễ end-to-end.

Cấu hình nâng cao cho môi trường Production

Chạy speculative decoding trên một prompt đơn lẻ rất đơn giản. Nhưng duy trì thông lượng (throughput) xuyên suốt hàng nghìn request đồng thời trong production đòi hỏi phải tính toán tài nguyên và tinh chỉnh kỹ lưỡng. Trong sáu tháng qua, chúng tôi đã xử lý hơn 14 triệu request tìm kiếm và lập trình mỗi ngày bằng chính kiến trúc này.

1. Dự đoán không tốn VRAM qua Prompt Lookup

Bạn không phải lúc nào cũng cần đến một mạng neural thứ hai. Khi bộ nhớ GPU đã bão hòa, các phương pháp dự đoán theo cơ chế heuristic như Prompt Lookup Decoding hoặc bộ nhớ đệm N-gram mang lại hiệu năng vượt trội mà không tốn thêm bất kỳ dung lượng VRAM nào.

from vllm import LLM, SamplingParams

sampling_params = SamplingParams(temperature=0.0, max_tokens=256)

# Cấu hình n-gram speculative decoding mà không cần tải draft model
llm = LLM(
    model="mistralai/Mistral-7B-Instruct-v0.3",
    speculative_model="[ngram]",
    num_speculative_tokens=5,
    ngram_prompt_lookup_max=4,
    trust_remote_code=True
)

prompts = ["Tóm tắt đoạn hội thoại chăm sóc khách hàng: Đơn hàng #9823 bị hoãn do thời tiết tại Chicago..."]
outputs = llm.generate(prompts, sampling_params)

for output in outputs:
    print(output.outputs[0].text)

Trong các tác vụ tóm tắt tài liệu, tái cấu trúc mã (code refactoring) và trích xuất dữ liệu có cấu trúc, các token đầu ra thường xuyên lặp lại các thuật ngữ từ prompt. Phương pháp tra cứu n-gram speculative đem lại tỷ lệ chấp nhận (acceptance rate) từ 58% đến 64% trên các workload này, trong khi mức tiêu thụ bộ nhớ hoàn toàn không tăng thêm.

2. Hiệu chuẩn độ dài nhìn trước (K)

Cờ --num-speculative-tokens chỉ định số lượng token ứng viên mà draft model sẽ sinh ra trước khi target model xác thực. Việc lựa chọn con số này là một bài toán tối ưu hóa:

  • K = 3 đến 4: Tối ưu nhất cho văn xuôi mở, văn bản sáng tạo hoặc suy luận đa bước phức tạp, nơi tỷ lệ chấp nhận draft token giảm xuống dưới 50%.
  • K = 5 đến 6: Điểm cân bằng lý tưởng cho việc sinh nội dung có cấu trúc (payload JSON, cú pháp SQL, kiểm thực schema). Khả năng dự đoán token cao giúp duy trì tỷ lệ chấp nhận trên 75%.
  • K > 8: Hiệu suất giảm dần (diminishing returns). Nếu draft engine đoán sai ngay ở token thứ 2, việc xác thực các token từ 3 đến 8 chỉ làm lãng phí chu kỳ tính toán của GPU mục tiêu mà không mang lại kết quả gì.

3. Phân bổ VRAM trên các node đa GPU

Draft model dùng chung VRAM vật lý với mô hình chính. Việc nạp một draft model 8B định dạng FP16 tiêu tốn khoảng 16GB VRAM, điều này trực tiếp cắt giảm dung lượng dành cho KV cache và hạ thấp ngưỡng xử lý đồng thời tối đa của bạn.

Để tránh hiện tượng quá tải KV cache (KV cache thrashing) trên một node 4x A100 (80GB) chạy Llama-3.1-70B, hãy lượng tử hóa (quantize) draft model sang FP8 hoặc ghim mô hình một cách tối ưu trên topology phần cứng:

vllm serve meta-llama/Meta-Llama-3.1-70B-Instruct \
    --tensor-parallel-size 4 \
    --speculative-model neuralmagic/Meta-Llama-3.1-8B-Instruct-FP8 \
    --speculative-draft-tensor-parallel-size 1 \
    --gpu-memory-utilization 0.90 \
    --max-num-seqs 64

Ghi chép thực tế từ môi trường Production

Bắt buộc đồng bộ hoàn toàn Tokenizer

Draft model và target model bắt buộc phải chia sẻ cùng một bộ từ vựng (vocabulary) và cơ chế tokenization. Việc kết hợp một target model Llama-3 với một draft model Qwen sẽ gây lỗi crash ngay lập tức vì bảng token ID của chúng khác nhau. Hãy luôn sử dụng các cặp mô hình thuộc cùng một dòng kiến trúc:

  • Target: Llama-3.1-70B → Draft: Llama-3.1-8B hoặc Llama-3.2-3B
  • Target: Qwen2.5-72B → Draft: Qwen2.5-1.5B hoặc Qwen2.5-7B
  • Target: DeepSeek-Coder-33B → Draft: DeepSeek-Coder-1.3B

Duy trì Temperature dưới 0.4

Speculative decoding mang lại mức cải thiện hiệu năng lớn nhất khi phân phối token có độ tập trung cao (sharp). Ở mức temperature = 0.0 (greedy decoding), target model và draft model có độ tương đồng cao nhất, duy trì tỷ lệ chấp nhận trên 72%.

Khi temperature tăng lên gần mức 0.8 hoặc 1.0, tỷ lệ chấp nhận giảm mạnh xuống chỉ còn khoảng 25%. Tại ngưỡng đó, chi phí xử lý phụ trội (overhead) của draft model sẽ triệt tiêu hoàn toàn tốc độ tăng thêm từ việc xác thực, dẫn đến độ trễ tổng thể thậm chí còn cao hơn cả phương pháp autoregression thông thường.

Theo dõi tỷ lệ chấp nhận trong Prometheus

Hãy expose các metric của vLLM sang Prometheus và thiết lập cảnh báo cho chỉ số vllm:spec_decode_draft_acceptance_rate. Trong các cụm production hoạt động ổn định, metric này thường dao động trong khoảng 0.55 đến 0.75. Nếu tỷ lệ chấp nhận tụt xuống dưới 0.40, hãy chủ động tắt speculative decoding hoặc chuyển xuống sử dụng n-gram lookup để tránh lãng phí năng lực tính toán của máy chủ.

Share: