Quantizationgiáo trình tự học
Chương 149 bài · 48 câu trắc nghiệm

14. Bài tập#

Chín bài tập của giáo trình kèm lời giải chi tiết, bộ câu hỏi trắc nghiệm theo chương và các bài luyện tính tay.

Bài 1
tính tayCơ bảnChương 3
#

Lượng tử dải [−0,5; 2,5][-0{,}5;\ 2{,}5] sang uint8. Tính SS, ZZ, rồi lượng tử và giải lượng tử các giá trị −0,5-0{,}5; 00; 11; 33. Giá trị nào chịu sai số cắt?

Gợi ý của đề
Gợi ý: S=3/255S = 3/255, Z=⌊42,5⌉Z = \lfloor 42{,}5 \rceil. Chú ý quy tắc làm tròn nửa về số chẵn của NumPy.
Xem lời giải chi tiết

Bước 1 — kiểm tra dải. Dải [α,β]=[−0,5; 2,5][\alpha, \beta] = [-0{,}5;\ 2{,}5] đã chứa số 0 nên không phải mở rộng (Mục 3.4).

Bước 2 — tính SS.

S=β−αqmax−qmin=2,5−(−0,5)255−0=3255=185≈0,0117647.S = \frac{\beta - \alpha}{q_{max} - q_{min}} = \frac{2{,}5 - (-0{,}5)}{255 - 0} = \frac{3}{255} = \frac{1}{85} \approx 0{,}0117647.

Con số này rất dễ chịu: 1/S=851/S = 85 đúng bằng số nguyên, nên mọi phép chia phía dưới đều tròn.

Bước 3 — tính ZZ.

Z=⌊qmin−αS⌉=⌊0−(−0,5)⋅85⌉=⌊42,5⌉=42.Z = \left\lfloor q_{min} - \frac{\alpha}{S} \right\rceil = \left\lfloor 0 - (-0{,}5) \cdot 85 \right\rceil = \lfloor 42{,}5 \rceil = 42.

Đây chính là chỗ gợi ý của đề nhắc: 42,542{,}5 rơi đúng vào điểm giữa. NumPy (và cả round() của Python) dùng quy tắc làm tròn nửa về số chẵn, nên cho 42 chứ không phải 43.

Hệ quả quan trọng. Vì ZZ bị làm tròn xuống, dải thực sự biểu diễn được không còn là [−0,5; 2,5][-0{,}5;\ 2{,}5] mà là

[ S(0−42), S(255−42) ]=[−4285, 21385]=[−0,494118; 2,505882],\begin{aligned} [\,S(0 - 42),\ S(255 - 42)\,] &= \left[-\tfrac{42}{85},\ \tfrac{213}{85}\right] \\ &= [-0{,}494118;\ 2{,}505882], \end{aligned}

tức toàn bộ lưới bị đẩy sang phải S/2S/2. Đúng hiện tượng đã mô tả ở Mục 3.3 với Hình 4.

Bước 4 — lượng tử và giải lượng tử.

xx x/Sx/S q=clamp(⌊x/S⌉+Z)q = \text{clamp}(\lfloor x/S \rceil + Z) x^=S(q−Z)\hat{x} = S(q - Z) Sai số x^−x\hat{x} - x Loại sai số
−0,5-0{,}5 −42,5-42{,}5 −42+42=0-42 + 42 = 0 −0,494118-0{,}494118 +0,005882+0{,}005882 làm tròn, đúng bằng S/2S/2
00 00 0+42=42=Z0 + 42 = 42 = Z 00 00 không có
11 8585 85+42=12785 + 42 = 127 11 00 không có
33 255255 255+42=297→255255 + 42 = 297 \to \mathbf{255} 2,5058822{,}505882 −0,494118-0{,}494118 cắt

Trả lời câu hỏi của đề. Chỉ x=3x = 3 chịu sai số cắt: phép clamp thực sự can thiệp (297 bị kéo về 255) và sai số 0,49410{,}4941 lớn gấp 84 lần sai số làm tròn tối đa.

Hai giá trị đáng chú ý khác:

  • x=0x = 0 và x=1x = 1 không sai số chút nào. Với x=0x = 0 là do thiết kế (q=Zq = Z, Mục 3.4); với x=1x = 1 là may mắn số học: 1/S=851/S = 85 nguyên nên 1 nằm đúng trên lưới.
  • x=−0,5x = -0{,}5 không bị clamp (vì q=0q = 0 vẫn nằm trong [0,255][0, 255]) nhưng sai số của nó đúng bằng S/2S/2 — mức làm tròn lớn nhất có thể. Nó nằm ngay ngoài dải biểu diễn được, đúng ở ranh giới giữa hai loại sai số.

Biến thể đáng thử. Nếu thư viện của bạn làm tròn nửa ra xa số 0 thì Z=43Z = 43, dải biểu diễn thành [−0,505882; 2,494118][-0{,}505882;\ 2{,}494118], và khi đó β=2,5\beta = 2{,}5 mới là giá trị bị cắt còn α=−0,5\alpha = -0{,}5 thì không. Quy ước làm tròn không phải chi tiết vụn vặt.

Python
import numpy as np

S = 3 / 255
Z = int(np.round(0 - (-0.5) / S))          # 42
x = np.array([-0.5, 0.0, 1.0, 3.0])
q = np.clip(np.round(x / S) + Z, 0, 255).astype(np.int32)
xh = S * (q - Z)
print(S, Z); print(q); print(xh); print(xh - x)
Bài 2
FP16Cơ bảnChương 2
#

Hãy giải thích vì sao np.float16(2049.0) bằng 2048, và tìm số nguyên dương nhỏ nhất không biểu diễn chính xác được bằng FP16.

Gợi ý của đề
Gợi ý: FP16 có 11 bit độ chính xác kể cả bit ẩn. Đáp số: 2049.
Xem lời giải chi tiết

Phần a — vì sao np.float16(2049.0) bằng 2048.

FP16 có 10 bit mantissa được lưu, cộng bit ẩn là p=11p = 11 bit độ chính xác (Mục 2.2). Với một số nằm trong [2e, 2e+1)[2^e,\ 2^{e+1}), hai số FP16 liền kề cách nhau

Δ=2 e−10.\Delta = 2^{\,e - 10}.

Số 2049 nằm trong [211,212)=[2048,4096)[2^{11}, 2^{12}) = [2048, 4096), nên e=11e = 11 và Δ=21=2\Delta = 2^{1} = 2. Hai số biểu diễn được kề nó là 2048 và 2050, còn 2049 rơi đúng điểm giữa.

