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

Lượng tử hoá theo nhóm, và một ngoại lệ đủ phá cả nhóm

Vector sửa đượcKế toán từng bitTính lại thật

Lượng tử hoá theo nhóm, và một ngoại lệ đủ phá cả nhóm

Cắt trọng số từ 16 bit xuống 4 bit nghe như tiết kiệm bốn lần. Không phải, và phần chênh lệch nằm ở chỗ gần như không ai kể: mỗi nhóm trọng số còn kéo theo một hệ số tỉ lệ phải lưu.

Bài định dạng số nói về việc một con số được xếp bit như thế nào. Bài này bắt đầu từ chỗ đó và hỏi câu tiếp theo, câu thực sự quyết định một mô hình lượng tử hoá nặng bao nhiêu và tệ đi bao nhiêu: một hệ số tỉ lệ nên dùng chung cho bao nhiêu trọng số?

Ý tưởng thì đơn giản. Trọng số của mạng nơ ron là số thực nằm quanh 0, thường trong khoảng vài phần mười. Muốn lưu chúng bằng số nguyên 4 bit, bạn cần một hệ số quy đổi: chia trọng số cho hệ số đó, làm tròn thành số nguyên, và khi dùng thì nhân ngược lại. Câu hỏi là hệ số đó tính trên phạm vi nào. Cả tầng? Từng cột? Từng 128 trọng số liền nhau? Mỗi lựa chọn cho một sai số khác nhau một dung lượng khác nhau, và hai thứ đó đi ngược chiều nhau.

Bảng tính dưới đây cho bạn đặt vector trọng số, số bit, cỡ nhóm và cách xử lý ngoại lệ, rồi tính lại từng con số.

