Bỏ qua

Lượng tử hóa

Lượng tử hóa làm giảm độ chính xác (precision) của trọng số và activation của mô hình, giúp mô hình nhỏ hơn, nhanh hơn và rẻ hơn khi chạy. File này bao quát các định dạng số, lượng tử hóa hậu huấn luyện, lượng tử hóa có nhận thức trong huấn luyện (QAT), các phương pháp chỉ lượng tử hóa trọng số (GPTQ, AWQ), lượng tử hóa activation, độ chính xác hỗn hợp, và lượng tử hóa cache KV

  • Một mô hình 70B tham số ở dạng float16 cần tới 140 GB bộ nhớ, vượt quá dung lượng của bất kỳ GPU đơn nào. Lượng tử hóa xuống INT4 giúp nó nằm gọn trong 35 GB (một GPU A100) hoặc thậm chí 20 GB (card RTX 4090 phổ thông với offloading). Lượng tử hóa không phải là một thủ thuật tối ưu hóa cho vui: nó là thứ làm cho việc triển khai các mô hình lớn trở nên khả thi về mặt kinh tế.

  • Sự đánh đổi cơ bản: độ chính xác thấp hơn đồng nghĩa với ít bộ nhớ hơn, thông lượng cao hơn và tiêu thụ điện thấp hơn, nhưng đổi lại nó sinh ra lỗi lượng tử hóa (quantisation error) có thể làm giảm chất lượng mô hình. Nghệ thuật của lượng tử hóa nằm ở việc tối thiểu hóa sự suy giảm chất lượng này.

Tại sao phải lượng tử hóa

  • Giảm bộ nhớ: INT8 nhỏ bằng một nửa FP16, INT4 nhỏ bằng một phần tư. Với các LLM, trọng số mô hình chiếm phần lớn bộ nhớ. Giảm một nửa độ chính xác nghĩa là giảm một nửa yêu cầu bộ nhớ.

  • Tăng thông lượng: độ chính xác thấp hơn nghĩa là nhiều phép tính hơn mỗi giây. Các NVIDIA Tensor Core (chương 16) đạt thông lượng gấp 2 lần với FP16 so với FP32, gấp 2 lần tiếp theo với INT8 so với FP16, và gấp 2 lần nữa với INT4 so với INT8. Một GPU H100 thực hiện 989 TFLOPS ở FP8 so với 67 TFLOPS ở FP32 — chênh lệch 15 lần.

  • Tiết kiệm băng thông: suy luận LLM thường bị giới hạn bởi băng thông bộ nhớ (memory-bandwidth-bound, chương 16, mô hình roofline). Nút thắt cổ chai nằm ở việc nạp trọng số từ bộ nhớ GPU, chứ không phải ở việc tính toán với chúng. Trọng số nhỏ hơn nghĩa là ít byte hơn cần truyền tải, trực tiếp tăng số token mỗi giây. Đây là lý do lượng tử hóa thường mang lại tốc độ tăng gần tuyến tính cho suy luận LLM.

  • Tiết kiệm năng lượng: độ chính xác thấp hơn tiêu thụ ít năng lượng hơn trên mỗi phép tính. Ở quy mô trung tâm dữ liệu (hàng ngàn GPU), điều này chuyển thành sự giảm đáng kể chi phí điện năng.

Các định dạng số

  • Chúng ta đã tìm hiểu dấu phẩy động IEEE 754 trong chương 13 (kiến trúc máy tính). Dưới đây là toàn cảnh về độ chính xác trong ML:

Precision formats bit layout: FP32 through ternary, showing how sign, exponent, and mantissa bits are arranged in memory, with memory-per-parameter comparison