IEEE 754 mặc định làm tròn về số gần nhất, hoà thì chọn mantissa chẵn:

  • 2048=1,00000000002×2112048 = 1{,}0000000000_2 \times 2^{11} — bit cuối mantissa là 0 (chẵn),
  • 2050=1,00000000012×2112050 = 1{,}0000000001_2 \times 2^{11} — bit cuối mantissa là 1 (lẻ).

Nên kết quả là 2048. Đây cùng một quy tắc đã khiến Z=⌊42,5⌉=42Z = \lfloor 42{,}5 \rceil = 42 ở Bài 1, và khiến np.float16(1993.5) = 1994 ở Mục 2.3.

Phần b — số nguyên dương nhỏ nhất không biểu diễn chính xác được.

Một số nguyên nn biểu diễn được chính xác khi và chỉ khi nó viết được dạng n=m⋅2kn = m \cdot 2^k với mm nguyên và ∣m∣<2p|m| < 2^{p}. Nói cách khác: nn cần không quá p=11p = 11 chữ số có nghĩa trong hệ nhị phân.

  • Mọi n≤2048=211n \le 2048 = 2^{11} đều được: 2048=12×2112048 = 1_2 \times 2^{11} chỉ cần 1 chữ số có nghĩa.
  • 2049=211+1=10000000000122049 = 2^{11} + 1 = 100000000001_2 cần 12 chữ số có nghĩa, và không rút gọn được vì nó lẻ.

Vậy đáp số là 2049\boxed{2049}, đúng như gợi ý của đề.

Tổng quát hoá. Với định dạng có pp bit độ chính xác, số nguyên dương nhỏ nhất không biểu diễn chính xác được luôn là 2p+12^{p} + 1:

Kiểu Mantissa lưu pp 2p+12^p + 1
FP32 23 24 16 777 21716\,777\,217
FP16 10 11 20492049
BF16 7 8 257257
FP8 E4M3 3 4 1717

Bảng này giải thích một sự cố rất hay gặp trong thực tế: BF16 không đếm nổi quá 256. Bộ đếm bước huấn luyện, chỉ số token hay ID mà để ở BF16 sẽ đứng yên từ 257 trở đi. Cũng vì vậy mà bộ cộng dồn trong Chương 6 phải là int32 chứ không thể là một kiểu dấu phẩy động ngắn.

Python
import numpy as np
n = 1
while np.float16(n) == n:
    n += 1
print(n)          # 2049
Bài 3
SQNRTrung bìnhChương 4
#

Chứng minh rằng với lưới đối xứng đầy đủ [−2b−1,2b−1−1][-2^{b-1}, 2^{b-1} - 1] và dữ liệu đều trên đúng dải biểu diễn, SQNR bằng 20log⁡10(2b)20\log_{10}(2^b) dB. So sánh với kết quả 20log⁡10(2b−2)20\log_{10}(2^b - 2) ở Mục 4.2 và giải thích sự khác biệt.

Xem lời giải chi tiết

Thiết lập. Lưới đối xứng đầy đủ [−2b−1, 2b−1−1][-2^{b-1},\ 2^{b-1}-1] có đúng 2b2^b mức, bước nhảy SS. Mỗi mức "phụ trách" một ô rộng SS, nên vùng mà lưới phủ được có bề rộng tổng cộng

W=2b⋅S.W = 2^b \cdot S.

Đề cho dữ liệu phân phối đều trên đúng dải biểu diễn, tức đều trên một đoạn rộng WW.

Công suất tín hiệu. Biến ngẫu nhiên đều trên đoạn rộng WW (lấy tâm làm gốc) có

E[x2]=W212=(2bS)212=4bS212.\mathbb{E}[x^2] = \frac{W^2}{12} = \frac{(2^b S)^2}{12} = \frac{4^{b} S^2}{12}.

Công suất nhiễu. Mọi giá trị đều nằm trong dải nên không có sai số cắt; chỉ còn sai số làm tròn, phân phối đều trên [−S/2,S/2][-S/2, S/2] (Mục 4.1):

E[e2]=S212.\mathbb{E}[e^2] = \frac{S^2}{12}.

Kết quả.

SQNR=4bS2/12S2/12=4b=22b⟹  10log⁡1022b=20log⁡10(2b)≈6,0206 b dB.\begin{gathered} \text{SQNR} = \frac{4^b S^2 / 12}{S^2/12} = 4^{b} = 2^{2b} \\ \Longrightarrow\; 10\log_{10} 2^{2b} = \boxed{20\log_{10}(2^b)} \approx 6{,}0206\,b \ \text{dB}. \end{gathered}

So sánh với 20log⁡10(2b−2)20\log_{10}(2^b - 2) ở Mục 4.2. Hai giả thiết khác nhau ở đúng hai chỗ, và cả hai đều làm giảm công suất tín hiệu chứ không đụng tới nhiễu:

  1. Bỏ bớt một mã. Lưới hạn chế dùng [−(2b−1−1), 2b−1−1][-(2^{b-1}-1),\ 2^{b-1}-1], tức vứt giá trị −2b−1-2^{b-1} để lưới đối xứng hoàn hảo. Còn 2b−12^b - 1 mức thay vì 2b2^b.
  2. Đo dải theo mức chứ không theo ô. Mục 4.2 lấy dữ liệu đều trên [−c,c][-c, c] với c=qSc = qS, q=2b−1−1q = 2^{b-1}-1, tức từ mức thấp nhất đến mức cao nhất, bề rộng 2qS=(2b−2)S2qS = (2^b - 2)S. So với bề rộng phủ ô 2bS2^b S, cách này hụt nửa ô ở mỗi đầu.

Chênh lệch giữa hai kết quả vì thế đúng bằng tỉ số biên độ:

Δ=20log⁡102b−22b=20log⁡10 ⁣(1−2 1−b).\Delta = 20\log_{10}\frac{2^b - 2}{2^b} = 20\log_{10}\!\left(1 - 2^{\,1-b}\right).

bb 20log⁡10(2b)20\log_{10}(2^b) 20log⁡10(2b−2)20\log_{10}(2^b-2) Δ\Delta
4 24,08 dB 22,92 dB −1,16-1{,}16 dB
8 48,16 dB 48,09 dB −0,07-0{,}07 dB
16 96,33 dB 96,33 dB −0,0003-0{,}0003 dB

Hai cột giữa khớp đúng bảng số liệu ở Mục 4.2.

Điều cần rút ra. Hằng số cộng thêm phụ thuộc quy ước, nhưng độ dốc thì không: cả hai đều cho 6,026{,}02 dB mỗi bit. Khi bb lớn, 21−b→02^{1-b} \to 0 và hai công thức trùng nhau. Chênh lệch chỉ đáng kể ở số bit rất thấp — đúng vùng mà mọi thứ khác (chọn ngưỡng, granularity, QAT) cũng bắt đầu quan trọng.

