Quantizationgiáo trình tự học
Chương 64 phút đọcSuy luận số nguyên

6Suy luận hoàn toàn bằng số nguyên#

Chương này trả lời câu hỏi: vì sao INT8 không chỉ nhẹ hơn mà còn nhanh hơn? Nội dung dựa theo Jacob và cộng sự (2018), nền tảng của cơ chế lượng tử trong TFLite.

6.1Nhân ma trận với các đại lượng đã lượng tử#

Xét y=∑k=1Kwkxky = \sum_{k=1}^{K} w_k x_k (một phần tử của phép nhân ma trận hay tích chập). Thay mọi đại lượng bằng biểu diễn lượng tử r=S(q−Z)r = S(q - Z):

Sy(qy−Zy)=∑kSw(qw,k−Zw)  Sx(qx,k−Zx).S_y(q_y - Z_y) = \sum_{k} S_w(q_{w,k} - Z_w)\; S_x(q_{x,k} - Z_x).

Giải ra qyq_y:

qy=Zy+SwSxSy⏟M∑k(qw,k−Zw)(qx,k−Zx).q_y = Z_y + \underbrace{\frac{S_w S_x}{S_y}}_{M} \sum_{k}(q_{w,k} - Z_w)(q_{x,k} - Z_x).

6.2Khai triển và những gì tính trước được#

Khai triển tích trong tổng:

∑kqw,k qx,k  −  Zx∑kqw,k  −  Zw∑kqx,k  +  KZwZx.\sum_k q_{w,k}\,q_{x,k} \;-\; Z_x \sum_k q_{w,k} \;-\; Z_w \sum_k q_{x,k} \;+\; K Z_w Z_x.

Phân tích từng số hạng:

  1. ∑kqw,kqx,k\sum_k q_{w,k} q_{x,k} là phần việc chính, gồm KK phép nhân int8×int8, cộng dồn vào thanh ghi int32.
  2. Zx∑kqw,kZ_x \sum_k q_{w,k} chỉ phụ thuộc trọng số và hằng ZxZ_x, nên tính trước được lúc chuyển đổi mô hình.
  3. Zw∑kqx,kZ_w \sum_k q_{x,k} phụ thuộc đầu vào, phải tính lúc chạy, tốn thêm KK phép cộng.
  4. KZwZxK Z_w Z_x là hằng số.

Nếu trọng số lượng tử đối xứng (Zw=0Z_w = 0), hai số hạng 3 và 4 biến mất. Đây là lý do kỹ thuật khiến trọng số gần như luôn được lượng tử đối xứng.

6.3Bias#

Bias được lưu dạng int32 với scale SwSxS_w S_x và zero-point 0: qb=⌊b/(SwSx)⌉q_b = \lfloor b / (S_w S_x) \rceil. Nhờ cùng scale với bộ cộng dồn, nó được cộng thẳng vào kết quả int32 mà không cần đổi đơn vị. Bias dùng 32 bit vì nó ít tham số nhưng ảnh hưởng trực tiếp lên mọi đầu ra, và vì bước SwSxS_w S_x rất nhỏ.

6.4Requantization: nhân với số thực mà không dùng số thực#

Hệ số M=SwSx/SyM = S_w S_x / S_y là số thực. Thực nghiệm cho thấy MM gần như luôn nằm trong (0,1)(0, 1). Ta viết

M=2−nM0,M0∈[0,5; 1),M = 2^{-n} M_0, \qquad M_0 \in [0{,}5;\ 1),

rồi lưu M0M_0 dưới dạng số nguyên 32 bit dấu chấm tĩnh: M0int=⌊M0⋅231⌉M_0^{\text{int}} = \lfloor M_0 \cdot 2^{31} \rceil. Phép nhân với MM trở thành một phép nhân số nguyên 64 bit và một phép dịch phải có làm tròn:

acc⋅M≈(acc⋅M0int)≫(31+n).\text{acc} \cdot M \approx \left(\text{acc} \cdot M_0^{\text{int}}\right) \gg (31 + n).

Cuối cùng cộng ZyZ_y và cắt về [0,255][0, 255]. Toàn bộ quy trình được tóm tắt trong Hình 9.

Hình 9
Hình 9. Luồng dữ liệu của một lớp Linear/Conv chạy hoàn toàn bằng số nguyên. Hộp màu cam là các đại lượng tính trước khi triển khai.

Hàm kích hoạt ReLU được "gộp miễn phí": nếu dải đầu ra được calibrate sau ReLU thì αy=0\alpha_y = 0, suy ra Zy=qminZ_y = q_{min}, và phép cắt ở bước cuối tự động thực hiện ReLU. ReLU6 tương tự với cận trên. Các hàm phi tuyến phức tạp hơn (sigmoid, softmax, GELU) thường dùng bảng tra hoặc được tính ở độ chính xác cao hơn.

6.5Kiểm chứng bằng mã#

Đoạn mã sau cài đặt đúng quy trình Hình 9 bằng NumPy cho một lớp Linear 256→64256 \to 64 và so sánh với cách mô phỏng bằng số thực (trích từ code/numpy_experiments.py):

Python
def quantize_multiplier(M):
    """Viết M (0 < M < 1) dưới dạng M0 * 2^-(31 + shift), với M0 là int32 trong [2^30, 2^31)."""
    shift = 0
    while M < 0.5:
        M *= 2; shift += 1
    M0 = int(round(M * (1 << 31)))
    if M0 == (1 << 31):
        M0 //= 2; shift -= 1
    return M0, shift

def rounding_right_shift(x, n):          # chia cho 2^n, làm tròn gần nhất
    return (x + (1 << (n - 1))) >> n

# X: activation (uint8 bất đối xứng), W: trọng số (int8 đối xứng), b: bias
q_x = np.clip(np.round(X / S_x) + Z_x, 0, 255).astype(np.int32)
q_w = np.clip(np.round(W / S_w), -127, 127).astype(np.int32)
q_b = np.round(b / (S_w * S_x)).astype(np.int32)

acc = q_x @ q_w.T - Z_x * q_w.sum(axis=1) + q_b          # int32; q_w.sum tính trước được
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)

Kết quả (code/numpy_experiments_output.txt):

Kết quả chạy
S_x=0.038963 Z_x=106 | S_w=0.001661 | S_y=0.031995 Z_y=120
M = 0.0020223740 = 1111811840 * 2^-(31+8) -> 0.0020223740
max |q_y(integer) - q_y(float sim)| = 0 LSB; tỉ lệ khác nhau: 0.0000%
sai số tương đối so với FP32: 1.6085%
max |acc| = 66876 ; giới hạn int16 = 32767 ; cận trên lý thuyết 255*127*K = 8290560

Ba điều rút ra:

  1. Đường tính toán thuần số nguyên cho kết quả trùng khớp từng bit với cách mô phỏng bằng số thực. Đây là lý do mô phỏng fake quantization (Chương 7) dự đoán đúng độ chính xác của mô hình khi triển khai.
  2. Sai số so với FP32 khoảng 1,6%, hoàn toàn do lượng tử chứ không do phép tính số nguyên.
  3. Giá trị cộng dồn lên tới 66 876, vượt giới hạn int16. Đó là lý do bộ cộng dồn phải là int32. Cận trên lý thuyết 8,3×1068{,}3 \times 10^6 vẫn nằm rất xa giới hạn int32 (≈2,1×109\approx 2{,}1 \times 10^9).

Khi dùng per-channel cho trọng số, mỗi kênh đầu ra jj có hệ số Mj=Sw,jSx/SyM_j = S_{w,j} S_x / S_y riêng, còn lại mọi thứ giữ nguyên.


Tự kiểm tra

7 câu. Chọn đáp án để xem ngay lời giải thích.

  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