Định dạng Số bit Số mũ (Exponent) Phần định trị (Mantissa) Miền giá trị Trường hợp sử dụng
FP32 32 8 23 ±3.4×10³⁸ Huấn luyện (tiêu chuẩn vàng)
TF32 19 8 10 ±3.4×10³⁸ Huấn luyện Tensor Core (A100+)
FP16 16 5 10 ±65504 Huấn luyện độ chính xác hỗn hợp
BF16 16 8 7 ±3.4×10³⁸ Huấn luyện (cùng miền giá trị với FP32)
FP8 E4M3 8 4 3 ±448 Lan truyền thuận (Hopper+)
FP8 E5M2 8 5 2 ±57344 Gradient (miền giá trị rộng hơn)
INT8 8 -128 đến 127 Suy luận PTQ
INT4 4 -8 đến 7 Lượng tử hóa chỉ trọng số
INT2/Ternary 2 {-1, 0, 1} Nén cực độ
  • FP8 có hai biến thể: E4M3 (số mũ 4 bit, phần định trị 3 bit, miền giá trị hẹp hơn nhưng độ chính xác cao hơn) dùng cho lan truyền thuận, và E5M2 (số mũ 5 bit, phần định trị 2 bit, miền giá trị rộng hơn nhưng độ chính xác thấp hơn) dùng cho gradient. Transformer Engine (chương 16) tự động chuyển đổi giữa hai loại này cho từng tensor.

  • BF16 so với FP16: BF16 có cùng miền giá trị số mũ với FP32 (không rủi ro tràn số) nhưng phần định trị kém chính xác hơn. FP16 chính xác hơn nhưng miền giá trị hẹp (tối đa 65504), đòi hỏi phải dùng loss scaling trong lúc huấn luyện. Với suy luận, cả hai đều hoạt động tốt; với huấn luyện, BF16 an toàn hơn.

  • Các định dạng số nguyên không có số mũ — chúng biểu diễn giá trị dấu phẩy cố định (fixed-point). Để chuyển đổi giữa float và int, ta cần một hệ số tỷ lệ (scale factor) và tùy chọn một điểm zero (zero point): \(x_{\text{float}} = \text{scale} \times (x_{\text{int}} - \text{zero\_point})\).

Phương trình lượng tử hóa

  • Mọi phương pháp lượng tử hóa đều ánh xạ các giá trị dấu phẩy động sang số nguyên và ngược lại:
\[x_q = \text{clamp}\left(\text{round}\left(\frac{x}{\text{scale}}\right) + \text{zero\_point}, \; q_{\min}, \; q_{\max}\right)\]
\[\hat{x} = \text{scale} \times (x_q - \text{zero\_point})\]
  • Hệ số tỷ lệ (scale) xác định độ phân giải: \(\text{scale} = \frac{x_{\max} - x_{\min}}{q_{\max} - q_{\min}}\). Với INT8: \(q_{\min} = -128\), \(q_{\max} = 127\).

  • Lượng tử hóa đối xứng (symmetric) đặt \(\text{zero\_point} = 0\), nên \(\text{scale} = \frac{\max(|x|)}{127}\). Đơn giản và nhanh hơn (không cần trừ zero-point khi suy luận).

  • Lượng tử hóa không đối xứng (asymmetric) dùng một \(\text{zero\_point}\) khác không để xử lý các phân bố không đối xứng (ví dụ, các output của ReLU đều không âm). Nó ánh xạ \([x_{\min}, x_{\max}]\) sang \([0, 255]\) cho INT8 không dấu.

Quantisation granularity: per-tensor uses one scale for the entire matrix, per-channel one per column, per-group one per small block

  • Độ hạt lượng tử hóa (quantisation granularity): bao nhiêu giá trị chia sẻ cùng một hệ số tỷ lệ:
    • Per-tensor: một hệ số tỷ lệ cho toàn bộ tensor. Đơn giản nhất nhưng độ chính xác thấp nhất (một giá trị ngoại lai làm sai lệch toàn bộ tỷ lệ của tensor).
    • Per-channel: một hệ số tỷ lệ cho mỗi kênh output (với tích chập) hoặc mỗi hàng (với các lớp tuyến tính). Độ chính xác tốt hơn nhiều với chi phí tối thiểu.
    • Per-group: một hệ số tỷ lệ cho mỗi nhóm \(g\) phần tử (ví dụ \(g = 128\)). Độ chính xác tốt nhất, được dùng trong các phương pháp lượng tử hóa chỉ trọng số hiện đại (GPTQ, AWQ).
    • Per-token: một hệ số tỷ lệ cho mỗi token của activation. Xử lý được việc các token khác nhau có biên độ activation rất khác nhau.

Lượng tử hóa hậu huấn luyện (PTQ)

  • PTQ lượng tử hóa một mô hình đã được huấn luyện mà không cần huấn luyện lại. Ta đưa một tập hiệu chuẩn (calibration set, một tập dữ liệu đại diện nhỏ, thường 128-512 mẫu) qua mô hình để thu thập thống kê activation, sau đó tính toán các hệ số tỷ lệ tối ưu.

