Một bit dùng vào việc gì
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.
1e30| Định dạng | Byte | Giá trị lưu được | Sai số tuyệt đối | Sai số tương đối | Lớn nhất | Dương nhỏ nhất | Loại |
|---|---|---|---|---|---|---|---|
| FP32 | 4 | 0,100000001 | 1,49e-9 | 1,49e-6% | 3,4028e38 | 1,4013e-45 | chuẩn |
| FP16 | 2 | 0,0999755859 | 2,441e-5 | 0,0244% | 65504 | 5,9605e-8 | chuẩn |
| BF16 | 2 | 0,100097656 | 9,766e-5 | 0,0977% | 3,3895e38 | 9,1835e-41 | chuẩn |
| FP8 E4M3 | 1 | 0,1015625 | 0,001562 | 1,56% | 448 | 0,0019531 | chuẩn |
| FP8 E5M2 | 1 | 0,09375 | 0,00625 | 6,25% | 57344 | 1,5259e-5 | chuẩn |
| INT8 | 1 | 0 | 0,1 | 100% | 127 | 1 | số không |
| INT4 | 0,5 | 0 | 0,1 | 100% | 7 | 1 | số không |
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.
| Số | FP16 | BF16 | INT8 | |||
|---|---|---|---|---|---|---|
| tuyệt đối | tương đối | tuyệt đối | tương đối | tuyệt đối | tương đối | |
| 0,0017 | 4,013e-7 | 0,0236% | 1,355e-6 | 0,0797% | 0,0017 | 100% |
| 0,017 | 1,709e-6 | 0,0101% | 3,223e-5 | 0,19% | 0,017 | 100% |
| 0,17 | 4,395e-5 | 0,0259% | 7,813e-5 | 0,046% | 0,17 | 100% |
| 1,7 | 0,0001953 | 0,0115% | 0,003125 | 0,184% | 0,3 | 17,6% |
| 17 | 0 | 0 | 0 | 0 | 0 | 0 |
| 170 | 0 | 0 | 0 | 0 | 43 | 25,3% |
| 1700 | 0 | 0 | 4 | 0,235% | 1573 | 92,5% |
| Định dạng | Byte mỗi số | Tổng trọng số | So với FP32 | độ lớn |
|---|---|---|---|---|
| FP32 | 4 | 29,8 GiB | 1 | |
| FP16 | 2 | 14,9 GiB | 0,5 | |
| BF16 | 2 | 14,9 GiB | 0,5 | |
| FP8 E4M3 | 1 | 7,5 GiB | 0,25 | |
| FP8 E5M2 | 1 | 7,5 GiB | 0,25 | |
| INT8 | 1 | 7,5 GiB | 0,25 | |
| INT4 | 0,5 | 3,7 GiB | 0,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ạng | Bố cục | Dải giá trị | Chữ số thập phân giữ được |
|---|---|---|---|
| FP32 | 1 + 8 + 23 | tới 3,4028e38 | khoảng 7 |
| FP16 | 1 + 5 + 10 | tới 65.504 | khoảng 3 |
| BF16 | 1 + 8 + 7 | tới 3,3895e38 | khoảng 2 |
| FP8 E4M3 | 1 + 4 + 3 | tới 448 | khoảng 1 |
| FP8 E5M2 | 1 + 5 + 2 | tới 57.344 | dướ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ànvì 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ạng | Trọng số |
|---|---|
| FP32 | 29,8 GiB |
| FP16 hoặc BF16 | 14,9 GiB |
| FP8 hoặc INT8 | 7,5 GiB |
| INT4 | 3,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ì.
Một định dạng số là một cách chia bit giữa dải và độ 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 đó.
- 1Bạn lưu giá trị 1e30 vào FP16. Kết quả là gì?
- 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?
- 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?