Một hệ số tỉ lệ dùng chung cho bao nhiêu trọng số
Sai số bp tb 0,001177Byte 40Nén thực tế 3,20×
Vector mẫu:
Phân bố quanh 0: 64 số sinh bằng công thức tất định 0,6·sin(2,1i) + 0,3·sin(5,3i+1) + 0,1·sin(11,7i+2), làm tròn 3 chữ số thập phân. Đây là vector minh hoạ, không phải trọng số của mô hình nào.
Vector trọng số (64 số)✎ sửa được
Ngăn cách bằng dấu phẩy hoặc khoảng trắng, dấu thập phân là dấu chấm. Tối đa 256 số.
Cách lượng tử hoá✎ sửa được
số bit mỗi mã
độ rộng của mã nguyên, càng ít bit càng nhỏ và càng sai
cỡ nhóm
bao nhiêu trọng số dùng chung một hệ số tỉ lệ
giữ riêng k phần tử lớn nhất
k trọng số lớn nhất được giữ nguyên FP16, 0 là tắt
ngân sách bit mỗi trọng số
trần chi phí trung bình cho mỗi trọng số, dùng cho bảng cuối
dải mã nguyêntừ -7 tới 7, tức 15 mức
tổng chi phí lưu64 × 4 + 4 × 16 = 320 bit
bit mỗi trọng số320 / 64 = 5,000 bit
0,001177
sai số bình phương trung bình
trên 64 số
0,063714
sai số lớn nhất
một phần tử tệ nhất
40
byte sau khi nén
gốc 128 byte
3,20×
tỉ lệ nén thực tế
nếu quên chi phí: 4,00×
5,00
bit mỗi trọng số
4 bit
4
số nhóm
hệ số tỉ lệ 64 bit
Sai số và số byte, cùng một trục hoành là cỡ nhóm
bản gốc FP16 128 byterẻ nhất trong ngân sách124816326400,0004250,0008490,0012740,00169804488132176cỡ nhóm (số trọng số dùng chung một hệ số tỉ lệ)sai số bình phương trung bìnhbyte sau khi nén
■ sai số bp tb (trục trái)■ byte sau nén (trục phải)
Sai số của từng nhóm. Một ngoại lệ chỉ đội cột của nhóm chứa nó
01230,0017200số hiệu nhóm
Bảng phần tử chỉ hiện 24 cột đầu trong tổng 64. Mọi con số tổng hợp vẫn tính trên toàn bộ vector.
Đang chạm sàn: giữ riêng k phần tử lớn nhất = 0.
Từng nhóm: hệ số tỉ lệ riêng và sai số riêng
NhómCỡDải nhómHệ số tỉ lệĐiểm khôngGiữ riêngSai số bp tbSai số lớn nhất
016-0,743 tới 0,7040,743 / 7 = 0,106143000,0007160,051000
116-0,678 tới 0,7670,767 / 7 = 0,109571000,0008470,052857
216-0,960 tới 0,7640,960 / 7 = 0,137143000,0014250,063714
316-0,802 tới 0,6370,802 / 7 = 0,114571000,0017200,056429
Từng phần tử: trọng số, mã nguyên, giá trị giải lượng tử, sai số
chỉ số01234567891011121314151617181920212223
trọng số0,3430,614-0,743-0,3250,351-0,3900,3210,704-0,600-0,2230,297-0,5180,2490,692-0,536-0,1270,307-0,5600,2250,666-0,532-0,0970,283-0,585
mã nguyên36-7-33-437-6-23-527-5-13-526-5-13-5
giải lượng tử0,3180,637-0,743-0,3180,318-0,4250,3180,743-0,637-0,2120,318-0,5310,2120,743-0,531-0,1060,329-0,5480,2190,657-0,548-0,1100,329-0,548
sai số-0,0250,0230,0000,007-0,033-0,035-0,0030,039-0,0370,0110,021-0,013-0,0370,0510,0050,0210,0220,012-0,006-0,009-0,016-0,0130,0460,037
Cỡ nhóm rẻ nhất còn nằm dưới ngân sách 5,00 bit mỗi trọng số, theo từng độ rộng mã
Số bitCỡ nhóm tốt nhấtBit mỗi trọng sốSai số bp tbByte
284,0000,04298032
385,0000,00509340
4165,0000,00117740
5không cỡ nào vừa---
6không cỡ nào vừa---
8không cỡ nào vừa---
Bảng trên quét các độ rộng mã 2, 3, 4, 5, 6, 8 bit. Mỗi nhóm tốn thêm 16 bit hệ số tỉ lệ, và ở chế độ bất đối xứng thêm 16 bit điểm không, nên mã càng rộng thì càng phải dùng nhóm lớn để còn nằm trong ngân sách.

Hệ số tỉ lệ theo nhóm là gì

Chia vector trọng số thành các nhóm liền nhau cỡ g. Mỗi nhóm tự tính hệ số tỉ lệ của riêng nó từ giá trị lớn nhất trong nhóm:

hệ số tỉ lệ của nhóm = max(|w| trong nhóm) / (2^(n-1) - 1)
mã nguyên = làm tròn(w / hệ số tỉ lệ)
giá trị giải lượng tử = mã nguyên × hệ số tỉ lệ

Với n = 4 bit, mẫu số là 2^3 - 1 = 7, tức mã chạy từ -7 tới 7. Trọng số lớn nhất của nhóm luôn ánh xạ đúng vào mã 7, còn những trọng số nhỏ hơn thì rơi vào các mức ở giữa và mất một ít trong lúc làm tròn.

Chỗ quan trọng: mỗi nhóm phải lưu hệ số tỉ lệ của nó. Bảng này lưu hệ số ở FP16, tức 16 bit một cái. Nghe nhỏ, nhưng chia cho cỡ nhóm thì nó thành một khoản phụ thu cố định trên mỗi trọng số:

bit mỗi trọng số = n + 16 / g

Đây là dòng làm hỏng mọi phép ước lượng kiểu "4 bit thì nhỏ hơn bốn lần".

Đọc con số ở trạng thái mặc định

Bảng mở ra với 64 trọng số phân bố quanh 0, mã 4 bit, nhóm 16, chế độ đối xứng.