Các phương pháp hiệu chuẩn

  • Min-max: đặt tỷ lệ dựa trên giá trị nhỏ nhất và lớn nhất quan sát được. Đơn giản nhưng nhạy với các giá trị ngoại lai (một giá trị cực đoan làm lãng phí phần lớn dải lượng tử hóa cho các giá trị ít khi dùng).

  • Phần trăm (percentile): dùng mức phần trăm 99.99 thay vì giá trị lớn nhất tuyệt đối. Cắt bớt các ngoại lai cực đoan, cho độ phân giải tốt hơn cho đa số các giá trị. Các giá trị bị cắt sẽ bão hòa về \(q_{\min}\) hoặc \(q_{\max}\).

  • Tối ưu theo MSE: tìm tỷ lệ sao cho cực tiểu hóa sai số bình phương trung bình (mean squared error) giữa tensor gốc và tensor đã lượng tử hóa. Đây là một bài toán tối ưu 1D (tìm kiếm trên các giá trị cắt có thể) và thường cho độ chính xác PTQ tốt nhất.

  • Dựa trên entropy (phân kỳ KL): tìm tỷ lệ sao cho cực tiểu hóa phân kỳ KL giữa phân bố giá trị gốc và phân bố giá trị đã lượng tử hóa. Được dùng trong hiệu chuẩn INT8 của TensorRT.

PTQ trong thực tế

# Simplified PTQ with PyTorch (conceptual)
import torch

def quantise_tensor_symmetric(tensor, bits=8):
    qmax = 2 ** (bits - 1) - 1  # 127 for INT8
    scale = tensor.abs().max() / qmax
    quantised = torch.clamp(torch.round(tensor / scale), -qmax, qmax).to(torch.int8)
    return quantised, scale

def dequantise(quantised, scale):
    return quantised.float() * scale

# Quantise a weight matrix
weight = torch.randn(512, 512)  # pretrained weight
weight_q, scale = quantise_tensor_symmetric(weight, bits=8)
weight_reconstructed = dequantise(weight_q, scale)

# Quantisation error
error = (weight - weight_reconstructed).abs().mean()
print(f"Mean absolute error: {error:.6f}")
print(f"Compression: {weight.numel() * 4 / (weight_q.numel() * 1 + 4):.1f}x")  # +4 bytes for scale
  • PTQ hoạt động tốt cho INT8 trên hầu hết các mô hình với độ suy giảm độ chính xác dưới 1%. Với INT4, chất lượng PTQ giảm đáng kể — các phương pháp chỉ trọng số (dưới đây) xử lý INT4 tốt hơn nhiều.

Lượng tử hóa có nhận thức trong huấn luyện (QAT)

  • QAT chèn các phép toán lượng tử hóa giả (fake quantisation) vào đồ thị huấn luyện: trọng số và activation được lượng tử hóa và giải lượng tử hóa trong lượt lan truyền thuận, nhưng gradient vẫn chảy qua như thể không có lượng tử hóa (dùng bộ ước lượng xuyên qua — straight-through estimator).
\[\text{Forward: } \hat{W} = \text{dequant}(\text{quant}(W))\]
\[\text{Backward: } \frac{\partial L}{\partial W} \approx \frac{\partial L}{\partial \hat{W}}\]
  • Mô hình học cách chịu được nhiễu lượng tử hóa trong quá trình huấn luyện. QAT thường phục hồi phần lớn hoặc toàn bộ độ chính xác bị mất bởi PTQ, đặc biệt ở độ rộng bit thấp (INT4, INT2).

  • Chi phí: QAT đòi hỏi huấn luyện lại (hoặc tinh chỉnh) mô hình, rất tốn kém với các mô hình lớn. Với mô hình 70B, QAT có thể tốn \(10,000-\)100,000 chi phí tính toán. PTQ hầu như không tốn gì (chỉ cần hiệu chuẩn).

  • Khi nào dùng QAT: khi chất lượng PTQ không thể chấp nhận được (thường là INT4 hoặc thấp hơn), khi ta triển khai lên thiết bị biên với ngân sách độ trễ nghiêm ngặt, hoặc khi mô hình sẽ được lượng tử hóa hàng triệu lần (chi phí QAT một lần được phân bổ đều).