Bài 4
granularityTrung bìnhChương 5
#

Viết lại lập luận ở Mục 5.2 cho phép tích chập 2D và chỉ ra trục nào của tensor trọng số [Cout,Cin,kh,kw][C_{out}, C_{in}, k_h, k_w] có thể mang scale riêng mà vẫn tính được bằng số nguyên. Riêng với depthwise convolution (Cin/groups=1C_{in}/\text{groups} = 1), điều gì thay đổi?

Xem lời giải chi tiết

Viết lại phép tích chập. Với trọng số W[ j,c,u,v ]W[\,j, c, u, v\,] có hình [Cout,Cin,kh,kw][C_{out}, C_{in}, k_h, k_w]:

y[n,j,p,q]=∑c=1Cin∑u=1kh∑v=1kwW[j,c,u,v]×x[n,c, ps+u, qs+v]+b[j].\begin{aligned} y[n, j, p, q] = {} & \sum_{c=1}^{C_{in}} \sum_{u=1}^{k_h} \sum_{v=1}^{k_w} W[j, c, u, v] \\ & \times x[n, c, \,p s + u,\ q s + v] + b[j]. \end{aligned}

Tiêu chuẩn duy nhất (đúng như lập luận ở Mục 5.2): một hệ số scale đưa được ra ngoài tổng khi và chỉ khi nó không phụ thuộc chỉ số bị lấy tổng. Ở đây các chỉ số bị lấy tổng là cc, uu, vv.

Xét từng trục của tensor trọng số:

Trục Bị lấy tổng? Được mang scale riêng?
CoutC_{out} (chỉ số jj) không có — đây chính là per-channel chuẩn
CinC_{in} (chỉ số cc) có không
khk_h (chỉ số uu) có không
kwk_w (chỉ số vv) có không

Vậy với tích chập 2D thông thường, chỉ trục CoutC_{out} mang được scale riêng mà vẫn giữ được bộ cộng dồn thuần số nguyên:

y[n,j,p,q]≈Sw,j Sx∑c,u,vqw[j,c,u,v] (qx[n,c,⋅,⋅]−Zx).y[n,j,p,q] \approx S_{w,j}\, S_x \sum_{c,u,v} q_w[j,c,u,v]\,\bigl(q_x[n,c,\cdot,\cdot] - Z_x\bigr).

Còn activation thì sao? Chỉ số của xx là [n,c,vị trıˊ][n, c, \text{vị trí}]. Chỉ số cc bị lấy tổng nên không được lượng tử activation theo kênh. Chỉ số batch nn thì không bị lấy tổng, nên scale theo từng mẫu là hợp lệ — đây đúng là bản sao của "per-token" ở LLM (Mục 11.2).

Depthwise convolution. Khi Cin/groups=1C_{in}/\text{groups} = 1, trọng số có hình [Cout,1,kh,kw][C_{out}, 1, k_h, k_w] và mỗi kênh đầu ra jj chỉ đọc đúng một kênh đầu vào g(j)g(j):

y[n,j,p,q]  =  ∑u,vW[j,0,u,v]  x[n, g(j), ps+u, qs+v].y[n, j, p, q] \;=\; \sum_{u, v} W[j, 0, u, v]\; x[n,\, g(j),\, ps+u,\, qs+v].

Hai thay đổi:

  1. Tổng không còn chạy theo cc. Chỉ số kênh đầu vào giờ được xác định bởi jj chứ không bị lấy tổng. Về mặt toán học, activation được phép mang scale theo kênh ở riêng lớp này, vì Sx,g(j)S_{x, g(j)} đưa được ra ngoài tổng:

    y[n,j,p,q]≈Sw,j Sx,g(j)∑u,vqw[j,0,u,v] (qx−Zx,g(j)).y[n,j,p,q] \approx S_{w,j}\, S_{x,g(j)} \sum_{u,v} q_w[j,0,u,v]\,(q_x - Z_{x,g(j)}).

    Lưu ý thực tế: đây là điều đúng nhưng hiếm được framework hỗ trợ, vì cùng tensor activation đó thường còn chảy vào các lớp pointwise/Linear phía sau — và ở đó kênh lại bị lấy tổng.
  2. Per-channel trở nên tối quan trọng. Mỗi kênh đầu ra chỉ có khkw=9k_h k_w = 9 trọng số, và sau khi gộp BatchNorm mỗi kênh bị nhân với γj/σj2+ϵ\gamma_j/\sqrt{\sigma_j^2 + \epsilon} khác nhau (Mục 7.3). Dải giữa các kênh vì thế chênh nhau rất mạnh — chính là hiện tượng đo được ở Mục 5.3 và Hình 8, và là lý do lượng tử per-tensor 8 bit làm MobileNetV2 sụp đổ.

Per-group thì sao? Chia trục CinC_{in} thành các nhóm liên tiếp (Mục 5.1) vẫn đặt scale bên trong tổng. Nó chạy được là vì ta chấp nhận tách tổng thành từng nhóm: cộng dồn số nguyên trong mỗi nhóm, rồi nhân scale của nhóm đó và cộng các nhóm lại bằng số thực (hoặc int32 sau requantize). Đổi lại độ chính xác cao hơn là một lần nhân scale cho mỗi nhóm — chấp nhận được khi nhóm đủ lớn (32–128), và đó là lý do per-group là chuẩn cho LLM 4 bit.

Bài 5
số học số nguyênTrung bìnhChương 6
#

Trong code/numpy_experiments.py, đổi trọng số sang lượng tử bất đối xứng (Zw≠0Z_w \ne 0). Bổ sung đúng các số hạng ở Mục 6.2 để kết quả số nguyên vẫn trùng khớp từng bit với mô phỏng. Đếm số phép toán phát sinh thêm.

Xem lời giải chi tiết

Công thức đầy đủ. Khai triển ở Mục 6.2 không giả định gì về ZwZ_w:

∑k(qw,k−Zw)(qx,k−Zx)=∑kqw,kqx,k⏟(1)−Zx∑kqw,k⏟(2)−Zw∑kqx,k⏟(3)+KZwZx⏟(4).\begin{aligned} \sum_k (q_{w,k} - Z_w)(q_{x,k} - Z_x) &= \underbrace{\sum_k q_{w,k} q_{x,k}}_{(1)} - \underbrace{Z_x \sum_k q_{w,k}}_{(2)} \\ &\quad - \underbrace{Z_w \sum_k q_{x,k}}_{(3)} + \underbrace{K Z_w Z_x}_{(4)}. \end{aligned}

Mã trong code/numpy_experiments.py bỏ (3) và (4) vì trọng số đối xứng. Muốn dùng trọng số bất đối xứng thì phải cộng lại đúng hai số hạng đó.

