Chuyển tới nội dung chính

Một bit dùng vào việc gì

Nhìn từng bitLàm tròn tới số chẵnTính lại thật

Một bit dùng vào việc gì

FP16 và BF16 cùng chiếm hai byte, cùng một hoá đơn bộ nhớ, cùng một tốc độ nạp. Nhưng một cái sống sót qua huấn luyện còn cái kia thì không, và lý do nằm gọn trong cách chúng chia mười sáu bit đó.

Ở bài KV cache có một thừa số tên là byte mỗi phần tử: đổi bộ đệm từ FP16 sang INT8 là cắt hoá đơn đi một nửa, không cần huấn luyện lại gì cả. Đó là cái núm rẻ nhất trong toàn bộ chương trước. Bài này mở cái núm đó ra xem bên trong có gì.

Câu hỏi rất cụ thể: một số thực khi bị nhét vào 16 bit, 8 bit hay 4 bit thì mất chính xác cái gì. Không phải mất "một ít độ chính xác" chung chung, mà mất bit nào, sai bao nhiêu, và sai theo kiểu gì. Bảng dưới đây gõ số vào là thấy từng bit.

Một bit dùng vào việc gì · bố cục bit của các định dạng số
Đang lưu 0,1Ghi đúng 0/7Tràn 0
Số mẫu:
0,1: một phần mười là số tuần hoàn trong hệ nhị phân nên không định dạng nào ghi đúng.
Cấu hình✎ sửa được
số cần lưu
dùng dấu phẩy hoặc dấu chấm cho phần thập phân, số mũ viết kiểu 1e30
số tham số (tỉ)
chỉ dùng cho hàng kế toán byte ở cuối, thanh kéo tới 200 tỉ còn ô số nhận tới 2000
bit dấu số mũ phần định trịcác hàng canh phải theo 32 bit của FP32, nên nhìn dọc là thấy định dạng nào ngắn hơn ở đâumàn hình hẹp nên mỗi hàng bắt đầu ngay ở bit dấu của nó, kéo ngang để xem hết
FP321 + 8 + 23001111011100110011001100110011010x3DCCCCCD
FP161 + 5 + 1000101110011001100x2E66
BF161 + 8 + 700111101110011010x3DCD
FP8 E4M31 + 4 + 3000111010x1D
FP8 E5M21 + 5 + 2001011100x2E
INT81 + 7 (bù hai)000000000x00
INT41 + 3 (bù hai)00000x0
Định dạngByteGiá trị lưu đượcSai số tuyệt đốiSai số tương đốiLớn nhấtDương nhỏ nhấtLoại
FP3240,1000000011,49e-91,49e-6%3,4028e381,4013e-45chuẩn
FP1620,09997558592,441e-50,0244%655045,9605e-8chuẩn
BF1620,1000976569,766e-50,0977%3,3895e389,1835e-41chuẩn
FP8 E4M310,10156250,0015621,56%4480,0019531chuẩn
FP8 E5M210,093750,006256,25%573441,5259e-5chuẩn
INT8100,1100%1271số không
INT40,500,1100%71số không
Không định dạng nào tràn với số này.
Sập về 0 ở 2 định dạng: INT8, INT4. Số khác 0 mà lưu xong thành 0 thì sai số tương đối là 100%.
Không định dạng nào trong bảng ghi đúng được số này, kể cả FP32. Mọi hàng đều là một phép làm tròn.
Soi kỹ BF16: số của bạn rơi vào giữa hai giá trị nào
0,100097656
giá trị lưu được
chuẩn
0,099609375
số kề dưới
0,100097656
số kề trên
được chọn
0,000488281
bước lưới quanh đây
khoảng cách hai số kề
0,391%
sai số tương đối tệ nhất
trong vùng số chuẩn
2
chữ số thập phân giữ được
16 bit tất cả

Giá trị nằm giữa 0,099609375 và 0,100097656, gần 0,100097656 hơn nên được làm tròn về đó. Khoảng cách giữa hai số kề nhau ở đây là 0,000488281.

Cùng 8 bit mũ với FP32 nên cùng dải, chỉ cắt bớt phần định trị. Đây là lý do nó thắng khi huấn luyện.

