4.6 Chọn lượng tử hóa và thực hành vLLM
Chọn ứng viên lượng tử hóa theo bottleneck, hiểu quan hệ giữa phương pháp, định dạng checkpoint và Kernel trong vLLM v0.32.0, rồi đối chiếu throughput và chất lượng sinh giữa FP16 và AWQ INT4
Phương pháp đã nói xong. Quay lại một bài toán triển khai cụ thể: trong tay một mô hình và một GPU, thử lượng tử hóa nào trước? Giống như đi khám: trước hết tìm triệu chứng khó chịu nhất (ràng buộc chặt nhất), loại các liệu pháp cơ thể không chịu nổi (đường phần cứng và backend không hỗ trợ), rồi dùng kết quả kiểm tra (đánh giá chất lượng và hiệu năng) để xác nhận hiệu quả. Mục này dựng thứ tự ứng viên trước, rồi so sánh FP16 với AWQ INT4 trên vLLM.
⏰ Độ cập nhật: Cổng lượng tử hóa, tên phương án và lựa chọn Kernel của vLLM dựa trên v0.32.0 (phát hành 2026-10-18). Ma trận hỗ trợ lượng tử hóa đổi khá nhanh; sau khi đổi phiên bản hãy đối chiếu lại tài liệu và log khởi động.
📑 Mục lục
- 1. Chọn ứng viên từ bottleneck
- 2. vLLM nhận mô hình lượng tử hóa thế nào
- 3. Khởi động dịch vụ FP16 và AWQ
- 4. Đo throughput offline với biến được kiểm soát
- 5. So sánh chất lượng sinh thế nào
- 6. Đọc kết quả và các vấn đề thường gặp
- Tóm tắt
- Checklist tự kiểm
- Tài liệu tham khảo
1. Chọn ứng viên từ bottleneck
Hình dưới là luồng chọn: trước hết theo ràng buộc chặt nhất vào một trong ba nhánh ứng viên xanh dương, vàng, lục, rồi thống nhất so chất lượng với baseline 16-bit; khung nền lục nghĩa là đạt chuẩn rồi mới so hiệu năng, khung nền đỏ nghĩa là chưa đạt thì điều chỉnh rồi quay lại đánh giá chất lượng:
Các nhánh có thể kết hợp. Khi weight không nhét nổi và cache ngữ cảnh dài cũng thiếu, hãy xác nhận INT4 weight trước, rồi chồng thêm FP8 KV Cache, mỗi lần thêm một hạng mục thì đánh giá riêng một lần.
| 📊 Ràng buộc chính | Ưu tiên ứng viên | Bước tiếp theo cần xác nhận |
|---|---|---|
| Chịu suy giảm độ chính xác thấp | Lấy FP16/BF16 làm baseline, thử 8-bit trước (FP8 hoặc INT8 W8A8) | Chất lượng trên bài toán mục tiêu; 8-bit cũng có thể không đạt |
| Weight không nhét nổi, Batch nhỏ đọc weight chậm | GPTQ/AWQ INT4 (W4A16) | Group Size, Kernel thực tế, các lớp nhạy |
| Ngữ cảnh dài hoặc đồng thời bị KV giới hạn | FP8 KV Cache | Scale đã calibration chưa, backend Attention, chất lượng văn bản dài |
| GEMM chiếm tỉ trọng tính toán cao | W8A8 mà phần cứng hỗ trợ; trên Blackwell đánh giá NVFP4 | Đường toán tử thực tế và lợi ích end-to-end |
Phần cứng sẽ lọc bớt một phần ứng viên trước. Theo tài liệu vLLM v0.32.0: tính toán FP8 cần compute capability 8.9 trở lên, trên Ampere chỉ đi được W8A16; INT8 W8A8 hỗ trợ từ Turing đến Hopper, không hỗ trợ Blackwell; tính toán gốc của NVFP4 cần Blackwell, GPU khác fallback về Marlin W4A16.
Khi chưa có checkpoint lượng tử hóa, cần sinh offline trước:
- LLM Compressor: do dự án vLLM duy trì, phủ GPTQ, AWQ, SmoothQuant, FP8 và calibration Scale cho KV Cache, xuất định dạng
compressed-tensors. - GPTQModel: tập trung dòng GPTQ.
- NVIDIA Model Optimizer: xuất các định dạng ModelOpt như FP8, NVFP4, MXFP8.
Calibration và xuất là một quy trình riêng. Thí nghiệm chính của mục này dùng thẳng checkpoint đã công bố để tập trung kiểm chứng hiệu quả triển khai.
2. vLLM nhận mô hình lượng tử hóa thế nào
2.1 Tách riêng method, format và Kernel
| 📊 Tên | Thuộc tầng nào | Khi load nhìn gì |
|---|---|---|
| GPTQ, AWQ | Thuật toán lượng tử hóa và định dạng checkpoint phổ biến của nó | Bit width, Group Size, zero-point, cách xếp weight |
compressed-tensors | Hệ định dạng và cấu hình có thể mang nhiều phương án | Thực tế có thể là INT8, INT4, FP8… |
| FP8, NVFP4, MXFP4 | Định dạng số và phương án scale tương ứng | Cấu hình checkpoint, phần cứng, backend |
| Marlin, Machete, CUTLASS… | Kernel thực thi | Có khớp layout weight và thiết bị hay không |
vLLM nhận phương án từ quant_method trong cấu hình checkpoint, thường không cần ghi --quantization. Ở v0.32.0 tên method và Kernel đã tách: awq, awq_marlin, auto_awq đều resolve về cùng cấu hình auto_awq, GPTQ tương tự; Kernel cụ thể do vLLM chọn theo độ ưu tiên (mục 4.3). Vì vậy dù có ghi --quantization hay không, vẫn phải xem log khởi động để xác nhận Kernel thực tế, ví dụ Using MarlinLinearKernel for AutoAWQMarlinLinearMethod, hoặc Selected <kernel> for <module> ở lớp FP8.
2.2 Ý nghĩa của các cổng vào khác nhau
Các lệnh dưới là cổng vào của các cấu hình khác nhau; mỗi lần chỉ khởi động một cái, đường dẫn local đổi thành checkpoint đã xuất:
# Đã có checkpoint AWQ: cấu hình nhận bit width, tự chọn Kernel
vllm serve Qwen/Qwen2.5-7B-Instruct-AWQ --dtype half
# Đã có checkpoint compressed-tensors (LLM Compressor xuất): cấu hình nhận phương án cụ thể
vllm serve /path/to/compressed-tensors-model
# Đã có checkpoint ModelOpt NVFP4
vllm serve /path/to/nvfp4-model --quantization modelopt_fp4
# FP8 online: lúc load chuyển weight BF16/FP16 thành FP8, không cần dữ liệu calibration
vllm serve Qwen/Qwen2.5-7B-Instruct --quantization fp8_per_tensor
Lượng tử hóa online hiện có các phương án như fp8_per_tensor, fp8_per_channel, fp8_per_block, mxfp8, mxfp4 (chi tiết định dạng ở mục 4.5); activation tính Scale động mỗi lần forward. Tài liệu ghi fp8_per_tensor mang lại ít lợi ích latency, hợp để thử nhanh; khi cần hiệu năng và chất lượng, dùng checkpoint đã calibration offline.
⚠️ Lưu ý: Cách viết lượng tử hóa online --quantization fp8 của bản cũ không còn được hỗ trợ ở v0.32.0; dùng trên checkpoint chưa lượng tử hóa sẽ báo lỗi và bảo đổi sang fp8_per_tensor. fp8 vẫn dùng được để load checkpoint vốn đã là FP8.
Chuyển đổi online xảy ra ở giai đoạn load. FP8 online của v0.32.0 trước hết dựng weight độ chính xác gốc trên thiết bị meta (chỉ có shape, không chiếm VRAM), mỗi lớp tuyến tính load xong weight là chuyển ngay sang FP8, không đưa cả bộ weight 16-bit vào VRAM cùng lúc. Dù vậy nó vẫn đọc checkpoint độ chính xác gốc và không nhận được lợi ích calibration offline, nên triển khai chính thức nên chuẩn bị sẵn checkpoint lượng tử hóa offline.
2.3 Ghi lại môi trường thí nghiệm trước
Tiếp tục dùng môi trường Linux/CUDA của chương 3, không cài lại. Trước thí nghiệm ít nhất ghi:
python -c "import vllm, torch; print('vLLM:', vllm.__version__); print('PyTorch:', torch.__version__); print('CUDA:', torch.version.cuda)"
nvidia-smi
Revision của kho mô hình cũng cần ghi; tốt nhất tải snapshot cố định trước rồi đổi tham số mô hình sau thành đường dẫn local. Model GPU, driver, phiên bản vLLM, cấu hình checkpoint và Kernel thực tế đều là một phần của kết quả. Phần dưới không điền sẵn số throughput; hãy đo trên thiết bị của bạn.
3. Khởi động dịch vụ FP16 và AWQ
Trước hết chạy baseline FP16 trên mô hình gốc:
vllm serve Qwen/Qwen2.5-7B-Instruct \
--served-model-name quant-demo \
--dtype half \
--max-model-len 4096 \
--max-num-seqs 32 \
--gpu-memory-utilization 0.9 \
--generation-config vllm \
--no-enable-prefix-caching
Gửi vài request, lưu log rồi dừng process, xác nhận VRAM đã giải phóng, rồi khởi động AWQ:
vllm serve Qwen/Qwen2.5-7B-Instruct-AWQ \
--served-model-name quant-demo \
--dtype half \
--max-model-len 4096 \
--max-num-seqs 32 \
--gpu-memory-utilization 0.9 \
--generation-config vllm \
--no-enable-prefix-caching
Hai bên giữ cùng kiểu activation, trần ngữ cảnh, trần đồng thời và độ chính xác cache, đều không truyền --quantization, để cấu hình checkpoint quyết định. Checkpoint Qwen gốc phát hành dạng BF16; ở đây thống nhất FP16 để khớp đường activation FP16 của AWQ (xem thẻ mô hình AWQ).
Mỗi lần khởi động ghi ba thứ: chỗ chiếm của weight mô hình, dung lượng Token KV khả dụng, và Kernel lượng tử hóa thực tế. Phần chỗ weight mà AWQ tiết kiệm được vLLM dành cho KV Cache, nên tổng chiếm trong nvidia-smi sẽ không co về một phần tư; cái đổi là dung lượng KV.
4. Đo throughput offline với biến được kiểm soát
4.1 Trả lời trước một câu hỏi có giới hạn
Thí nghiệm dưới chỉ trả lời: cùng số Token đầu vào, độ dài đầu ra cố định, cùng cấu hình engine, một batch request hoàn thành nhanh thế nào? Nó không cho TTFT/TPOT đuôi của online, cũng không đại diện hành vi dịch vụ dưới tốc độ đến thật.
Tắt dịch vụ online ở trên, lưu đoạn mã dưới thành compare_quant.py. Script mỗi lần chỉ load một mô hình, warm-up rồi đo ba vòng, lưu output Token/s, tổng Token/s và mẫu sinh. Hai mô hình dùng cùng Tokenizer để dựng đầu vào; giai đoạn throughput tắt Prefix Cache và bỏ qua EOS để cố định lượng đầu ra.
import argparse
import json
import statistics
import time
from pathlib import Path
from transformers import AutoTokenizer
from vllm import LLM, SamplingParams
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--model", required=True)
parser.add_argument("--tokenizer", default="Qwen/Qwen2.5-7B-Instruct")
parser.add_argument("--quantization", default=None)
parser.add_argument("--num-prompts", type=int, default=64)
parser.add_argument("--input-len", type=int, default=512)
parser.add_argument("--output-len", type=int, default=256)
parser.add_argument("--save", required=True)
args = parser.parse_args()
if min(args.num_prompts, args.input_len, args.output_len) <= 0:
parser.error("Số request và độ dài Token phải dương")
if args.input_len + args.output_len > 4096:
parser.error("Tổng đầu vào và đầu ra không được vượt quá trần 4096 Token của thí nghiệm này")
tokenizer = AutoTokenizer.from_pretrained(args.tokenizer)
# Dựng đầu vào độ dài cố định, lặp lại được. Batch này chỉ đo tốc độ, không dùng để chấm chất lượng.
prompts = []
for i in range(args.num_prompts):
unit = tokenizer.encode(
f"Mã request {i}. Hãy giải thích lượng tử hóa ảnh hưởng thế nào tới bộ nhớ và tính toán khi suy luận.",
add_special_tokens=False,
)
ids = (unit * (args.input_len // len(unit) + 1))[:args.input_len]
prompts.append({"prompt_token_ids": ids})
llm = LLM(
model=args.model,
tokenizer=args.tokenizer,
quantization=args.quantization,
dtype="half",
max_model_len=4096,
max_num_seqs=32,
max_num_batched_tokens=2048,
gpu_memory_utilization=0.9,
kv_cache_dtype="auto",
enable_prefix_caching=False,
generation_config="vllm",
seed=0,
)
params = SamplingParams(
temperature=0, max_tokens=args.output_len, ignore_eos=True, seed=0
)
llm.generate(prompts, params, use_tqdm=False) # Warm-up đủ tải, không tính giờ
measurements = []
for _ in range(3):
start = time.perf_counter()
outputs = llm.generate(prompts, params, use_tqdm=False)
elapsed = time.perf_counter() - start
output_tokens = sum(len(o.outputs[0].token_ids) for o in outputs)
expected = args.num_prompts * args.output_len
if output_tokens != expected:
raise RuntimeError(f"Đầu ra thực tế {output_tokens}, kỳ vọng {expected}")
input_tokens = sum(len(o.prompt_token_ids) for o in outputs)
measurements.append({
"seconds": elapsed,
"output_tokens_per_second": output_tokens / elapsed,
"total_tokens_per_second": (input_tokens + output_tokens) / elapsed,
})
# Mẫu chất lượng khôi phục EOS bình thường và dùng Chat Template thống nhất; không đưa vào đo throughput.
questions = [
"Chỉ trả JSON: name là Alice, count là số nguyên 3, không dùng hàng rào Markdown.",
"Có 17 thùng hàng, mỗi thùng 24 món, xuất 89 món rồi còn lại bao nhiêu? Hãy nêu phép tính.",
"Viết một hàm Python kiểm tra chuỗi có phải palindrome hay không, bỏ qua chữ hoa/thường và ký tự không phải chữ số.",
]
chats = [[{"role": "user", "content": q}] for q in questions]
texts = [tokenizer.apply_chat_template(
chat, tokenize=False, add_generation_prompt=True
) for chat in chats]
examples = llm.generate(
texts, SamplingParams(temperature=0, max_tokens=512, seed=0),
use_tqdm=False,
)
result = {
"config": vars(args),
"runs": measurements,
"median_output_tokens_per_second": statistics.median(
r["output_tokens_per_second"] for r in measurements
),
"examples": [
{"question": q, "answer": o.outputs[0].text}
for q, o in zip(questions, examples)
],
}
Path(args.save).write_text(
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
4.2 Chạy riêng hai process
python compare_quant.py \
--model Qwen/Qwen2.5-7B-Instruct \
--save fp16.json
python compare_quant.py \
--model Qwen/Qwen2.5-7B-Instruct-AWQ \
--save awq-int4.json
Lệnh thứ nhất xong mới chạy lệnh thứ hai, tránh hai engine tranh VRAM. Mỗi bên gửi 64 request, mỗi cái đầu vào 512 và đầu ra 256 Token, trần số sequence engine xử lý đồng thời là 32. Muốn xem kịch bản Batch nhỏ, thêm --num-prompts 1 --input-len 64 vào cả hai lệnh và lưu kết quả riêng.
Đồng hồ bọc cả lời gọi generate() dạng blocking, gồm cả lịch trình cục bộ và ghép đầu ra; tải mô hình, load và lần warm-up đầu không tính. Khi so nghiêm túc, giữ trạng thái GPU rảnh nhất quán, xen kẽ hai cấu hình và quan sát dao động; muốn phân tích thời gian toán tử thì dùng thêm công cụ Profiling.
5. So sánh chất lượng sinh thế nào
Ba câu hỏi trong script chỉ là kiểm tra khói, để phát hiện suy giảm rõ ở định dạng, số học và khả năng viết mã:
| 📊 Mẫu | Cách kiểm |
|---|---|
| Đầu ra JSON | Parse được thành object, trường và giá trị đúng, count là số nguyên |
| Số học | Phải ra , các bước và đáp án khớp |
| Hàm palindrome | Thử với chuỗi rỗng, A man, a plan, a canal: Panama, ab và các đầu vào tương tự |
Hai bản đầu ra khác từng chữ không đồng nghĩa chất lượng suy giảm. Dù temperature = 0, đường số khác nhau cũng không bảo đảm văn bản giống hệt; hãy chấm theo yêu cầu bài toán và theo dõi thay đổi tỉ lệ đạt.
Đánh giá chính thức phải độc lập với tập calibration và phủ workload thật: lệnh tiếng Trung, mã, toán, đầu ra có cấu trúc, hỏi đáp tài liệu dài… Cố định Prompt, Chat Template, độ dài sinh tối đa và quy tắc chấm rồi mới so baseline với mô hình đã lượng tử hóa. PPL có thể bổ sung, nhưng không thay được tỉ lệ mã đúng hay độ chính xác retrieval.
Khi một lớp bài suy giảm rõ, trước hết kiểm tra template, cắt ngắn và cấu hình lượng tử hóa; sau khi bản thân phép đánh giá đã nhất quán, mới xét độ chi tiết mịn hơn, giữ lớp nhạy ở độ chính xác cao, nâng bit width hoặc calibration lại. PTQ vẫn không đạt mà lại có điều kiện huấn luyện thì mới xét QAT.
6. Đọc kết quả và các vấn đề thường gặp
Gộp hai file JSON và log khởi động thành bảng dưới. Hạng mục chưa chạy thì để trống, đừng lấy số từ paper hay thiết bị khác để điền:
| 📊 Chỉ số | Baseline FP16 | AWQ INT4 |
|---|---|---|
| Model revision / vLLM / GPU | Cần ghi | Cần ghi |
| Kernel lượng tử hóa thực tế | Không áp dụng | Cần ghi |
| Chỗ chiếm weight / dung lượng Token KV | Cần ghi | Cần ghi |
| Trung vị output Token/s (3 vòng) | Cần đo | Cần đo |
| Tổng Token/s và độ dài đầu vào/đầu ra | Cần đo | Cần đo |
| Tỉ lệ đạt bài / mẫu thất bại | Cần đánh giá | Cần đánh giá |
Tỉ số output Token/s có thể coi là tốc độ tăng dưới tải cố định này. Tổng throughput tính cả Token đầu vào phải liệt kê riêng; hai cách đo không được trộn khi so.
| 📊 Hiện tượng | Ưu tiên kiểm |
|---|---|
| Checkpoint AWQ không load được | Có phải checkpoint AWQ thật không, cấu hình và phiên bản công cụ có khớp không |
| INT4 nhét được nhưng chậm hơn FP16 | Kernel thực tế trong log, có cảnh báo “Falling back to unoptimized AWQ kernels” không, hình dạng ma trận và bottleneck hiện tại |
| Tổng VRAM gần như không giảm | Pool KV có nuốt hết chỗ weight tiết kiệm được không; xem dung lượng Token KV |
| Batch nhỏ nhanh, Batch lớn cải thiện ít | GEMM đã chuyển sang compute-bound chưa (mục 4.3) |
| Chất lượng văn bản dài giảm | Độ chính xác KV, Scale đã calibration chưa, độ dài cắt, phạm vi đánh giá văn bản dài |
| Mô hình NVFP4 không nhanh hơn | GPU có Kernel FP4 gốc không, log có báo fallback về Marlin W4A16 không |
💡 Gợi ý: Muốn loại một Kernel để đối chứng, dùng biến môi trường VLLM_DISABLED_KERNELS; backend lớp tuyến tính của các định dạng như NVFP4 có thể cố định bằng --linear-backend.
Khi baseline FP16 không chạy được trên thiết bị hiện tại, ghi thẳng giới hạn dung lượng. Có thể giảm tải chung, hoặc làm lại đối chứng trên máy đủ VRAM. Chỉ chạy được mô hình đã lượng tử hóa chứng minh triển khai khả thi, nhưng không suy ra được nhanh hơn FP16 bao nhiêu.
📝 Tóm tắt
- Bắt đầu từ bottleneck weight, KV hoặc tính toán để chọn ứng viên, rồi lọc đường không hỗ trợ theo phần cứng: tính toán FP8 cần CC 8.9+, INT8 W8A8 không hỗ trợ Blackwell, tính toán gốc NVFP4 cần Blackwell.
- Thuật toán lượng tử hóa, định dạng checkpoint, định dạng số và Kernel thực thi là bốn việc khác nhau; vLLM v0.32.0 tự nhận phương án từ checkpoint và chọn Kernel theo độ ưu tiên, lấy log khởi động làm chuẩn.
- Lượng tử hóa online dùng các tên phương án như
fp8_per_tensor,fp8_per_block,mxfp8,mxfp4; cách viết online cũ--quantization fp8không còn được hỗ trợ. - Thí nghiệm đối chứng phải cố định nguồn mô hình, độ dài đầu vào/đầu ra, cấu hình engine và cách đo, mỗi lần chỉ đổi một biến.
- Giá trị của lượng tử hóa là lợi ích bộ nhớ hoặc throughput đổi được khi chất lượng và latency vẫn đạt.
🎯 Checklist tự kiểm
- Nói được theo bottleneck hiện tại nên thử lượng tử hóa nào trước, và vì sao
- Nói được ngưỡng phần cứng của FP8, INT8 W8A8, NVFP4 trong vLLM v0.32.0
- Giải thích được vì sao
compressed-tensorskhông có một bit width duy nhất - Từ log khởi động phán được Kernel thực tế của mô hình lượng tử hóa, và có xảy ra fallback hay không
- Viết được tham số đúng cho lượng tử hóa FP8 online ở v0.32.0
- Phân biệt được tỉ lệ nén, tốc độ tăng throughput offline và chỉ số latency online
- Chỉ ra được thí nghiệm mục này đã kiểm soát những biến nào, còn thiếu những đánh giá chất lượng nào
📚 Tài liệu tham khảo
- vLLM v0.32.0 — Tổng quan hỗ trợ Quantization
- vLLM v0.32.0 — Online Quantization
- vLLM v0.32.0 — AutoAWQ
- vLLM v0.32.0 — INT4 W4A16 (LLM Compressor)
- vLLM v0.32.0 — INT8 W8A8 (LLM Compressor)
- vLLM v0.32.0 — FP8 W8A8
- vLLM v0.32.0 — NVIDIA Model Optimizer
- Thẻ mô hình Qwen2.5-7B-Instruct-AWQ
- LLM Compressor