Cài đặt. Với XX hình [M,K][M, K] và WW hình [N,K][N, K]:

Python
# Trọng số bất đối xứng: dùng hết miền int8 [-128, 127], Z_w tính như activation
S_w, Z_w = affine_params(W.min(), W.max(), -128, 127)
q_w = np.clip(np.round(W / S_w) + Z_w, -128, 127).astype(np.int32)

row_w = q_w.sum(axis=1)                       # [N] - TÍNH TRƯỚC được (chỉ phụ thuộc W)
row_x = q_x.sum(axis=1, keepdims=True)        # [M,1] - phải tính LÚC CHẠY

acc = (q_x @ q_w.T                            # (1)  M*N*K phép nhân-cộng int8 -> int32
       - Z_x * row_w                          # (2)  tính trước, gộp thẳng vào q_b
       - Z_w * row_x                          # (3)  phải tính lúc chạy
       + K * Z_w * Z_x                        # (4)  hằng số, gộp vào q_b
       + q_b)

M0, shift = quantize_multiplier(S_w * S_x / S_y)
q_y = np.clip(Z_y + rounding_right_shift(acc.astype(np.int64) * M0, 31 + shift), 0, 255)

Mẹo gộp. Cả (2) và (4) chỉ phụ thuộc trọng số và các hằng Zx,Zw,KZ_x, Z_w, K, nên gộp hẳn vào bias lúc chuyển đổi mô hình:

qb′=qb−Zx∑kqw,k+KZwZx.q_b' = q_b - Z_x \sum_k q_{w,k} + K Z_w Z_x.

Sau khi gộp, lúc chạy chỉ còn dư đúng số hạng (3).

Đếm phép toán phát sinh thêm (so với trọng số đối xứng):

Việc Chi phí Khi nào
(2) Zx∑kqw,kZ_x \sum_k q_{w,k} NKNK phép cộng + NN phép nhân offline, 0 lúc chạy
(4) KZwZxK Z_w Z_x 2 phép nhân offline, 0 lúc chạy
(3) Zw∑kqx,kZ_w \sum_k q_{x,k} M(K−1)M(K-1) cộng + MM nhân + MNMN cộng để phát tán lúc chạy

So với MNKMNK phép nhân-cộng của số hạng (1), phần dư lúc chạy chiếm tỉ lệ

MK+MNMNK=1N+1K.\frac{MK + MN}{MNK} = \frac{1}{N} + \frac{1}{K}.

Với lớp 256→64256 \to 64 trong thí nghiệm (K=256K = 256, N=64N = 64): 1/64+1/256≈1,95%1/64 + 1/256 \approx 1{,}95\%.

Nhưng vì sao thực tế vẫn tránh? Ba lý do mà phép đếm FLOP không thấy:

  1. Số hạng (3) buộc phải quét thêm một lượt toàn bộ ma trận XX để lấy tổng theo hàng. Với bài toán memory-bound (Mục 1.3), thêm một lượt đọc đắt hơn nhiều so với 2% phép tính.
  2. Tổng theo hàng không gộp được vào vòng lặp GEMM đã tối ưu sẵn của thư viện, nên thường thành một kernel riêng.
  3. Trọng số vốn phân bố khá đối xứng quanh 0 (Mục 3.5), nên Zw≠0Z_w \ne 0 gần như không đổi lại được độ chính xác nào. Trả chi phí mà không nhận được gì.

Kiểm chứng. Sau khi sửa, max |q_y(integer) - q_y(float sim)| vẫn phải bằng 0 LSB. Nếu khác 0, gần như chắc chắn bạn quên một trong bốn số hạng, hoặc dùng miền clamp không khớp giữa đường số nguyên và đường mô phỏng số thực (nhớ rằng bất đối xứng dùng hết [−128,127][-128, 127], còn đối xứng chỉ dùng [−127,127][-127, 127]).

Một nguồn lệch nữa, không liên quan tới bốn số hạng: quy ước làm tròn của bước dịch phải. Hàm rounding_right_shift trong bài làm tròn nửa về phía +∞+\infty, còn gemmlowp/TFLite làm tròn nửa ra xa số 0; hai quy ước chỉ khác nhau ở đúng điểm giữa của giá trị âm và lệch tối đa 1 LSB. Nếu bạn so kết quả với một backend thật mà thấy vài phần tử lệch đúng 1 LSB thì hãy nghi chỗ này trước — xem ghi chú Mục 6.4.

Bài 6
calibrationTrung bìnhChương 8
#

Chạy lại thí nghiệm Mục 8.2 nhưng thêm vào tập calibration 1% ảnh nhiễu có biên độ lớn gấp 10 lần bình thường. Phương pháp nào bị ảnh hưởng nhiều nhất? Vì sao?

Xem lời giải chi tiết

Chưa chạy

Bài này cần chạy lại thí nghiệm trong code/torch_experiments.py, tức cần PyTorch và scikit-learn. Phần dưới vì thế là phân tích dự đoán kèm cách kiểm chứng, không phải số liệu đã đo — khác với Bài 8 là bài duy nhất trong nhóm này đã chạy thật. Hãy chạy rồi đối chiếu với dự đoán.

Thay đổi cần làm. Trong phần calibration của code/torch_experiments.py, làm bẩn tập calibration:

Python
idx = torch.randperm(len(X_calib))[: max(1, len(X_calib) // 100)]   # 1% số ảnh
X_calib_bad = X_calib.clone()
X_calib_bad[idx] *= 10.0

rồi chạy lại cả bốn phương pháp và ghi lại ngưỡng chọn được lẫn độ chính xác W4A4.

Dự đoán, xếp theo mức thiệt hại.

  1. Min–max — hỏng nặng nhất. Theo định nghĩa nó lấy giá trị lớn nhất trên toàn tập, nên β\beta nhảy lên khoảng 10 lần. Bước nhảy SS thô lên 10 lần, sai số làm tròn tăng 10 lần, công suất nhiễu tăng 100 lần (Mục 4.1) — tương đương mất hơn 3 bit theo quy tắc 6 dB. Ở 4 bit chỉ còn 16 mức, mà 99% dữ liệu giờ nằm gọn trong khoảng 1/10 dưới của dải, tức thực chất chỉ còn 1–2 mức hữu ích. Đây là minh hoạ sống động cho Hình 7: min–max không hề "an toàn".

  2. Percentile 99,99% — hỏng nhiều hơn người ta tưởng. Phân vị chỉ an toàn khi ta cắt nhiều hơn tỉ lệ dữ liệu bị nhiễm. Ở đây 1% số ảnh bị nhiễm, và mỗi ảnh bẩn sinh ra rất nhiều activation lớn, nên tỉ lệ giá trị nằm trong vùng outlier vượt xa 0,01%. Phân vị 99,99% vì thế vẫn rơi vào vùng nhiễu. Bài học: con số phân vị phải chọn theo tỉ lệ nhiễm, mà tỉ lệ đó thì ta không biết trước.

  3. MSE — chịu được một phần. Nó tối thiểu E[(x−x^)2]\mathbb{E}[(x-\hat{x})^2], mà cắt 1% giá trị ở biên độ 10c10c tốn khoảng 0,01⋅(9c)20{,}01 \cdot (9c)^2 sai số bình phương — không nhỏ, nên ngưỡng tối ưu có dịch lên. Nhưng đây là một phép đánh đổi trơn chứ không phải yêu cầu cứng "phải phủ hết", nên mức xấu đi nhẹ hơn hẳn min–max.

  4. KL (entropy) — bền nhất. Nó so sánh hình dạng phân phối. 1% khối lượng nằm ở đuôi xa đóng góp rất ít vào DKLD_{KL}, trong khi làm thô vùng trung tâm dày dữ liệu thì phạt rất nặng. Ngưỡng chọn được sẽ gần với ngưỡng trên tập sạch.

Thứ tự dự đoán: min–max >> percentile 99,99% >> MSE >> KL (thiệt hại giảm dần).

Hai cảnh báo khi đọc kết quả.

  • Mô hình thí nghiệm rất nhỏ và bài toán rất dễ. Mục 8.2 cho thấy ngưỡng chênh nhau 1,7 lần mà độ chính xác gần như không đổi. Rất có thể độ chính xác không phản ánh rõ thứ tự trên; hãy nhìn cả ngưỡng chọn được và MSE của từng lớp, đó mới là đại lượng nhạy.
  • Mỗi ảnh test đổi kết quả 0,19 điểm phần trăm. Đừng kết luận từ chênh lệch dưới 1 điểm nếu chỉ chạy một lần.

Bài học thực hành (Mục 8.3). Lỗi thật sự trong bài này không nằm ở phương pháp calibration mà ở tập calibration. Một tập bị nhiễm bẩn phá min–max từ rất lâu trước khi nó phá mô hình. Trong quy trình sản xuất, luôn kiểm tra histogram activation trước khi tin vào ngưỡng.

Bài 7
QATKhóChương 9
#

Thay dải activation cố định bằng EMA trong qat() của code/torch_experiments.py bằng scale học được theo LSQ (Mục 9.2). So sánh độ chính xác ở W3A3 và W2A2.

Xem lời giải chi tiết

Chưa chạy

Bài này cần huấn luyện lại bằng PyTorch. Phần dưới là hướng dẫn cài đặt đầy đủ cùng dự đoán, không phải số liệu đã đo. Khi báo cáo kết quả, hãy ghi rõ đâu là số đo được và đâu là kỳ vọng.

Ý tưởng LSQ (Mục 9.2). Thay vì để SS bám theo EMA của min/max, ta coi SS là tham số huấn luyện được và cho nó nhận gradient từ chính hàm mất mát. Với lượng tử đối xứng

x^=S⋅clamp ⁣(⌊x/S⌉, −QN, QP),\hat{x} = S \cdot \text{clamp}\!\left(\left\lfloor x/S \right\rceil,\ -Q_N,\ Q_P\right),

đạo hàm theo SS (vẫn dùng STE cho phép làm tròn) là

∂x^∂S={−QNx/S≤−QN⌊x/S⌉−x/S−QN<x/S<QPQPx/S≥QP\frac{\partial \hat{x}}{\partial S} = \begin{cases} -Q_N & x/S \le -Q_N \\ \lfloor x/S \rceil - x/S & -Q_N < x/S < Q_P \\ Q_P & x/S \ge Q_P \end{cases}

Cài đặt trong PyTorch. Hai hàm STE nhỏ là đủ, không cần viết autograd.Function:

Python
def round_ste(x):                       # lượt xuôi: làm tròn; lượt ngược: đồng nhất
    return (x.round() - x).detach() + x

def grad_scale(x, g):                   # lượt xuôi: x; lượt ngược: nhân gradient với g
    return (x - x * g).detach() + x * g

class LsqQuant(nn.Module):
    def __init__(self, n_bits, signed):
        super().__init__()
        self.Qn = 2 ** (n_bits - 1) if signed else 0
        self.Qp = 2 ** (n_bits - 1) - 1 if signed else 2 ** n_bits - 1
        self.S = nn.Parameter(torch.tensor(1.0))
        self.inited = False

    def forward(self, x):
        if not self.inited:             # khởi tạo theo bài báo LSQ
            self.S.data = 2 * x.detach().abs().mean() / (self.Qp ** 0.5)
            self.inited = True
        g = 1.0 / (x.numel() * self.Qp) ** 0.5      # hệ số chỉnh gradient
        s = grad_scale(self.S, g)
        return torch.clamp(round_ste(x / s), -self.Qn, self.Qp) * s

Ba chi tiết quyết định thành bại.

  1. Hệ số chỉnh gradient g=1/N⋅QPg = 1/\sqrt{N \cdot Q_P}, với NN là số phần tử dùng chung scale đó. Không có nó, gradient của SS lớn hơn gradient của từng trọng số khoảng N\sqrt{N} lần và quá trình huấn luyện dao động rồi phân kỳ. Đây là phần hay bị bỏ sót nhất khi cài lại LSQ.
  2. Khởi tạo. Dùng S←2 ∣x∣‾/QPS \leftarrow 2\,\overline{|x|}/\sqrt{Q_P} trên batch đầu, hoặc khởi tạo từ ngưỡng PTQ–MSE như qat() đang làm. Khởi tạo tệ làm LSQ mất vài epoch chỉ để bò về vùng hợp lý.
  3. QAT là fine-tune. Giữ nguyên bài học ở Mục 10.1.3: xuất phát từ mô hình FP32 đã hội tụ, learning rate nhỏ (5×10−45 \times 10^{-4}), vài epoch. Có thể đặt learning rate riêng, lớn hơn một chút, cho các tham số SS.

Dự đoán kết quả. Mốc so sánh là QAT dùng EMA ở Mục 9.3: 94,01% tại W3A3 và 49,44% tại W2A2 (FP32: 99,26%).

  • W3A3: kỳ vọng cải thiện ít. Ở 3 bit, EMA đã cho ngưỡng gần đủ tốt và mạng còn dư sức bù bằng trọng số.
  • W2A2: kỳ vọng cải thiện rõ nhất. Chỉ còn 4 mức, nên vị trí đặt mức quyết định gần như tất cả, và đó đúng là thứ LSQ tối ưu trực tiếp.

Cách báo cáo cho trung thực. Chạy 3 hạt giống như Mục 9.3 và báo cáo trung bình kèm khoảng dao động. Ở W2A2 độ chính xác dao động rất mạnh giữa các lần chạy; một con số đơn lẻ không nói lên điều gì. Nhớ rằng 1 ảnh test = 0,19 điểm phần trăm.

Hướng mở rộng. LSQ+ bổ sung thêm offset học được (tức ZZ cũng học), hợp với activation bất đối xứng — đáng thử ở W2A2, nơi mọi mức đều quý.

Bài 8
LLMTrung bìnhChương 11
#

Trong mô phỏng ở Mục 11.3, quét α\alpha từ 0 đến 1 với bước 0,1 và vẽ sai số đầu ra theo α\alpha. Giải thích vì sao cả hai đầu α=0\alpha = 0 và α=1\alpha = 1 đều không tốt.

Xem lời giải chi tiết

Bài này đã được chạy thật

Script code/sweep_alpha.py dựng lại đúng thí nghiệm ở Mục 11.3 — cùng hạt giống 42 và cùng thứ tự rút số ngẫu nhiên — nên nó tái lập chính xác hai mốc đã in trong giáo trình: per-tensor 7,7435% và SmoothQuant α=0,5\alpha = 0{,}5 cho 1,4430%. Mọi con số dưới đây lấy từ lần chạy đó, không phải dự đoán.

Cách quét.

Python
ax = np.abs(Xl).max(axis=0)          # biên độ activation theo kênh đầu vào
aw = np.abs(Wl).max(axis=0)          # biên độ trọng số theo kênh đầu vào

for alpha in np.arange(0.0, 1.001, 0.05):
    s = ax ** alpha / aw ** (1 - alpha)
    Xs, Ws = Xl / s, Wl * s
    assert np.allclose(Xs @ Ws.T, Yl)                      # biến đổi tương đương
    err = rel(a8_per_tensor(Xs) @ w8_per_channel(Ws).T, Yl)

Chạy bằng python code/sweep_alpha.py; có matplotlib thì script vẽ luôn đường cong.

Kết quả đo được.

α\alpha Sai số đầu ra Chênh lệch giữa các kênh XX Chênh lệch giữa các cột WW
0,00 8,02% 91,3× 1,0×
0,20 3,60% 37,0× 2,5×
0,30 2,48% 23,6× 3,9×
0,40 1,81% 15,0× 6,1×
0,50 1,44% 9,6× 9,6×
0,60 1,34% 6,1× 15,0×
0,70 1,46% 3,9× 23,6×
0,80 1,84% 2,5× 37,0×
1,00 3,64% 1,0× 91,3×

Đường cong đúng là chữ U như trực giác dự đoán. Nhưng hai chi tiết chỉ lộ ra khi đo thật.

Chi tiết 1 — α=0\alpha = 0 không phải "không làm gì"#

Không biến đổi gì cả: 7,74%. Đặt α=0\alpha = 0: 8,02%. Tức là tệ hơn một chút.

Khi α=0\alpha = 0 thì sj=1/max⁡∣W:,j∣s_j = 1/\max|W_{:,j}|, nên mỗi kênh activation bị nhân với max⁡∣W:,j∣\max|W_{:,j}|. Đây là một phép nhân thật, nên:

  • Nó không xoá được cấu trúc outlier, vì max⁡∣W:,j∣\max|W_{:,j}| chẳng liên quan gì tới việc kênh jj có phải outlier hay không. Sáu kênh lớn vẫn lớn.
  • Nhưng nó vẫn làm đổi tỉ lệ giữa các kênh: tỉ lệ giữa kênh jj và kênh kk bị nhân thêm max⁡∣W:,j∣/max⁡∣W:,k∣\max|W_{:,j}| / \max|W_{:,k}|. Ở lần chạy này max⁡∣W∣\max|W| chênh nhau 1,94 lần giữa các cột, và kết quả là chênh lệch giữa các kênh activation tăng từ 77,4× lên 91,3×.

Nhận xét

Đây là chỗ dễ nói sai nhất của bài. "Hệ số không tương quan với outlier" không đồng nghĩa với "tỉ lệ giữa các kênh không đổi". Không tương quan chỉ có nghĩa là phép nhân ấy không giúp gì; nó vẫn là một phép nhân và vẫn làm xáo trộn tỉ lệ, theo hướng nào thì tuỳ dữ liệu.

Chạy thêm hai hạt giống khác cho 8,48% và 8,43%, so với 8,05% và 8,52% khi không biến đổi. Kết luận bền vững là: α=0\alpha = 0 gần như không mang lại gì, chứ không phải lúc nào cũng có hại. Toàn bộ độ khó vẫn nằm ở activation, nơi ta chỉ có một scale duy nhất cho cả tensor.

Chi tiết 2 — cực tiểu nằm ở α≈0,60\alpha \approx 0{,}60, không phải 0,50#

Ở α=0,5\alpha = 0{,}5, hai vế có biên độ bằng nhau đúng như lý thuyết: sau biến đổi cả hai đều bằng

max⁡∣X:,j∣/sj  =  sjmax⁡∣W:,j∣  =  max⁡∣X:,j∣⋅max⁡∣W:,j∣,\max|X_{:,j}| / s_j \;=\; s_j \max|W_{:,j}| \;=\; \sqrt{\max|X_{:,j}| \cdot \max|W_{:,j}|},

tức trung bình nhân — bảng trên cho thấy cả hai dòng đều là 9,6×. Đây đúng là ý tưởng cân bằng giữa các lớp (CLE) ở Mục 9.5, áp dụng giữa activation và trọng số.

Nhưng biên độ bằng nhau không có nghĩa sai số bằng nhau. Tách riêng từng nguồn sai số:

α\alpha chỉ lượng tử WW chỉ lượng tử XX cả hai
0,0 0,66% 7,99% 8,02%
0,5 0,60% 1,31% 1,44%
1,0 3,55% 0,80% 3,64%

Ở α=0,5\alpha = 0{,}5, activation vẫn là vế đắt gấp hơn hai lần trọng số. Vì vậy đẩy thêm chút độ khó sang phía trọng số còn có lợi, và cực tiểu dịch sang 0,60.

Vì sao trọng số chịu đựng giỏi hơn với cùng mức chênh lệch? Vì granularity của hai vế khác nhau (Chương 5):

  • Trọng số được lượng tử per-kênh-đầu-ra: 512 scale, mỗi hàng một cái.
  • Activation được lượng tử per-tensor: đúng một scale cho toàn bộ tensor.

Cùng một mức chênh lệch 9,6×, vế có 512 scale hấp thụ rẻ hơn hẳn vế chỉ có 1 scale. Quy tắc mang đi được: cực tiểu luôn lệch về phía tensor có granularity mịn hơn. Nếu đổi activation sang per-token (nhiều scale hơn), cực tiểu sẽ dịch ngược lại gần 0,5.

Cực tiểu ở 0,60 lặp lại y hệt trên cả ba hạt giống đã thử, nên đây không phải chuyện may rủi của một lần rút số.

Vì sao α=1\alpha = 1 cũng không tốt#

Khi đó sj=max⁡∣X:,j∣s_j = \max|X_{:,j}|:

  • X diag(s)−1X\,\text{diag}(s)^{-1}: mọi kênh activation về biên độ 1 — activation phẳng lý tưởng, sai số chỉ còn 0,80%.
  • W diag(s)W\,\text{diag}(s): cột jj của trọng số bị nhân lên max⁡∣X:,j∣\max|X_{:,j}|, nên toàn bộ độ chênh 91,3× chuyển sang trọng số. Chỗ chết người: độ chênh vừa tạo ra nằm dọc theo cột (kênh đầu vào), trong khi scale của trọng số lại theo hàng (kênh đầu ra). Vậy là độ chênh nằm gọn bên trong phạm vi của một scale, đúng chỗ mà per-channel không với tới. Sai số do trọng số nhảy từ 0,60% lên 3,55%.

Những gì mang đi được#

  • Hình dạng chữ U và lý do tạo ra nó: hai vế cùng chia nhau một lượng độ khó cố định, ép hết về một phía đều đắt.
  • Đáy khá phẳng: mọi α∈[0,5; 0,7]\alpha \in [0{,}5;\ 0{,}7] đều dưới 1,5%. Chọn 0,5 như bài báo chỉ đắt hơn cực tiểu 8%, không đáng để dò kỹ.
  • Cực tiểu lệch về phía có granularity mịn hơn — đây mới là quy tắc dùng lại được cho mô hình thật.
  • Số tuyệt đối thì không mang đi được. Dữ liệu ở đây là tổng hợp, chỉ 6 kênh outlier, biên độ gấp đúng 60 lần. Bài báo SmoothQuant lấy α=0,5\alpha = 0{,}5 làm mặc định và ghi nhận mô hình có outlier nặng hơn cần giá trị lớn hơn; muốn biết con số cho mô hình của mình thì phải quét trên chính mô hình đó.
Bài 9
tính dung lượngCơ bảnChương 11
#

Một mô hình 13 tỉ tham số, 40 lớp, 40 head, dhead=128d_{\text{head}} = 128, không dùng GQA. Tính dung lượng trọng số ở dạng GGUF Q4_K và dung lượng KV cache FP16 cho batch 4, ngữ cảnh 8192 token. Thành phần nào lớn hơn?

Xem lời giải chi tiết

Dữ kiện. 13 tỉ tham số; nlayers=40n_{\text{layers}} = 40; nheads=40n_{\text{heads}} = 40; dhead=128d_{\text{head}} = 128; không dùng GQA nên nkv_heads=nheads=40n_{\text{kv\_heads}} = n_{\text{heads}} = 40. Batch 4, ngữ cảnh 8192 token.

a) Dung lượng trọng số ở GGUF Q4_K#

Bảng ở Mục 11.8 cho số bit thực tế mỗi trọng số của Q4_K:

256⋅4  +  8⋅12  +  32256=1024+96+32256=1152256=4,5 bit/trọng soˆˊ.\begin{aligned} \frac{256 \cdot 4 \;+\; 8 \cdot 12 \;+\; 32}{256} &= \frac{1024 + 96 + 32}{256} \\ &= \frac{1152}{256} = 4{,}5 \ \text{bit/trọng số}. \end{aligned}

13×109×4,5 bit=58,5×109 bit=7,3125×109 byte=7,31 GB=6,81 GiB.\begin{aligned} 13 \times 10^{9} \times 4{,}5 \ \text{bit} &= 58{,}5 \times 10^{9} \ \text{bit} \\ &= 7{,}3125 \times 10^{9} \ \text{byte} \\ &= \boxed{7{,}31\ \text{GB}} = 6{,}81 \ \text{GiB}. \end{aligned}

Đối chiếu thực tế: một file Q4_K_M 13B thật thường lớn hơn con số này một chút (quãng 7,5–8 GiB), vì llama.cpp giữ embedding và lớp đầu ra ở độ chính xác cao hơn và trộn Q6_K cho một số tensor — đúng như cảnh báo cuối Mục 11.8.

b) Dung lượng KV cache FP16#

Áp công thức ở Mục 11.7:

2×nlayers×nkv_heads×dhead×soˆˊ token×batch×soˆˊ byte2 \times n_{\text{layers}} \times n_{\text{kv\_heads}} \times d_{\text{head}} \times \text{số token} \times \text{batch} \times \text{số byte}

Tính theo từng bậc cho dễ kiểm tra:

Đại lượng Phép tính Kết quả
Mỗi token, mỗi chuỗi 2×40×40×128×22 \times 40 \times 40 \times 128 \times 2 819 200819\,200 B =0,78= 0{,}78 MiB
Một chuỗi 8192 token × 8192\times\, 8192 6,716{,}71 GB =6,25= 6{,}25 GiB
Batch 4 × 4\times\, 4 26,84 GB=25,0\boxed{26{,}84\ \text{GB}} = 25{,}0 GiB

c) Thành phần nào lớn hơn#