Cùng một dáng số ở bảy độ lớn: dấu phẩy động chặn sai số tương đối, số nguyên chặn sai số tuyệt đối
SốFP16BF16INT8
tuyệt đốitương đốituyệt đốitương đốituyệt đốitương đối
0,00174,013e-70,0236%1,355e-60,0797%0,0017100%
0,0171,709e-60,0101%3,223e-50,19%0,017100%
0,174,395e-50,0259%7,813e-50,046%0,17100%
1,70,00019530,0115%0,0031250,184%0,317,6%
17000000
17000004325,3%
17000040,235%157392,5%
Hoá đơn byte cho riêng trọng số của một mô hình 8 tỉ tham số
Định dạngByte mỗi sốTổng trọng sốSo với FP32độ lớn
FP32429,8 GiB1
FP16214,9 GiB0,5
BF16214,9 GiB0,5
FP8 E4M317,5 GiB0,25
FP8 E5M217,5 GiB0,25
INT817,5 GiB0,25
INT40,53,7 GiB0,125

Ba mảnh của một số thực

Mọi định dạng dấu phẩy động trong bảng đều chia bit thành đúng ba mảnh, theo cùng một công thức:

giá trị = (-1)^dấu × 2^(số mũ - bias) × 1,phần định trị

Bit dấu luôn đúng một bit. Số mũ quyết định số đó lớn cỡ nào, tức nó đẩy dấu phẩy đi bao xa. Phần định trị quyết định trong khoảng độ lớn đó thì số được ghi chi tiết tới đâu. Vì luôn có một chữ số 1 đứng trước dấu phẩy nên người ta không lưu nó, và đó là lý do FP32 với 23 bit định trị thật ra ghi được 24 bit ý nghĩa.

Chia bit cho mũ hay cho định trị là một cuộc mặc cả, và nó là toàn bộ nội dung của bài này:

Định dạngBố cụcDải giá trịChữ số thập phân giữ được
FP321 + 8 + 23tới 3,4028e38khoảng 7
FP161 + 5 + 10tới 65.504khoảng 3
BF161 + 8 + 7tới 3,3895e38khoảng 2
FP8 E4M31 + 4 + 3tới 448khoảng 1
FP8 E5M21 + 5 + 2tới 57.344dưới 1

Nhìn cột giữa: BF16 có cùng số bit mũ với FP32 nên có cùng dải giá trị, còn FP16 dừng ở 65.504. Đó không phải chi tiết kỹ thuật vụn vặt, đó là cả câu chuyện.

Chỗ FP16 gãy mà BF16 không gãy

Bấm số mẫu 1e30. Hai hàng 16 bit cho hai kết quả khác hẳn nhau:

  • FP16 ghi ra vô cực. Không phải một số hơi sai, mà là hết số. Cột sai số ghi tràn vì không còn con số nào để nói nó sai bao nhiêu.
  • BF16 ghi ra 1,00025555e30, sai 0,0256%.

Cùng hai byte. Cùng một hoá đơn bộ nhớ tính theo bài trước. Một cái mất trắng, một cái sai chưa tới ba phần vạn.

Vì sao chuyện này quan trọng khi huấn luyện: gradient và các đại lượng trung gian trong lượt truyền ngược trải trên một dải độ lớn rất rộng, và một giá trị tràn thành vô cực sẽ lan ra làm hỏng cả lượt cập nhật, trong khi một giá trị bị làm tròn thô chỉ làm bước đi hơi lệch. Một cái là mất dữ liệu, một cái là nhiễu. Nhiễu thì mạng chịu được, mất dữ liệu thì không. Đó là lý do phần lớn quy trình huấn luyện hiện nay dùng BF16, và cũng là lý do người ta phải phát minh ra kỹ thuật nhân tỉ lệ mất mát khi buộc phải huấn luyện bằng FP16.

Đổi lại, BF16 kém chính xác hơn FP16 đúng 8 lần ở gần số 1, vì nó có ít hơn 3 bit định trị. Không có bữa trưa miễn phí, chỉ có chuyện chọn mất cái nào.

Ranh giới: đúng ở đâu thì tràn

Kéo số về gần biên là chỗ thú vị nhất, vì tràn không xảy ra ngay ở giá trị lớn nhất.