64 × 4 + 4 × 16 = 320 bit

Tức 256 bit cho mã và 64 bit chỉ để lưu bốn hệ số tỉ lệ. Chia cho 64 trọng số được 5 bit mỗi trọng số, không phải 4. Bản gốc FP16 là 128 byte, dạng nén là 40 byte, nên tỉ lệ nén thực tế là 3,20× chứ không phải 4,00× như con số 16 chia 4 hứa hẹn. Ô "nếu quên chi phí" trong sim ghi đúng cái con số hứa hẹn đó để bạn so.

Sai số bình phương trung bình ra 0,001177 và sai số lớn nhất là 0,063714. Nhìn bảng nhóm sẽ thấy bốn nhóm không giống nhau: nhóm 0 có trọng số lớn nhất là 0,743 nên hệ số tỉ lệ 0,743 chia 7, còn nhóm 2 có 0,96 nên hệ số của nó lớn hơn. Nhóm nào có trọng số lớn nhất càng lớn thì bước lượng tử càng thô, và sai số của nhóm đó càng cao: 0,000716 ở nhóm 0 so với 0,001425 ở nhóm 2.

Đó chính là lý do người ta chia nhóm ngay từ đầu. Một hệ số dùng chung cho cả tầng thì bị quyết định bởi trọng số lớn nhất của cả tầng, và mọi trọng số nhỏ ở nơi khác phải chịu bước lượng tử thô của nó.

Nhóm nhỏ hơn thì chính xác hơn, và bạn trả bằng byte

Kéo thanh cỡ nhóm đi hết một lượt, hoặc nhìn thẳng vào biểu đồ hai trục. Đây là toàn bộ dải, ở 4 bit, chế độ đối xứng, trên đúng vector mặc định:

Cỡ nhómSố nhómBit mỗi trọng sốByteNén thực tếSai số bp tb
164201600,80×2,29·10⁻³⁴
23212961,33×0,000359
4168642,00×0,000690
886482,67×0,000934
1645403,20×0,001177
3224,5363,56×0,001215
6414,25343,76×0,001544

Hàng đầu tiên là chỗ đáng dừng lại. Ở cỡ nhóm 1, mỗi trọng số có hệ số tỉ lệ của riêng nó, nên max(|w|) chính là |w|, mã luôn bằng 7 và giá trị giải lượng tử trùng khít trọng số gốc. Sai số về 0, chính xác hơn thì về 2,29·10⁻³⁴, tức chỉ còn nhiễu dấu phẩy động của máy chứ không còn sai số lượng tử hoá nào. Và tệp thành 160 byte, lớn hơn cả bản gốc 128 byte, tỉ lệ nén 0,80×. Bạn vừa nén một cách hoàn hảo và làm dữ liệu phình ra. Sim báo thẳng chuyện đó bằng một dòng cảnh báo chứ không để bạn đọc nhầm.

Ngược lại, một nhóm duy nhất cho cả vector cho 4,25 bit mỗi trọng số, gần sát con số 4 bit lý tưởng, nhưng sai số cao hơn 31% so với nhóm 16.

Nói cách khác, cỡ nhóm không có đáp án tuyệt đối. Nó chỉ có đáp án khi bạn nói rõ mình chịu chi bao nhiêu.

Cỡ nhóm tốt nhất phụ thuộc số bit

Đặt một trần chi phí, ví dụ 5 bit mỗi trọng số, rồi hỏi: ở mỗi độ rộng mã, cỡ nhóm nhỏ nhất nào còn nằm dưới trần đó? Vì chi phí là n + 16/g, điều kiện là g >= 16 / (5 - n). Bảng cuối của sim tính đúng phép đó:

Số bitCỡ nhóm tốt nhấtBit mỗi trọng sốSai số bp tb
2840,042980
3850,005093
41650,001177
5không cỡ nào vừa
6không cỡ nào vừa
8không cỡ nào vừa

