4.1 Cơ bản về lượng tử hóa
Hiểu lượng tử hóa và giải lượng tử hóa qua thước có ít vạch chia: Scale, zero-point, độ rộng bit, độ hạt, hiệu chuẩn, cùng khác biệt giữa PTQ và QAT
Lượng tử hóa (quantization) giống như đổi sang thước có ít vạch hơn: trước đọc được tới milimet, giờ chỉ còn centimet. Dữ liệu nhỏ đi, và số đọc ra cũng thô hơn. Lượng tử hóa trả lời ba câu hỏi: cách đặt vạch chia (Scale và zero-point), bao nhiêu giá trị dùng chung một thước (độ hạt / granularity), và làm sao để mô hình ít bị ảnh hưởng bởi sai số đọc (hiệu chuẩn cùng PTQ/QAT).
Phần này chỉ nói các khái niệm không phụ thuộc framework. SmoothQuant, GPTQ/AWQ, lượng tử hóa KV Cache và số thực dấu phẩy thấp bit ở các phần sau đều xây trên những ý này.
⏰ Độ mới của thông tin: Phần này là nguyên lý chung, không phụ thuộc phiên bản framework cụ thể. Quy tắc làm tròn lấy định nghĩa toán tử QuantizeLinear của ONNX làm ví dụ.
📑 Mục lục
- 1. Trước hết hãy tách đối tượng lượng tử hóa
- 2. Ánh xạ số, Scale và Zero-point
- 3. Trục của tensor và độ hạt lượng tử hóa
- 4. Hiệu chuẩn: chọn khoảng đo cho thước
- 5. PTQ và QAT
- 6. Thực hành: quan sát ảnh hưởng của outlier
- Tóm tắt
- Danh sách tự kiểm
- Tài liệu tham khảo
1. Trước hết hãy tách đối tượng lượng tử hóa
Khi suy luận, bộ nhớ GPU chủ yếu chứa ba loại dữ liệu. Cả ba đều có thể lượng tử hóa, nhưng tính chất khác nhau:
| 📊 Đối tượng | Đặc điểm dữ liệu | Lợi ích chính sau lượng tử hóa |
|---|---|---|
| Weight (trọng số) | Cố định sau khi huấn luyện; có thể xử lý offline thong thả | Mô hình nhỏ hơn; mỗi bước đọc ít weight hơn |
| Activation (kích hoạt) | Đổi theo từng input; chỉ xử lý được online | Cho phép nhân ma trận đi kernel độ chính xác thấp |
| KV Cache | Tăng theo request và ngữ cảnh | Mỗi token lịch sử chiếm ít cache hơn |
Ký hiệu thường gặp W4A16 nghĩa là weight 4-bit, activation 16-bit; W8A8 nghĩa là cả hai đều 8-bit. Ký hiệu này chỉ mô tả hai đầu vào của lớp tuyến tính. Độ chính xác của KV Cache phải nói riêng.
Ước nhanh: 7B tham số toàn 16-bit khoảng 14 GB dữ liệu thô; toàn 4-bit khoảng 3.5 GB (GB thập phân, chưa tính tham số lượng tử hóa). Lượng dữ liệu còn một phần tư; tốc độ nhanh thêm bao nhiêu thì phụ thuộc điểm nghẽn nằm ở đâu, như Chương 1 đã nói.
2. Ánh xạ số, Scale và Zero-point
2.1 Lượng tử hóa và giải lượng tử hóa
FP16 bản thân cũng chỉ biểu diễn hữu hạn giá trị. Lượng tử hóa ánh xạ chúng sang ít mã hơn nữa. Với mã hóa số nguyên đều, lượng tử hóa affine thường viết:
- là Scale, khoảng số giữa hai vạch liền kề trên thước;
- là Zero-point, vạch số nguyên mà số thực 0 rơi vào;
- Lượng tử hóa cho mã ; giải lượng tử hóa (dequantization) dùng , để đưa mã về giá trị xấp xỉ .
Quy tắc round do từng triển khai quyết định. Ví dụ QuantizeLinear kiểu số nguyên của ONNX dùng “làm tròn nửa về số chẵn” (Round Half to Even).
Lấy : thành , đọc lại được . Phần 0.04 bị bỏ đi không lấy lại được. Đổi sang , cùng một 0.26 thành 4 và đọc lại 0.25. Vậy “giữ một chữ số thập phân” chỉ là trùng hợp của . Thứ thực sự quyết định biểu diễn được những giá trị nào là độ rộng bit và Scale. Mọi số trong một khoảng đều rơi vào cùng một ô; với , 0.251, 0.26 và 0.349 đều ánh xạ thành 3.
Hình dưới vẽ hai khoảng 4-bit trên cùng một trục số thực (đối xứng lấy ; không đối xứng dùng lại ở mục 2.3). Nền xanh là phạm vi lượng trình bao phủ: điểm xanh lá bị kéo về vạch gần nhất, tạo sai số làm tròn; điểm đỏ vượt ngoài lượng trình bị ép về đầu mút, tạo sai số cắt cụt. Sai số lượng tử hóa về cơ bản là hai loại này.
2.2 Lượng tử hóa đối xứng
Dữ liệu dương âm khá cân bằng thì có thể đặt . Quy ước INT8 phổ biến chỉ dùng :
Kiểu INT8 bản thân biểu diễn được tới . Ở đây ta chủ động bỏ nó để hai đầu đối xứng. 4-bit cũng chỉ dùng ; trục đối xứng trong hình trên được lấy như vậy. Lợi ích là giải lượng tử hóa chỉ cần nhân một Scale, và nhân ma trận không phải sửa zero-point.
2.3 Lượng tử hóa không đối xứng
Nếu dữ liệu toàn nằm trong , lượng tử hóa đối xứng sẽ lãng phí nửa số mã cho số âm không dùng tới. Lượng tử hóa không đối xứng để lượng trình bám theo dữ liệu: chọn khoảng chứa 0 là , rồi tính
Ví dụ , khoảng mã , được . Với , , giải lượng tử hóa ra . Zero-point phải làm tròn thành số nguyên, nên hai đầu lượng trình thực tế lệch một chút.
Lượng tử hóa không đối xứng khớp phân phối lệch tốt hơn. Cái giá là phải lưu thêm zero-point và thêm một bước sửa khi tính. Tensor toàn không cần xử lý riêng để tránh ra .
2.4 Độ rộng bit quyết định vạch dày đến đâu
Khi lượng trình cố định, mỗi lần bớt 1 bit thì số ô giảm nửa, khoảng cách ô nhân đôi. Lượng tử hóa đều :
| 📊 Độ rộng bit | Số mã | Khoảng cách vạch liền kề | Sai số làm tròn tối đa |
|---|---|---|---|
| 8-bit | 256 | ||
| 4-bit | 16 | ||
| 2-bit | 4 |
Từ 8-bit xuống 4-bit, khoảng cách ô thành khoảng 17 lần. Vì vậy các phương án thấp bit gần như luôn cần độ hạt mịn hơn hoặc bù sai số thông minh hơn. GPTQ, AWQ, KIVI phía sau đều đang giải quyết việc này.
3. Trục của tensor và độ hạt lượng tử hóa
3.1 Nhìn rõ các trục của ma trận
Chương này thống nhất dùng cách viết toán :
Hàng của là token; cột là kênh đầu vào. nn.Linear.weight của PyTorch có shape , đúng bằng chuyển vị của ở đây. Đọc code thì đừng đảo “hàng” với “cột”.
Xem một ma trận activation nhỏ (3 token, 4 kênh):
Cột 3 lớn trên mọi token. Hiện tượng “vài kênh liên tục lớn hơn hẳn” rất thường gặp ở activation của LLM, và là điểm xuất phát của SmoothQuant ở mục 4.2.
3.2 Bao nhiêu dữ liệu dùng chung một thước
Độ hạt là “nhóm dữ liệu dùng chung Scale lớn đến đâu”. Nhóm càng nhỏ, thước càng khớp dữ liệu cục bộ, nhưng cũng phải lưu và đọc nhiều Scale hơn:
| 📊 Độ hạt | Phạm vi chia sẻ | Dùng điển hình | Cái giá |
|---|---|---|---|
| Per-tensor | Cả tensor một Scale | Activation tĩnh, weight FP8 | Một outlier kéo cả nhóm |
| Per-channel | Mỗi kênh đầu ra một | Weight INT8 | Số Scale bằng số kênh đầu ra |
| Per-group | Mỗi phần tử liên tiếp một (vd. 128) | Weight INT4 | Nhiều Scale hơn; kernel phải giải lượng tử hóa theo nhóm |
| Per-token | Mỗi token một | Activation động | Runtime phải tìm max từng hàng trước |
Hình dưới vẽ bốn độ hạt trên layout . Ô cùng màu cùng nhãn dùng chung một Scale. góc dưới phải lấy từ ma trận trên; khung nét đứt đỏ là kênh đầu vào thứ 3 luôn lớn:
Với trên, Per-token cho mỗi hàng một lượng trình riêng, nhưng 8.0 và 0.2 trong cùng hàng vẫn dùng chung một thước, nên giá trị nhỏ vẫn bị thô. Độ hạt chỉ cắt được theo một trục nào đó. Hướng cắt không khớp hướng outlier thì có mịn hơn cũng chẳng giúp. Outlier của activation nằm theo kênh, trong khi nhân ma trận đòi hỏi Scale của activation rút được ra khỏi tổng (chỉ chia được theo token). Đó là gốc rễ khiến W8A8 khó làm.
4. Hiệu chuẩn: chọn khoảng đo cho thước
4.1 Lượng trình rộng thì mờ, hẹp thì đứt
Scale do lượng trình quyết định. Lấy lượng trình tới max thì outlier vừa, nhưng phần lớn dữ liệu chen vào vài ô và sai số làm tròn lớn; thu hẹp lượng trình thì phần lớn dữ liệu đọc mịn hơn, nhưng phần vượt ra bị cắt. Hiệu chuẩn (calibration) là cân bằng giữa sai số làm tròn và sai số cắt cụt. Cách thường gặp gồm lấy max, lấy phân vị, hoặc tìm điểm cắt theo mục tiêu MSE hay sai số đầu ra.
Weight cố định nên thống kê trực tiếp từ weight. Activation cần mẫu thật mới thấy phân phối; lô mẫu đó gọi là tập hiệu chuẩn (calibration set). Nó phải phủ ngôn ngữ, Chat Template và độ dài input thực tế trên production, và tách khỏi tập đánh giá, không thì điểm số sẽ lạc quan quá.
4.2 Tĩnh và động
- Lượng tử hóa tĩnh (static): dùng tập hiệu chuẩn tính Scale offline, suy luận dùng luôn. Chi phí runtime thấp nhất, nhưng phân phối input đổi thì có thể sai.
- Lượng tử hóa động (dynamic): lúc suy luận tính Scale theo dữ liệu hiện tại. Khớp input hơn; cái giá là mỗi lần phải tìm max trước.
Bản thân “động” không nói gì về độ hạt. Thấy một phương án tự xưng lượng tử hóa động thì còn phải xác nhận ba việc: Scale tính lúc nào, theo trục nào, và áp lên những toán tử nào.
5. PTQ và QAT
PTQ (Post-Training Quantization) lượng tử hóa ngay sau khi huấn luyện xong, nhiều nhất là chỉnh Scale hoặc weight bằng ít dữ liệu hiệu chuẩn. QAT (Quantization-Aware Training) thì trong lúc huấn luyện hoặc fine-tune đã để mô hình forward “đeo” sai số lượng tử hóa, để tham số tự thích nghi.
Forward của QAT dùng Fake Quant: lượng tử hóa rồi giải lượng tử hóa ngay. Dữ liệu vẫn là số thực, nhưng đã mang sai số làm tròn. Đạo hàm của round hầu như mọi nơi bằng 0, nên lan truyền ngược dùng xấp xỉ STE (Straight-Through Estimator): trong lượng trình truyền gradient nguyên xi, ngoài lượng trình thường đặt 0.
| 📊 Chiều | PTQ | QAT |
|---|---|---|
| Cần gì | Mô hình đã huấn luyện, thêm ít dữ liệu hiệu chuẩn | Quy trình huấn luyện, dữ liệu và sức tính |
| Kiểm soát sai số thế nào | Chọn Scale, điểm cắt, hoặc bù theo lớp (GPTQ/AWQ) | Để tham số thích nghi lượng tử hóa trong lúc huấn luyện |
| Hợp lúc nào | Weight 8-bit, 4-bit và các cảnh sai số còn kiểm soát được | Bit thấp hơn, hoặc khi độ chính xác PTQ chưa đạt |
| Vẫn phải kiểm | Độ chính xác trên tác vụ mục tiêu | Độ chính xác trên tác vụ mục tiêu, và kernel suy luận có hỗ trợ định dạng đó không |
Triển khai suy luận LLM chủ yếu dùng PTQ. Các phương pháp phía sau chương này về cơ bản đều thuộc PTQ.
💡 Gợi ý: PTQ không có nghĩa là không cần dữ liệu; hầu hết phương pháp vẫn cần tập hiệu chuẩn. QAT cũng không phải huấn luyện trên số nguyên; phần tính chính vẫn là số thực, sai số lượng tử hóa được mô phỏng bằng Fake Quant.
6. Thực hành: quan sát ảnh hưởng của outlier
Đoạn Python thuần dưới đây làm lượng tử hóa INT8 đối xứng trên 4 số, so sánh “bao phủ hết” và “cắt về 1”:
def quantize(values, amax):
scale = amax / 127 if amax > 0 else 1.0
codes = [max(-127, min(127, round(x / scale))) for x in values]
return codes, scale
def dequantize(codes, scale):
return [q * scale for q in codes]
x = [0.2, 0.5, 0.8, 20.0]
for label, bound in [("bao phủ hết", 20.0), ("cắt về 1", 1.0)]:
codes, scale = quantize(x, bound)
restored = dequantize(codes, scale)
errors = [abs(a - b) for a, b in zip(x, restored)]
print(label, "scale =", round(scale, 6))
print("mã:", codes)
print("khôi phục:", [round(v, 6) for v in restored])
print("sai số tuyệt đối:", [round(v, 6) for v in errors])
Khi lượng trình bao phủ 20, Scale khoảng 0.15748. Ba giá trị nhỏ chỉ khôi phục được khoảng 0.157, 0.472, 0.787, và hai cái đầu đã có sai số tương đối trên 5%. Cắt về 1 thì giá trị nhỏ gần như không sai, còn 20 bị ép thành 1 với sai số tới 19.
Cả hai lựa chọn đều không tốt: một outlier đủ khiến cả nhóm tiến thoái lưỡng nan. Chỉ chỉnh lượng trình không giải quyết được; hoặc chuyển outlier đi chỗ khác (SmoothQuant ở 4.2), hoặc thu nhỏ nhóm dùng chung Scale và bù sai số (GPTQ/AWQ ở 4.3).
📝 Tóm tắt
- Lượng tử hóa ánh xạ số sang ít mã hơn; sai số đến từ làm tròn và cắt cụt.
- Scale là khoảng cách vạch; Zero-point là ô chứa 0. Lượng tử hóa đối xứng bỏ bước sửa zero-point; không đối xứng khớp phân phối lệch tốt hơn.
- Mỗi lần bớt 1 bit thì khoảng cách vạch nhân đôi; khoảng cách 4-bit khoảng 17 lần so với 8-bit.
- Độ hạt quyết định bao nhiêu dữ liệu dùng chung một Scale; hướng cắt phải khớp hướng outlier mới có hiệu quả.
- Hiệu chuẩn chọn lượng trình giữa làm tròn và cắt cụt; “động” phải nói rõ thời điểm, trục và toán tử áp dụng.
- PTQ là dòng chính của suy luận LLM; QAT dùng Fake Quant và STE để tham số thích nghi sai số lượng tử hóa.
🎯 Danh sách tự kiểm
- Viết được công thức lượng tử hóa affine và giải lượng tử hóa, giải thích ý nghĩa của Scale và Zero-point
- Tính tay được Scale, zero-point và kết quả khôi phục của lượng tử hóa đối xứng lẫn không đối xứng
- Ước được dung lượng lưu weight của một mô hình ở 16-bit và 4-bit
- Nói được Per-tensor, Per-channel, Per-group, Per-token mỗi loại chia sẻ Scale theo trục nào
- Giải thích được vì sao outlier theo kênh của activation không thể chỉ dựa Per-token để xử lý
- Phân biệt được lượng tử hóa tĩnh và động, nêu ba việc cần xác nhận khi gặp một phương án “động”
- Nói được khác biệt PTQ và QAT, cùng vai trò của Fake Quant và STE