Số lớn nhất FP16 ghi được là 65.504. Nhưng gõ 65505 thì nó không tràn, nó chỉ bị kéo về 65.504, vì 65.505 vẫn gần 65.504 hơn là gần cái mốc tiếp theo. Gõ 65519 cũng vậy. Phải tới đúng 65.520, điểm chính giữa 65.504 và 65.536, thì FP16 mới thành vô cực. Bảng làm đúng luật đó, và bạn thử được cả hai phía.

FP8 E4M3 còn một điểm lạ hơn. Theo đặc tả OCP, nó không có mẫu bit nào cho vô cực: mẫu mũ toàn 1 kèm định trị toàn 1 đã dành cho NaN. Nên khi tràn, E4M3 không ra vô cực mà ra NaN. Gõ 448 thấy nó ghi đúng, gõ 464 thấy nó vẫn về 448 (điểm chính giữa, và luật làm tròn tới số chẵn chọn phía dưới), gõ 465 thì hàng đó thành NaN.

Còn số nguyên thì không tràn kiểu đó. INT8 gặp số ngoài dải thì kẹp về biên: gõ 1e30 nó ghi 127. Không báo vô cực, không báo NaN, chỉ lặng lẽ ghi một số nhỏ xíu như thể đó là câu trả lời. Ba kiểu hỏng, ba cách hỏng khác nhau, và bảng gọi tên từng cái ở cột cuối.

Làm tròn tới số chẵn, và vì sao không phải làm tròn lên

Bấm số mẫu 2049 rồi chọn định dạng đang soi là FP16. Ở quanh 2048, lưới FP16 có bước bằng 2, nên 2049 rơi đúng chính giữa 2048 và 2050. Chọn cái nào?

Luật IEEE 754 là làm tròn tới số chẵn gần nhất: khi hoà, chọn phía có phần định trị chẵn. 2048 có định trị chẵn nên 2049 thành 2048. Nhưng 2051 lại thành 2052, vì lần này 2052 mới là phía chẵn. Nghe tuỳ tiện, nhưng nó có lý do: nếu luôn làm tròn lên khi hoà thì mọi phép cộng dồn sẽ trôi lên trên một cách có hệ thống, và với hàng tỉ phép tính thì cái trôi đó tích lại thành sai lệch thật. Chia đôi cho hai phía thì sai số triệt tiêu lẫn nhau.

Luật này áp cho cả cột số nguyên: 0,5 thành 0, 1,5 thành 2, còn 2,5 thành 2 chứ không phải 3.

Sai số tương đối và sai số tuyệt đối là hai con thú khác nhau

Đây là chỗ trực giác hay lạc nhất, và bảng thứ hai trong sim dựng riêng cho nó. Cùng một dáng số, 1,7 nhân với các luỹ thừa của mười:

  • BF16 giữ sai số tương đối trong một dải hẹp: 0,0797% ở 0,0017 và 0,184% ở 1,7, không bao giờ vượt 0,391%. Nhưng sai số tuyệt đối thì nhảy từ 1,355e-6 lên 0,003125, tức lớn lên cùng với số.
  • INT8 thì ngược hẳn: sai số tuyệt đối không bao giờ quá 0,5, vì lưới của nó có bước cố định bằng 1. Nhưng sai số tương đối bung ra: 0,0017 lưu vào INT8 thành 0, sai 100%.

Lý do là dấu phẩy động có lưới co giãn theo độ lớn (bước lưới quanh 1700 rộng gấp một triệu lần bước lưới quanh 0,0017), còn số nguyên có lưới cố định ở mọi nơi. Trên vector trọng số của một mạng, nơi các giá trị nằm cùng một bậc độ lớn, lưới cố định lại là lợi thế: nó không lãng phí bit cho những bậc độ lớn chẳng có trọng số nào.

Nhưng lưới cố định chỉ giữ được lời hứa khi số còn nằm trong dải. Gõ 170 vào bảng: INT8 kẹp về 127 và sai số tuyệt đối vọt lên 43, gấp 86 lần cái ngưỡng 0,5. Ra khỏi dải thì không còn ràng buộc nào cả.

Hoá đơn byte, tính theo cả mô hình

Bảng cuối nhân số byte mỗi giá trị với số tham số bạn nhập. Với 8 tỉ tham số:

Định dạngTrọng số
FP3229,8 GiB
FP16 hoặc BF1614,9 GiB
FP8 hoặc INT87,5 GiB
INT43,7 GiB