Cỡ nhóm tối ưu dịch từ 8 lên 16 khi mã rộng ra, vì mã càng rộng thì càng ít dư địa cho phần phụ thu. Và từ 5 bit trở lên thì không cỡ nhóm nào cứu nổi: 5 + 16/64 = 5,25, vẫn quá trần. Trong ba dòng còn khả thi, mã 4 bit với nhóm 16 thắng, và đó đúng là cấu hình mặc định của bảng.

Thử hạ trần xuống 4,25 thì mã 4 bit chỉ còn sống được với một nhóm duy nhất. Hạ thêm một chút nữa, xuống 4,24, thì mã 4 bit rớt hẳn và mã 3 bit lên thay. Ranh giới sắc như vậy vì đây là số học chứ không phải cảm tính.

Một trọng số ngoại lệ phá cả nhóm

Đây là điểm quan trọng nhất của bài, và nó là lý do tồn tại của gần như mọi phương pháp lượng tử hoá hiện đại.

Bấm nút đặt một ngoại lệ vào phần tử 20. Một số duy nhất trong 64 số vừa nhảy từ -0,532 lên 8. Nhìn bảng nhóm:

NhómSai số trướcSai số sau
00,0007160,000716
10,0008470,128961
20,0014250,001425
30,0017200,001720

Ba nhóm không đổi một chữ số. Nhóm 1, nhóm chứa phần tử 20, tệ đi 152 lần. Sai số của cả vector từ 0,001177 lên 0,033206, tức 28 lần tệ hơn, chỉ vì một con số trong sáu mươi tư.

Cơ chế thì hiển nhiên khi viết ra: hệ số tỉ lệ của nhóm 1 nhảy từ 0,767 chia 7 lên 8 chia 7, tức từ 0,110 lên 1,143. Bước lượng tử vừa to lên hơn mười lần, mà mười lăm trọng số còn lại trong nhóm vẫn nhỏ như cũ. Chúng bị ép vào một lưới quá thô so với chúng, và mất gần hết thông tin.

Cỡ nhóm chính là bán kính thiệt hại. Nhóm 16 thì một ngoại lệ làm hỏng 16 trọng số. Một hệ số cho cả tầng thì nó làm hỏng cả tầng.

Trọng số thật của mô hình lớn có chuyện này. Một số kênh trong mạng mang giá trị lớn hơn hẳn phần còn lại, và chúng không phải nhiễu, chúng cần thiết. Không cắt bỏ được.

Cách chữa: giữ riêng vài phần tử lớn nhất

Kéo thanh giữ riêng k phần tử lớn nhất lên 1. Sim tìm trọng số có độ lớn cao nhất, lấy nó ra khỏi quá trình lượng tử hoá, lưu nguyên ở FP16, và tính lại hệ số tỉ lệ của nhóm trên phần còn lại.

Nhóm 1 lập tức trở về sai số 0,000831, tức tốt hơn 155 lần so với lúc để nguyên. Sai số của cả vector về 0,001173, thậm chí còn thấp hơn vector sạch ban đầu, vì bây giờ phần tử 20 được lưu chính xác tuyệt đối.

Cái giá thì phải nói cho đủ, và đây là chỗ hay bị bỏ quên nhất:

63 × 4 + 4 × 16 + 1 × (16 + 6) = 338 bit

Số hạng cuối là chi phí của một ngoại lệ được giữ riêng: 16 bit cho giá trị, cộng 6 bit cho chỉ số. Bộ giải mã phải biết trọng số đó nằm ở đâu mới đặt lại đúng chỗ được, và với 64 trọng số thì một chỉ số cần 6 bit. Tệp từ 40 byte lên 43 byte, tỉ lệ nén từ 3,20× xuống 2,98×. Ba byte để lấy lại 28 lần sai số. Rẻ.