KV cache lớn hơn trọng số 3,67 lần (26,84 GB so với 7,31 GB). Tổng cộng khoảng 34 GB, tức không vừa một GPU 24 GB, dù "mô hình 4 bit" nghe như chỉ tốn 7 GB.

Điều cần rút ra. Với ngữ cảnh dài và batch không nhỏ, lượng tử trọng số thôi là chưa đủ. Các hướng xử lý, theo đúng Mục 11.7:

Cách Hiệu quả trong ví dụ này
Lượng tử KV cache sang FP8/INT8 26,84 GB → 13,4 GB
Lượng tử KV cache sang INT4 (kiểu KIVI) 26,84 GB → 6,7 GB
Dùng GQA với 8 KV head 26,84 GB → 5,4 GB (giảm 5 lần)
Giảm batch xuống 1 26,84 GB → 6,7 GB

Chính phép tính này giải thích vì sao GQA (và trước đó là MQA) trở thành lựa chọn phổ biến ở các mô hình mở cỡ lớn ra đời từ 2023 trở đi: nó cắt KV cache ngay từ kiến trúc, tức trước khi phải nhờ tới lượng tử hoá. Nhưng đây là một xu hướng, không phải quy luật và cũng không phải cách duy nhất — vẫn có mô hình giữ MHA đầy đủ, và các hướng khác như lượng tử KV cache, nén KV cache hay attention thưa đều nhắm vào cùng nút thắt. Khi đọc một mô hình cụ thể, hãy tra nkv_headsn_{\text{kv\_heads}} trong file cấu hình của nó thay vì suy đoán từ năm phát hành.