Con số 14,9 GiB chính là con số đã gặp ở bài KV cache, và đó không phải trùng hợp: cùng một phép nhân. Chú ý FP8 và INT8 cho cùng một hoá đơn vì cùng một byte, y như FP16 và BF16 cho cùng một hoá đơn vì cùng hai byte. Chọn giữa chúng không bao giờ là chuyện tiết kiệm bộ nhớ, mà là chuyện biểu diễn được cái gì trong một byte đó.

Cái bảng này không nói gì

Đây là chỗ phải nói thẳng, vì một phép mã hoá tính đúng vẫn có thể bị đọc quá xa.

Sim cho thấy đúng một trọng số lẻ mất gì khi cắt bit. Nó không nói được mô hình kém đi bao nhiêu. Chất lượng sau lượng tử hoá phụ thuộc phân bố trọng số của từng lớp, phụ thuộc cách chọn nhóm để chia tỉ lệ, và phụ thuộc cách xử lý các giá trị ngoại lệ. Một mô hình có thể nuốt INT4 gần như không suy chuyển, một mô hình khác gãy ngay ở INT8. Cái đó phải đo mới biết, không suy ra được từ bảng sai số của một con số.

Cột INT8 và INT4 ở đây là số nguyên thô, không có hệ số tỉ lệ. Lượng tử hoá thật luôn nhân trọng số với một hệ số tỉ lệ riêng cho từng nhóm trước khi làm tròn, nên INT4 thật không hề chỉ ghi được các số nguyên từ -8 tới 7. Cách chia nhóm và chọn hệ số đó là một cơ chế riêng và là một bài riêng; ở đây cột số nguyên chỉ đóng vai một cây thước có vạch cố định, để đối chiếu với cây thước co giãn của dấu phẩy động.

Số dưới chuẩn ở đây không bao giờ bị làm phẳng về 0. Bảng cài đúng vùng dưới chuẩn: dưới mức chuẩn nhỏ nhất, độ chính xác giảm dần chứ không rơi thẳng xuống 0. Phần cứng thật đôi khi bật chế độ làm phẳng số dưới chuẩn về 0 để chạy nhanh hơn, và khi đó các số nhỏ sẽ mất sớm hơn bảng này nói.

Tràn của E4M3 thành NaN là một quy ước, không phải chân lý. Đặc tả OCP quy định như vậy, nhưng một số thư viện chọn kẹp về giá trị lớn nhất thay vì trả NaN. Bảng theo đặc tả, và nói ra để bạn không ngạc nhiên khi thấy thư viện làm khác.

Số bạn gõ đã đi qua một lần làm tròn trước khi tới bảng. Trình duyệt giữ số ở kiểu 64 bit, nên 0,1 mà bạn gõ thật ra đã không đúng một phần mười ngay từ đầu. Với mọi con số trong bài thì sai lệch đó nhỏ hơn sai số của các định dạng 16 bit hàng chục bậc, nhưng nó có thật, và gõ 1e400 thì bạn thấy nó ngay: vô cực xuất hiện trước khi bảng kịp làm gì.

Điều rút ra

Một định dạng số là một cách chia bit giữa dảiđộ chính xác. Bit mũ mua dải, bit định trị mua độ chính xác, và tổng số bit thì cố định. BF16 thắng FP16 khi huấn luyện không phải vì nó tốt hơn, mà vì nó chọn mất độ chính xác thay vì mất dải, và mất độ chính xác thì chỉ là nhiễu trong khi mất dải là mất hẳn số. Với dấu phẩy động, sai số tương đối bị chặn trên toàn dải; với số nguyên, sai số tuyệt đối bị chặn nhưng chỉ trong dải. Còn số byte thì chỉ phụ thuộc độ rộng: FP8 và INT8 tốn y như nhau, khác nhau ở chỗ chúng mua được gì bằng tám bit đó.

Câu hỏi tự kiểm0/3 đúngchưa trả lời
  1. 1Bạn lưu giá trị 1e30 vào FP16. Kết quả là gì?
  2. 2FP16 và BF16 cùng chiếm hai byte. Vì sao các quy trình huấn luyện hiện đại phần lớn chọn BF16?
  3. 3Bạn lưu 0,0017 rồi lưu 1,7, mỗi số bằng BF16 và bằng INT8. Phát biểu nào đúng?