Người ta hay khoe cách chữa này mà chỉ đếm phần giá trị, không đếm phần chỉ số. Với k nhỏ thì chênh lệch không đáng kể, nhưng nó không bằng 0, và một bảng ước lượng bỏ nó đi thì luôn lạc quan hơn thực tế.

Ý tưởng nền của các phương pháp lượng tử hoá hiện đại chính là đây, chỉ khác ở cách chọn cái gì đáng giữ riêng và giữ ở dạng nào: có phương pháp giữ nguyên cả một số kênh, có phương pháp không giữ mà nhân trước một hệ số để kéo ngoại lệ về gần phần còn lại, có phương pháp tách ma trận thành một phần thưa chứa ngoại lệ và một phần dày đã lượng tử hoá. Cả ba đều xuất phát từ cùng một quan sát mà bạn vừa nhìn thấy bằng số.

Đối xứng hay bất đối xứng

Chế độ đối xứng chỉ lưu một hệ số tỉ lệ cho mỗi nhóm và giả định dải giá trị cân xứng quanh 0. Chế độ bất đối xứng lưu thêm một số thứ hai, điểm không, tức giá trị mà mã 0 giải ra, rồi trải các mức từ giá trị nhỏ nhất tới giá trị lớn nhất của nhóm.

Trên vector mặc định, phân bố vốn đã quanh 0, bất đối xứng chỉ tốt hơn 1,7 lần mà tốn thêm 8 byte. Không đáng.

Bấm preset lệch hẳn về dương, nơi mọi giá trị nằm giữa 0,05 và 0,997. Bây giờ đối xứng phải trải mã của nó từ -7 tới 7 trong khi không có số âm nào tồn tại, tức vứt đi đúng một nửa số mức:

Chế độByteSai số bp tb
đối xứng, nhóm 16400,002245
bất đối xứng, nhóm 16480,000223
đối xứng, nhóm 8480,002101

Hàng thứ ba là phép so công bằng: cùng 48 byte, đối xứng dùng nhóm nhỏ hơn để bù, và vẫn thua 9,4 lần. Khi phân bố lệch, tiền tiêu vào điểm không đáng hơn nhiều so với tiêu vào nhóm nhỏ.

Ngược lại, quay về vector mặc định rồi hạ mã xuống 1 bit thì đối xứng sụp hoàn toàn: dải mã co về đúng {0} nên mọi trọng số giải ra 0 và sai số bằng chính trọng số, 0,230387. Bất đối xứng ở 1 bit vẫn còn hai mã và cho 0,171996. Sim báo đúng ca suy biến đó bằng chữ thay vì trả về một số vô nghĩa.

Bảng này không nói gì

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

Nó đo sai số trên chính vector này, không đo chất lượng mô hình. Đây là giới hạn lớn nhất và không có cách nào vá được trong trình duyệt. Sai số trọng số nhỏ không bảo đảm mô hình giữ chất lượng. Cái thực sự quan trọng là sai số đó truyền qua nhiều lớp thành sai số ở đầu ra, và mức khuếch đại phụ thuộc vào trọng số của các lớp sau lẫn vào chính dữ liệu chạy qua. Có những trọng số sai nhiều mà mô hình không hề hấn, và có những trọng số sai rất ít mà đủ làm hỏng một hành vi. Muốn biết thì phải chạy mô hình thật trên dữ liệu thật, và trang này không làm được việc đó.

Nó không có dữ liệu hiệu chuẩn. Các phương pháp thật không chỉ chia nhóm rồi làm tròn. Chúng chạy một tập dữ liệu hiệu chuẩn qua mô hình, đo xem trọng số nào ảnh hưởng nhiều tới đầu ra, rồi chọn hướng làm tròn theo đó, thậm chí sửa các trọng số chưa lượng tử hoá để bù cho sai số vừa gây ra. Sim này làm tròn về mức gần nhất, đơn thuần và mù, không mô hình hoá gì trong số đó. Nên đừng đọc con số ở đây như dự báo cho một phương pháp có tên.