Luyện tính tay#

Mỗi lần bấm Câu khác sẽ sinh một đề mới với số liệu ngẫu nhiên. Mọi phép làm tròn đều theo quy tắc nửa về số chẵn giống NumPy.

Trắc nghiệm theo chương#

48 câu bám sát nội dung và số liệu của từng chương. Đáp án được xáo lại mỗi lần tải trang.

Chương 1 — Vì sao cần quantization

3 câu

  1. 1Theo Hình 1, đọc 64 bit từ DRAM tốn năng lượng gấp khoảng bao nhiêu lần một phép nhân FP32?

  2. 2Sinh từng token của một LLM với batch nhỏ là bài toán thuộc loại nào?

  3. 3"Lượng tử hoá luôn làm mô hình chạy nhanh hơn." Phát biểu này:

Đã trả lời 0/3

Chương 2 — Máy tính biểu diễn số

3 câu

  1. 1Phát biểu nào đúng về cách FP32 lưu một số?

  2. 2Vì sao BF16 được ưa dùng để huấn luyện hơn FP16?

  3. 3Khác biệt cốt lõi giữa lưới của số dấu phẩy động và của số nguyên là gì?

Đã trả lời 0/3

Chương 3 — Uniform affine quantization

4 câu

  1. 1Công thức đúng của bước nhảy SS là gì?

  2. 2Vì sao ZZ bắt buộc phải là một số nguyên?

  3. 3Quy ước phổ biến trong TFLite và PyTorch là gì?

  4. 4Dùng lưới đối xứng cho activation sau ReLU ở 3 bit thì lãng phí bao nhiêu mức?