Lượng tử hóa chỉ trọng số

  • Với suy luận LLM, nút thắt cổ chai nằm ở việc nạp trọng số từ bộ nhớ, chứ không phải tính toán với chúng (chế độ bị giới hạn bởi băng thông bộ nhớ). Lượng tử hóa chỉ trọng số (weight-only quantisation) lượng tử hóa trọng số xuống INT4 hoặc INT3 trong khi giữ activation ở FP16. Phép tính được thực hiện ở FP16 (sau khi giải lượng tử hóa trọng số ngay lập tức), nhưng dung lượng và băng thông bộ nhớ giảm 4-8 lần.

GPTQ

  • GPTQ (Frantar et al., 2022) lượng tử hóa trọng số từng cột một, bù đắp lỗi của mỗi cột bằng cách điều chỉnh các cột phía sau. Nó dùng ma trận Hessian (thông tin bậc hai từ một tập hiệu chuẩn) để xác định thứ tự lượng tử hóa và bù lỗi tối ưu:
\[\hat{W}_{:,j} = \text{quant}(W_{:,j}), \quad W_{:,j+1:} \mathrel{-}= \frac{(\hat{W}_{:,j} - W_{:,j}) \cdot H_{j,j+1:}}{H_{j,j}}\]
  • Ý tưởng then chốt: lượng tử hóa cột \(j\) sinh ra một lỗi. GPTQ bù đắp ngay lập tức bằng cách điều chỉnh tất cả các cột còn lại sao cho output tổng thể của lớp (\(XW\)) thay đổi ít nhất có thể. Đây là lượng tử hóa não bộ tối ưu (optimal brain quantisation, OBQ) được áp dụng cho transformer.

  • GPTQ với lượng tử hóa theo nhóm 4-bit (kích thước nhóm 128) đạt độ suy giảm perplexity dưới 1% trên hầu hết các LLM. Việc lượng tử hóa mất ~1 giờ cho mỗi mô hình 70B trên một GPU.

AWQ

  • AWQ (Activation-Aware Weight Quantisation, Lin et al., 2023) quan sát thấy một phần nhỏ các kênh trọng số (1-3%) quan trọng hơn hẳn phần còn lại — chúng tương ứng với các kênh activation có biên độ lớn. Bảo vệ các kênh nổi trội này giúp giảm đáng kể lỗi lượng tử hóa.

  • AWQ nhân các kênh quan trọng này với một hệ số \(s\) trước khi lượng tử hóa (làm chúng lớn hơn và do đó ít bị ảnh hưởng bởi làm tròn) và nhân các activation tương ứng với \(1/s\) (để bảo toàn output). Hệ số \(s\) được tối ưu cho mỗi nhóm nhằm cực tiểu hóa lỗi lượng tử hóa tổng thể.

  • AWQ đơn giản hơn GPTQ (không tính toán Hessian), chạy nhanh hơn và đạt chất lượng tương đương. Nó đã trở thành lựa chọn mặc định cho nhiều pipeline lượng tử hóa LLM mã nguồn mở.

Lượng tử hóa GGUF / llama.cpp

  • GGUF (GGML Universal Format) là định dạng được llama.cpp dùng cho suy luận CPU. Nó hỗ trợ nhiều phương án lượng tử hóa:

    • Q4_0: 4-bit, khối 32 phần tử, đối xứng.
    • Q4_K_M: 4-bit với các kênh quan trọng độ chính xác hỗn hợp (k-quants).
    • Q5_K_M: 5-bit với k-quants (chất lượng cao hơn).
    • Q8_0: 8-bit, đơn giản và nhanh.
  • Các biến thể "K" (k-quants) phân bổ nhiều bit hơn cho các khối trọng số quan trọng, tương tự như insight của AWQ nhưng được hiện thực ở mức định dạng. Q4_K_M là điểm cân bằng ngọt nhất cho hầu hết các mô hình: trung bình 4-bit với tổn thất chất lượng tối thiểu.