Cách tính byte là quy ước, không phải chân lý. Bài này lưu hệ số tỉ lệ ở FP16 16 bit, điểm không cũng 16 bit, ngoại lệ 16 bit cộng một chỉ số. Có bộ nhân thật đóng gói điểm không ở đúng độ rộng mã cho gọn, có bộ lưu ngoại lệ thành ma trận thưa với cách đánh chỉ số khác. Những lựa chọn đó đổi cột byte, không đổi hình dạng câu chuyện. Cách làm tròn nửa ra xa 0 cũng là một quy ước: Math.round của JavaScript đưa -3,5 về -3, tức thiên vị mọi trọng số âm, nên bài dùng cách khác và ghi rõ.

Một quy tắc nghe như định lý mà không phải. "Nhóm nhỏ hơn thì sai số không bao giờ tăng" nghe rất thuận tai và sai. Cái đúng là: chia nhỏ nhóm thì hệ số tỉ lệ của nhóm không bao giờ tăng, và sai số của một nhóm không suy biến không bao giờ vượt quá một nửa hệ số tỉ lệ của nó. Cả hai điều đó cổng kiểm số có canh. Nhưng bản thân sai số bình phương trung bình thì làm tròn không đơn điệu theo hệ số, nên nó có thể nhích lên khi bạn chia nhỏ nhóm. Ví dụ thật lấy từ chính preset của bài: vector lệch, chế độ bất đối xứng, 3 bit, nhóm 32 cho sai số 0,001247 trong khi nhóm 16 cho 0,001277, tức nhóm to hơn vừa rẻ hơn vừa chính xác hơn. Ở cấu hình mặc định thì đường cong đi đúng chiều trực giác, nên biểu đồ trong bài trung thực, nhưng ngoài đó thì không có gì bảo đảm.

Vector trong bài là vector bịa. Cả năm preset đều sinh bằng công thức tất định ghi ngay trên giao diện, không phải trọng số bóc từ mô hình nào. Chúng được chọn để làm nổi một hiện tượng, không phải để đại diện cho phân bố trọng số thật. Bạn dán vector của mình vào là kiểm lại được ngay. Cổng kiểm số của bài canh phép tính, tức với vector và cấu hình như vậy thì con số phải là như vậy; nó không canh và không thể canh việc phương pháp nào ngoài kia hành xử ra sao.

Điều rút ra

Lượng tử hoá theo nhóm là một bài toán kế toán có hai vế. Vế sai số muốn nhóm nhỏ, vế dung lượng muốn nhóm to, và chi phí thật là n + 16/g bit mỗi trọng số chứ không phải n. Cỡ nhóm tối ưu chỉ tồn tại khi bạn nói rõ trần chi phí, và nó dịch lên khi mã rộng ra. Còn thứ quyết định chất lượng nhiều nhất lại không phải cỡ nhóm mà là ngoại lệ: một trọng số vọt lên là đủ đội hệ số tỉ lệ của cả nhóm và làm hỏng mọi trọng số nằm cùng nhóm với nó. Giữ riêng vài phần tử lớn nhất ở độ chính xác cao, kèm chỉ số của chúng, gần như xoá sạch thiệt hại với vài byte. Đó là ý tưởng nền của gần như mọi phương pháp lượng tử hoá hiện đại.

Câu hỏi tự kiểm0/3 đúngchưa trả lời
  1. 1Ở trạng thái mặc định, 64 trọng số, mã 4 bit, nhóm 16, sim ghi tỉ lệ nén thực tế là 3,20× trong khi 16 chia 4 bằng 4. Chênh lệch đó đi đâu?
  2. 2Đặt cỡ nhóm bằng 1 thì mỗi trọng số có hệ số tỉ lệ riêng và sai số về 0. Vì sao không ai làm vậy?
  3. 3Bạn giữ riêng 1 trọng số lớn nhất ở FP16 để cứu nhóm bị ngoại lệ phá. Chi phí thật của việc đó là gì?