Đã trả lời 0/4

Chương 4 — Phân tích sai số

3 câu

  1. 1Thêm một bit vào độ rộng bit thì SQNR thay đổi thế nào?

  2. 2Theo mô hình nhiễu đều, kỳ vọng bình phương sai số làm tròn bằng bao nhiêu?

  3. 3Trên 10610^6 trọng số Laplace lượng tử INT4 đối xứng, dùng dải min–max thay vì ngưỡng tối ưu làm MSE tăng bao nhiêu lần?

Đã trả lời 0/3

Chương 5 — Độ mịn (granularity)

4 câu

  1. 1Vì sao scale per-channel của trọng số phải đặt theo trục đầu ra?

  2. 2Phát biểu nào chặt nhất về việc lượng tử activation theo kênh?

  3. 3Vì sao lượng tử per-group (nhóm 32–128) đặt được scale trên trục thu gọn?

  4. 4Trong thí nghiệm Mục 5.3, granularity ảnh hưởng ra sao ở 8 bit so với 4 bit?

Đã trả lời 0/4

Chương 6 — Suy luận bằng số nguyên

7 câu

  1. 1Vì sao bộ cộng dồn trong phép nhân ma trận INT8 phải là int32?

  2. 2Trọng số lượng tử đối xứng (Zw=0Z_w = 0) mang lại lợi ích tính toán gì?

  3. 3Bias được lưu như thế nào trong đường tính toán số nguyên?

  4. 4Requantization nhân với hệ số thực MM bằng cách nào mà không dùng số thực?

  5. 5Đoạn mã quantize_multiplier ở Mục 6.4 dùng được cho hệ số MM nào?

  6. 6Hàm dịch phải có làm tròn trong Mục 6.4 xử lý giá trị âm theo quy ước nào?

  7. 7Kết quả "trùng khớp từng bit" ở Mục 6.5 chứng minh điều gì?

Đã trả lời 0/7

Chương 7 — Lượng tử cả một mạng

3 câu

  1. 1BatchNorm phải được gộp vào tích chập ở thời điểm nào?

  2. 2Nút fake-quant làm gì?

  3. 3Chế độ nào không cần dữ liệu calibration?

Đã trả lời 0/3

Chương 8 — Calibration

3 câu

  1. 1Tập calibration thường cần bao nhiêu mẫu?

  2. 2Phương pháp calibration nào nhạy nhất với outlier?

  3. 3Ý tưởng của calibration theo KL divergence (entropy) là gì?

Đã trả lời 0/3

Chương 9 — PTQ và QAT

4 câu

  1. 1Straight-Through Estimator giải quyết vấn đề gì?

  2. 2Ở W8A8, QAT so với PTQ như thế nào trong thí nghiệm Mục 9.3?

  3. 3Ở W3A3, kết quả ra sao?

  4. 4Cross-Layer Equalization dựa trên tính chất nào của ReLU?

Đã trả lời 0/4

Chương 10 — PyTorch và TFLite

3 câu

  1. 1torch.ao.quantization.quantize_dynamic lượng tử những lớp nào?

  2. 2Vì sao Int8WeightOnly của torchao chạy eager lại chậm hơn FP32?

  3. 3Một báo cáo lượng tử hoá đáng tin cần đủ những con số nào?

Đã trả lời 0/3

Chương 11 — Quantization cho LLM

7 câu

  1. 1Vì sao weight-only 4 bit (W4A16) tăng tốc LLM gần tỉ lệ với mức nén?

  2. 2SmoothQuant làm gì?

  3. 3Kết quả nào của GPTQ nghe nghịch lý nhưng rất đáng suy ngẫm?

  4. 4NF4 đặt 16 mức lượng tử ở đâu?

  5. 5"Mô hình 4 bit" trong thực tế tốn bao nhiêu bit mỗi trọng số?

  6. 6Quét α\alpha của SmoothQuant trên chính dữ liệu của Mục 11.3 cho kết quả nào?

  7. 7Vì sao cực tiểu lại lệch sang phải điểm cân bằng α=0,5\alpha = 0{,}5?

Đã trả lời 0/7

Chương 12 — Quy trình làm việc

2 câu

  1. 1Trong phân tích độ nhạy ở Mục 12.2, lớp nào nhạy nhất?

  2. 2Cách đơn giản và hiệu quả nhất để tìm lớp gây mất độ chính xác là gì?

Đã trả lời 0/2

Chương 13 — Những hiểu lầm thường gặp

2 câu

  1. 1"INT8 luôn giảm kích thước file đúng 4 lần." Phát biểu này:

  2. 2"Sai số trọng số càng nhỏ thì mô hình càng chính xác." Phát biểu này:

Đã trả lời 0/2