QuIP và QuIP

  • QuIP (Chee et al., 2023) giới thiệu xử lý không mạch (incoherence processing): xoay ma trận trọng số bằng một phép biến đổi trực giao ngẫu nhiên trước khi lượng tử hóa. Điều này trải đều thông tin qua tất cả các trọng số, ngăn một vài trọng số ngoại lai chi phối lỗi lượng tử hóa.

  • Trực giác là: nếu một trọng số là 100 và phần còn lại là ~1, việc lượng tử hóa tất cả với cùng một tỷ lệ sẽ lãng phí phần lớn dải INT4 cho giá trị ngoại lai. Sau một phép xoay trực giao (bảo toàn các tính chất toán học của ma trận), mọi trọng số đều có độ lớn tương tự, và lượng tử hóa đều (uniform) hoạt động tốt hơn nhiều.

  • QuIP# mở rộng ý tưởng này với codebook mạng tinh thể (lattice codebooks): thay vì ánh xạ vào một lưới số nguyên đều, nó ánh xạ vào các điểm trong một lattice tối ưu (lattice E8 ở 8 chiều). Các mã lattice đóng gói nhiều điểm lượng tử hóa hơn vào cùng số bit, đạt tỷ lệ méo-rate tốt hơn lượng tử hóa đều. QuIP# đạt chất lượng khả dụng ở độ chính xác 2-bit — bằng một nửa số bit so với các phương pháp INT4 điển hình.

SpQR

  • SpQR (Dettmers et al., 2023) quan sát thấy một phần cực nhỏ các trọng số (0.1-1%) là những ngoại lai (outliers) đóng góp không cân xứng vào chất lượng output. Thay vì lượng tử hóa tất cả cùng một độ chính xác, SpQR:

    1. Xác định các trọng số ngoại lai bằng phân tích độ nhạy (việc lượng tử hóa trọng số này thay đổi output lớp bao nhiêu).
    2. Lưu các ngoại lai ở độ chính xác đầy đủ (FP16) trong một định dạng thưa (sparse).
    3. Lượng tử hóa toàn bộ trọng số còn lại xuống INT3 hoặc INT4.
  • Kết quả: ~99% trọng số được lượng tử hóa mạnh (nhỏ), trong khi 1% trọng số then chốt giữ nguyên độ chính xác đầy đủ. Việc lưu ngoại lai thưa thêm chi phí tối thiểu (dưới 5% tổng kích thước).

HQQ

  • HQQ (Half-Quadratic Quantisation, Badri & Shaji, 2023) là một phương pháp lượng tử hóa trọng số zero-shot không cần bất kỳ dữ liệu hiệu chuẩn nào. Nó mô hình hóa lượng tử hóa như một bài toán tối ưu hóa bậc hai nửa (half-quadratic), giải lần lượt cho các trọng số và hệ số tỷ lệ lượng tử hóa tối ưu.

  • Ưu điểm: không cần tập hiệu chuẩn nghĩa là không phụ thuộc dữ liệu, lượng tử hóa tức thì và không rủi ro sai lệch dữ liệu hiệu chuẩn. HQQ đặc biệt hữu ích cho các mô hình mà dữ liệu hiệu chuẩn đại diện không có sẵn hoặc nhạy cảm.

AQLM

  • AQLM (Egiazarian et al., 2024) áp dụng lượng tử hóa cộng (additive quantisation, lượng tử hóa vector đa-codebook) cho LLM. Thay vì lượng tử hóa từng trọng số độc lập, AQLM nhóm các trọng số thành các vector và biểu diễn mỗi vector như tổng của các phần tử từ nhiều codebook đã học:
\[\mathbf{w} \approx \mathbf{c}_1^{(1)} + \mathbf{c}_2^{(2)} + \cdots + \mathbf{c}_M^{(M)}\]
  • trong đó \(\mathbf{c}_i^{(m)}\) là một phần tử từ codebook \(m\). Với \(M = 2\) codebook, mỗi codebook 256 phần tử, một vector 8 phần tử được mã hóa thành hai chỉ số 8-bit = 2 byte cho 8 trọng số = 2 bit mỗi trọng số hiệu dụng. AQLM đạt chất lượng hàng đầu ở độ chính xác 2-bit, vượt trội so với GPTQ và AWQ ở mức nén cực độ này.

BitNet và các LLM 1-bit

  • BitNet (Wang et al., 2023) đẩy lượng tử hóa tới cực hạn: trọng số là ba ngôi (\(\{-1, 0, +1\}\)), chỉ cần ~1.58 bit mỗi trọng số. Phép nhân ma trận trở thành chỉ cộng và trừ — không cần phép nhân dấu phẩy động.

  • BitNet b1.58 (Ma et al., 2024) ràng buộc mọi trọng số vào \(\{-1, 0, +1\}\). "1.58 bit" đến từ \(\log_2(3) \approx 1.58\). Ở độ chính xác này, một mô hình 70B nằm gọn trong ~15 GB và suy luận không cần phép nhân — chỉ cộng, trừ và đảo dấu.

  • Phép matmul trở thành:

\[y_j = \sum_i W_{ij} \cdot x_i = \sum_{i: W_{ij}=+1} x_i - \sum_{i: W_{ij}=-1} x_i\]
  • Điều này rẻ hơn rất nhiều so với matmul FP16 trên mọi phần cứng, và có thể cho phép suy luận LLM trên các thiết bị không có đơn vị dấu phẩy động. Sự đánh đổi về chất lượng là đáng kể với các mô hình hiện tại nhưng cải thiện theo quy mô và theo mức độ nhận thức lượng tử hóa khi huấn luyện.

Các định dạng Microscaling (MX)

  • Các định dạng Microscaling (MX) là một tiêu chuẩn ngành mới (được AMD, Arm, Intel, Meta, Microsoft, NVIDIA, Qualcomm hỗ trợ) sử dụng block floating point: một nhóm các phần tử chia sẻ một số mũ duy nhất, và mỗi phần tử có phần định trị riêng.
Định dạng Số mũ chia sẻ Bit mỗi phần tử Tổng (mỗi phần tử) Tương đương
MXFP8 8-bit mỗi khối 8 (E4M3/E5M2) ~8 Như FP8 với miền giá trị tốt hơn
MXFP6 8-bit mỗi khối 6 ~6.5 Nằm giữa FP8 và INT4
MXFP4 8-bit mỗi khối 4 ~4.5 Như INT4 với hành vi giống float
MXINT8 8-bit mỗi khối 8 (số nguyên) ~8.5 INT8 với chia sẻ tỷ lệ
  • Số mũ chia sẻ phân bổ chi phí số mũ trên toàn khối (thường 16-32 phần tử). Mỗi phần tử giữ lại nhiều bit phần định trị hơn so với khi có số mũ riêng, cho độ chính xác tốt hơn trên mỗi bit. Các định dạng MX được kỳ vọng sẽ thay thế các định dạng FP8 và INT8 đơn lẻ trong phần cứng tương lai.

Huấn luyện FP8

  • Huấn luyện ở FP8 (không chỉ suy luận) nay đã khả thi trên các GPU NVIDIA Hopper và Blackwell. Công thức:

    • Lan truyền thuận: trọng số và activation ở E4M3 (độ chính xác cao hơn, miền giá trị hẹp hơn). Transformer Engine tính toán động các hệ số tỷ lệ per-tensor bằng delayed scaling (theo dõi thống kê từ vòng lặp trước, áp dụng cho vòng lặp hiện tại).
    • Lan truyền ngược: gradient ở E5M2 (miền giá trị rộng hơn, độ chính xác thấp hơn). Gradient có miền giá trị rộng hơn trọng số/activation, nên bit số mũ thừa ngăn tràn số.
    • Trọng số chính (master weights): được giữ ở FP32 cho trạng thái bộ tối ưu (như huấn luyện độ chính xác hỗn hợp chuẩn với FP16, chương 6). Tính toán FP8 chỉ dành cho các phép matmul, không phải cho việc cập nhật trọng số.
    • Loss scaling: vẫn cần cho FP8, giống như với FP16. Bộ scaler loss động điều chỉnh hệ số tỷ lệ để giữ các giá trị gradient trong miền biểu diễn của FP8.
  • Huấn luyện FP8 đạt chất lượng tương đương huấn luyện BF16 cho hầu hết kích thước mô hình, với cải thiện thông lượng ~2 lần. Nó là mặc định cho các đợt huấn luyện quy mô lớn mới trên các cụm H100.

Lượng tử hóa Activation

  • Activation (các tensor trung gian chảy giữa các lớp) cũng có thể được lượng tử hóa, cho phép tính toán hoàn toàn INT8 (cả trọng số và activation ở INT8, với cộng dồn INT32).

  • Lượng tử hóa động (dynamic): tính hệ số tỷ lệ tại thời điểm chạy từ các giá trị activation thực tế. Chính xác hơn (thích nghi với từng input) nhưng thêm chi phí (tính min/max hoặc percentile ở mỗi lớp).

  • Lượng tử hóa tĩnh (static): tính hệ số tỷ lệ một lần trong lúc hiệu chuẩn và cố định chúng. Nhanh hơn khi suy luận (không có thống kê thời gian chạy) nhưng kém chính xác hơn nếu dữ liệu hiệu chuẩn không đại diện.

  • Lượng tử hóa per-token: tính một hệ số tỷ lệ riêng cho mỗi token trong chuỗi. Quan trọng với LLM vì các token khác nhau có thể có biên độ activation rất khác nhau (một số token sinh ra activation lớn gấp 100 lần token khác).

  • Lượng tử hóa activation khó hơn lượng tử hóa trọng số vì activation phụ thuộc vào dữ liệu (thay đổi theo mỗi input), trong khi trọng số thì cố định. Vấn đề "ngoại lai" đặc biệt nghiêm trọng: một vài kênh activation có giá trị cực đoan (gấp 100 lần trung bình), và lượng tử hóa chúng cùng tỷ lệ với các kênh bình thường sẽ lãng phí độ chính xác.

  • SmoothQuant (Xiao et al., 2022) giải quyết ngoại lai bằng cách di chuyển toán học độ khó lượng tử hóa từ activation (khó lượng tử hóa do ngoại lai) sang trọng số (dễ lượng tử hóa): nhân activation với \(1/s\) và trọng số với \(s\), trong đó \(s\) cân bằng độ khó. Output \(XW = (X \cdot \text{diag}(s^{-1})) \cdot (\text{diag}(s) \cdot W)\) không đổi.

Lượng tử hóa độ chính xác hỗn hợp

  • Không phải mọi lớp đều nhạy cảm với lượng tử hóa như nhau. Các lớp attention thường chịu được INT4, trong khi các lớp embedding và bộ phân loại cuối cần độ chính xác cao hơn.

  • Phân tích độ nhạy (sensitivity analysis): lượng tử hóa từng lớp riêng biệt và đo lường tác động đến độ chính xác. Các lớp nhạy cao nhận nhiều bit hơn; các lớp không nhạy nhận ít bit hơn.

  • Transformer Engine (chương 16, NVIDIA Hopper) hiện thực độ chính xác hỗn hợp động ở mức phép toán: mỗi phép matmul chọn giữa FP8 và FP16 dựa trên thống kê tensor, cực đại hóa thông lượng trong khi vẫn duy trì chất lượng.

Lượng tử hóa cache KV

  • Trong quá trình sinh của LLM, cache KV lưu các tensor key và value cho tất cả các token trước đó. Với các chuỗi dài, nó chiếm phần lớn bộ nhớ:
\[\text{KV-cache size} = 2 \times n_{\text{layers}} \times n_{\text{heads}} \times d_{\text{head}} \times \text{seq\_len} \times \text{bytes\_per\_element}\]
  • Một mô hình 70B với 80 lớp, 64 head, head 128 chiều, ở độ dài chuỗi 128K trong FP16: \(2 \times 80 \times 64 \times 128 \times 131072 \times 2 = 330\) GB. Lớn hơn bộ nhớ GPU.

  • Lượng tử hóa cache KV giảm điều này bằng cách lưu các key và value đã cache ở INT8 hoặc INT4 thay vì FP16. Lỗi lượng tử hóa tích lũy dọc theo chuỗi (mỗi token mới attend vào tất cả K/V đã cache), nhưng với lượng tử hóa per-channel hoặc per-head, sự suy giảm là có thể chấp nhận được.

  • Lượng tử hóa cache KV có lợi theo kiểu nhân (multiplicatively beneficial): nó cho phép các chuỗi dài hơn (nhiều ngữ cảnh hơn), kích thước batch lớn hơn (nhiều người dùng đồng thời hơn), và suy luận nhanh hơn (ít băng thông bộ nhớ hơn để nạp cache). Đây là một trong những tối ưu hóa có tác động lớn nhất cho việc phục vụ LLM.

Bài tập lập trình (dùng CoLab hoặc notebook)

  1. Hiện thực lượng tử hóa INT8 đối xứng từ đầu. Lượng tử hóa một ma trận trọng số, giải lượng tử hóa nó, và đo lường sai số tái tạo theo phân bố giá trị.
import jax.numpy as jnp
import jax

def quantise_int8(tensor):
    scale = jnp.max(jnp.abs(tensor)) / 127.0
    quantised = jnp.clip(jnp.round(tensor / scale), -127, 127).astype(jnp.int8)
    return quantised, scale

def dequantise(quantised, scale):
    return quantised.astype(jnp.float32) * scale

# Normal weights (typical for trained models)
key = jax.random.PRNGKey(0)
weights = jax.random.normal(key, (1024, 1024)) * 0.02

q, s = quantise_int8(weights)
recon = dequantise(q, s)

print(f"Original:     {weights.nbytes / 1024:.0f} KB")
print(f"Quantised:    {q.nbytes / 1024:.0f} KB ({weights.nbytes / q.nbytes:.0f}x smaller)")
print(f"Mean abs err: {jnp.abs(weights - recon).mean():.6f}")
print(f"Max abs err:  {jnp.abs(weights - recon).max():.6f}")
print(f"Relative err: {jnp.abs(weights - recon).mean() / jnp.abs(weights).mean():.4%}")
  1. Minh họa vấn đề ngoại lai. Tạo các activation với một vài kênh cực đoan và cho thấy lượng tử hóa per-tensor thất bại trong khi per-channel thành công.
import jax.numpy as jnp
import jax

key = jax.random.PRNGKey(42)

# Activations: most channels are normal, 2 channels have 100x outliers
activations = jax.random.normal(key, (32, 512)) * 0.1
activations = activations.at[:, 0].set(activations[:, 0] * 100)   # outlier channel
activations = activations.at[:, 1].set(activations[:, 1] * 50)    # outlier channel

# Per-tensor quantisation (one scale for entire tensor)
scale_tensor = jnp.max(jnp.abs(activations)) / 127.0
q_tensor = jnp.clip(jnp.round(activations / scale_tensor), -127, 127)
recon_tensor = q_tensor * scale_tensor

# Per-channel quantisation (one scale per channel)
scales_channel = jnp.max(jnp.abs(activations), axis=0) / 127.0
q_channel = jnp.clip(jnp.round(activations / scales_channel), -127, 127)
recon_channel = q_channel * scales_channel

err_tensor = jnp.abs(activations - recon_tensor).mean()
err_channel = jnp.abs(activations - recon_channel).mean()

print(f"Per-tensor error: {err_tensor:.6f}")
print(f"Per-channel error: {err_channel:.6f}")
print(f"Per-channel is {err_tensor / err_channel:.1f}x better")
print(f"\nOutlier channels waste {(activations.shape[1] - 2) / activations.shape[1]:.0%} "
      f"of the quantisation range for {2 / activations.shape[1]:.1%} of channels")
  1. Tính bộ nhớ cache KV cho các kích thước mô hình và độ dài chuỗi khác nhau. Cho thấy tại sao lượng tử hóa cache KV là thiết yếu cho các mô hình ngữ cảnh dài.
def kv_cache_gb(n_layers, n_heads, d_head, seq_len, bytes_per_elem):
    return 2 * n_layers * n_heads * d_head * seq_len * bytes_per_elem / 1e9

models = [
    ("Llama-7B",  32, 32, 128),
    ("Llama-70B", 80, 64, 128),
    ("GPT-4 (est)", 120, 96, 128),
]

print(f"{'Model':<15} {'SeqLen':>8} {'FP16 (GB)':>10} {'INT8 (GB)':>10} {'INT4 (GB)':>10}")
print("-" * 60)

for name, layers, heads, d_head in models:
    for seq_len in [4096, 32768, 131072]:
        fp16 = kv_cache_gb(layers, heads, d_head, seq_len, 2)
        int8 = kv_cache_gb(layers, heads, d_head, seq_len, 1)
        int4 = kv_cache_gb(layers, heads, d_head, seq_len, 0.5)
        print(f"{name:<15} {seq_len:>8} {fp16:>9.1f}  {int8:>9.1f}  {int4:>9.1f}")
    print()