Chuyển đến nội dung
13/30Chương 13 trên 30

Làm inference rẻ hơn: KV cache, batching và lượng tử hóa

Cùng model trả lời trong 8,8 giây và 78,9 giây, output y hệt từng byte. INT4 được đo ba cách, không chỉ khẳng định.

Trên trang này

Cùng một model, trên cùng một máy, trả lời cùng một câu hỏi với cùng 48 token. Hai output giống hệt nhau từng token — đã kiểm tra, không giả định.

TEXT
with a key-value cache:     8.85 s   ( 6.01 tokens/second)
without a key-value cache: 78.95 s   ( 0.60 tokens/second)

Chỉ một đối số thay đổi: use_cache=False. Không có gì về model, prompt, sampling hay phép tính số học khác đi, và lần chạy thứ hai không chính xác hơn để bù cho công sức đó. Nó chậm hơn chín lần mà chẳng được gì.

Đó là hình dạng của chương này. Mọi thứ trong đó — cache, batch, trọng số đã lượng tử hóa — đều là một nỗ lực để ngừng trả tiền cho phần việc không làm thay đổi câu trả lời, hoặc để biết một câu trả lời rẻ hơn phải trả giá gì. Chương 10 đã lập bảng giá cho training. Đây là bảng giá cho phía bạn phải trả mãi mãi: một model đã triển khai tiêu tốn khoảng 2N2N FLOPs cho mỗi token nó phát ra, trên mọi request, trong suốt phần đời còn lại.

Thời gian của lần chạy thứ hai đã đi đâu

Liên kết đến mục: Thời gian của lần chạy thứ hai đã đi đâu

Để sinh một token, một decoder-only transformer nhận toàn bộ chuỗi cho đến lúc đó, chạy nó qua mọi layer, rồi đọc phân phối xác suất ở vị trí cuối. Sau đó nó nối token đã chọn vào và làm lại. Mô tả đó đúng, và đó là điều lần chạy chậm thực hiện.

Nó cũng cực kỳ lãng phí, và lý do là causal mask từ Chương 9. Vector key và value của vị trí 7 được tính từ input của vị trí 7 và các vị trí trước nó. Khi vị trí 8 xuất hiện, vị trí 7 không thể nhìn thấy nó — đó là ý nghĩa của causal — nên key và value của vị trí 7 là chính xác cùng những con số như trước. Lần chạy chậm vẫn tính lại chúng ở mọi bước.

Vậy hãy lưu chúng lại. Kho lưu đó là key-value cache, tối ưu quan trọng nhất trong serving language model:

generate.pyPYTHON
out = model(prompt_ids, use_cache=True)          # prefill: the whole prompt
past = out.past_key_values                        
nxt = out.logits[:, -1].argmax(-1, keepdim=True)

for _ in range(n - 1):
    out = model(nxt, past_key_values=past, use_cache=True)   
    past = out.past_key_values                                
    nxt = out.logits[:, -1].argmax(-1, keepdim=True)

Hãy nhìn thứ được đưa vào model bên trong vòng lặp: nxt, một token. Không phải cả chuỗi. Query của token mới attends với mọi key đã cache, và các key đã cache vốn sẽ không bao giờ thay đổi. Đây không phải một phép xấp xỉ — kiểm tra output giống hệt ở trên chính là điểm mấu chốt. Cache không đánh đổi chất lượng lấy tốc độ; nó xóa phép tính thừa.

Để thấy scaling một cách sạch sẽ, bỏ transformer ra và đo thời gian một attention head đơn với d=64d = 64, một bước generation được tính theo cả hai cách:

token trong contexttính lại mọi thứvới cachetỷ lệma trận score
1280.59 ms0.062 ms10x65,536 B vs 512 B
2561.20 ms0.163 ms7x262,144 B vs 1,024 B
5127.03 ms0.078 ms90x1,048,576 B vs 2,048 B
102417.31 ms0.114 ms152x4,194,304 B vs 4,096 B
204859.83 ms0.214 ms279x16,777,216 B vs 8,192 B
4096236.18 ms0.284 ms832x67,108,864 B vs 16,384 B

Cột bên phải là nguyên nhân. Tính lại sẽ dựng toàn bộ ma trận attention n×nn \times n ở mỗi bước — O(n2)O(n^2) từ hộp ký hiệu tiệm cận của Chương 9, trả một lần cho mỗi token. Với cache, bạn chỉ dựng một hàng 1×n1 \times n: ở 4.096 token, 67 MB score so với 16 KB.

Đếm multiply-accumulates thay vì mili giây sẽ loại máy khỏi lập luận. Để sinh TT token từ cold start:

token được sinhvới cachetính lạitỷ lệ
1282.6 M192.0 M73x
51223.1 M7.36 G318x
2048293.7 M392.6 G1,336x

Mỗi bước, phiên bản có cache tuyến tính theo context còn phiên bản không cache là bậc hai; cộng trên toàn bộ một generation, O(T2)O(T^2) so với O(T3)O(T^3), với tỷ lệ tăng không giới hạn. Chênh lệch chín lần ở phần mở đầu được đo trên 48 token — còn ngắn hơn hàng đầu tiên của bảng đó.

Cache cũng thay đổi thứ phải nằm trong memory. Trên GPU laptop 8 GB sinh 256 token ở fp16, lấy đỉnh của allocator rồi trừ trọng số thường trú:

peak working memory
với cache21.8 MB
tính lại181.7 MB

Tốn memory hơn 8,3 lần, để tạo ra cùng những token đó nhưng chậm hơn. Đây là lời hứa trong Chương 5, đến từ một hướng không ngờ: ở đó, reverse-mode autodiff phải giữ mọi trung gian sống cho backward pass, và activations chi phối memory training. Ở inference không có backward pass và không cần giữ gì cho nó — nên thứ chi phối memory thay vào đó là cache, và nó là một lựa chọn có chủ ý chứ không phải chi phí không thể tránh.

Nhìn lại lần chạy nhanh: token đầu tiên của nó cư xử khác bốn mươi bảy token còn lại.

TEXT
prefill, 40 prompt tokens : 1.0224 s   ->  25.6 ms per token
decode,  47 steps         : 0.1665 s mean per step

Prompt tốn 25,6 ms mỗi token và mỗi token được sinh tốn 166 ms. Cùng model, cùng phần cứng, cùng trọng số, chênh lệch sáu lần mỗi token — và theo hướng hầu hết mọi người không ngờ. Prompt là phần rẻ. Generation tách thành hai pha với vật lý thật sự khác nhau:

Một forward pass trên toàn bộ prompt. Mọi token được xử lý song song, nên mỗi ma trận trọng số được tải từ memory một lần và nhân với một ma trận gồm hàng trăm vector token — một phép nhân ma trận-ma trận, với rất nhiều phép tính trên mỗi byte được di chuyển, đúng thứ GPU được tạo ra để làm. Prefill bị compute-bound, và chi phí của nó gần tuyến tính theo độ dài prompt.

Một forward pass cho mỗi token, batch một và chuỗi một. Mỗi ma trận trọng số vẫn được tải đầy đủ từ memory, và nhân với một vector duy nhất — một phép nhân ma trận-vector, gần như không có phép tính nào trên mỗi byte được di chuyển. Decode bị memory-bandwidth-bound, và chi phí mỗi token hầu như không phụ thuộc vào độ dài context.

Cả hai nửa đều đo được. Prefill, một pass trên PP token:

prompt tokengiâyms mỗi token
160.351521.97
320.525416.42
641.049116.39
1281.655212.93
2563.096512.10

Decode, một token đối với cache CC:

token đã cachems cho một token
16110.05
6497.57
256108.53
1024103.86

Hãy đọc bảng thứ hai hai lần. Đi từ 16 token context lên 1.024 — lịch sử để attend nhiều hơn sáu mươi tư lần — không làm chi phí một bước thay đổi theo cách đo được. Attention đối với cache là công việc thật, nhưng nó bị chi phí cố định của việc kéo nửa tỷ trọng số qua memory bus để tạo ra một vector lấn át. Chi phí cố định đó là lý do cho mọi thứ ở phần tiếp theo.

Hai pha này là nguồn gốc của hai con số mà mọi serving system báo cáo. Time to first token về cơ bản là prefill, và nó tăng theo prompt, đó là lý do một cuộc trò chuyện dài tạo cảm giác chậm lúc bắt đầu. Tokens per second1/decode step1/\text{decode step}, và nó gần như hằng định, đó là lý do câu trả lời sau đó chảy đều. Một chat bắt đầu chậm rồi stream mượt không phải mẹo render. Nó chính là hai bảng này.

Cache đổi phép tính lấy memory, và memory nó muốn không hề nhỏ. Với mỗi token trong context, mỗi layer giữ một vector key và một vector value cho mỗi key-value head:

bytes per token=2×L×Hkv×dhead×bytes per element\text{bytes per token} = 2 \times L \times H_{kv} \times d_{\text{head}} \times \text{bytes per element}

Số 2 là cho keys và values; mọi thứ còn lại là kiến trúc. Với model được đo xuyên suốt chương này — 24 layer, 14 query head, 2 key-value head, head dimension 64 — ở fp16, con số đó là 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 byte mỗi token.

Các công thức trong lĩnh vực này có thói quen lệch một hệ số hai, nên hãy kiểm tra với allocator thay vì tin nó:

TEXT
KV cache tensors per layer: (1, 2, 295, 64) float16
measured: 3,624,960 bytes for 295 tokens = 12,288 bytes/token
formula : 2 * 24 * 2 * 64 * 2                = 12,288 bytes/token

Chính xác, và vẫn chính xác trên mọi shape đã thử:

batchcontextcache đo đượcdự đoánpeak working memory
15126.0 MB6.0 MB15.4 MB
116,384192.0 MB192.0 MB207.3 MB
165,536768.0 MB768.0 MB793.7 MB
84,096384.0 MB384.0 MB401.5 MB
322,048768.0 MB768.0 MB794.2 MB
641,024768.0 MB768.0 MB797.0 MB
128512768.0 MB768.0 MB816.4 MB

Ba hàng cuối đáng nhìn lại lần nữa. Ba mươi hai user với 2.048 token mỗi người, sáu mươi tư với 1.024, một trăm hai mươi tám với 512 — cache đều là 768 MB trong mọi trường hợp, vì cả ba cùng giữ 65.536 token. Cache chỉ phụ thuộc vào tổng số token đang thường trú, không phụ thuộc vào cách chúng được phân bổ giữa các user. Sự thật đó là nền móng của phần batching.

Chương 9 đã giới thiệu multi-query và grouped-query attention rồi hoãn lý do sang chương này. Lý do là công thức đó, và cụ thể là HkvH_{kv} trong đó.

Standard multi-head attention cho mỗi query head key và value head riêng của nó. Model ở đây có 14 query head; với full multi-head attention, cache của nó sẽ là 2×24×14×64×2=86,0162 \times 24 \times 14 \times 64 \times 2 = 86{,}016 byte mỗi token — 84 KB thay vì 12 KB, nhiều hơn đúng bảy lần, tỷ lệ giữa query head và key-value head.

Multi-query attention1 đẩy chuyện này đến cực hạn: mọi query head dùng chung một key-value head. Grouped-query attention2 là thỏa hiệp đã thắng — một vài key-value head, mỗi head được chia sẻ bởi một nhóm query head — vì mất mát chất lượng của MQA là thật còn của GQA thì không. Cả hai không mua thêm phép tính nào. Chúng tồn tại để chia công thức đó cho một số nguyên, và lan khắp ngành ngay khi context dài khiến cache trở thành ràng buộc chính.

Và nó nhanh chóng trở thành như vậy. Với một model lớp 7B có 32 layer và 8 key-value head dimension 128, cache là 128 KB mỗi token ở fp16:

context tokenmột user8 user64 user
4,0000.49 GB3.91 GB31.2 GB
32,0003.91 GB31.25 GB250.0 GB
128,00015.62 GB125.00 GB1,000.0 GB
1,000,000122.07 GB976.56 GB7,812.5 GB

Trọng số riêng của model đó là 13,0 GB ở fp16, con số trong bảng cuối chương này. Vì vậy ở context 128.000 token, cache của một user lớn hơn cả model. Đây là phép tính mà Chương 16 biến thành tiền, và là lý do một cuộc trò chuyện dài không chỉ chậm — nó chiếm một lát cố định của một máy chừng nào request còn sống.

Decode bị memory-bound: trọng số bị kéo qua bus để tạo ra một token, còn các đơn vị tính toán nhàn rỗi. Vậy hãy nhét thêm việc vào cùng một bước. Chạy nhiều request cùng lúc, và trọng số, đọc một lần, phục vụ tất cả. Đo trên cùng model, mỗi request giữ cache 64 token và decode một token:

batchlatency mỗi bướcthroughputlatency vs B=1
10.1286 s7.78 tok/s1.00x
20.1839 s10.88 tok/s1.43x
40.1909 s20.95 tok/s1.49x
80.2781 s28.76 tok/s2.16x
160.3430 s46.64 tok/s2.67x
320.6302 s50.78 tok/s4.90x

Hãy đọc hai cột bên phải cùng nhau, vì chúng là toàn bộ vấn đề. Đi từ một request lên mười sáu nhân throughput lên 6,0 và nhân thời gian chờ của bất kỳ request riêng lẻ nào lên 2,67. Batch làm server tốt hơn và mọi user tệ hơn.

Đó không phải bug để tuning cho mất đi; đó chính là trade-off, và mỗi phía có một cái tên. Latency là thứ một người đang chờ trả lời cảm nhận. Throughput là thứ hóa đơn được chia cho. Không có thiết lập nào cải thiện cả hai.

Cũng hãy để ý nơi nó dừng lại. Từ 16 lên 32, throughput tăng 9 % trong khi latency gần như gấp đôi: bước này đã ngừng bị memory-bound và trở thành compute-bound, và qua điểm gối đó batch không mua thêm gì. Mọi deployment đều có một điểm gối như vậy; vị trí của nó phải được đo trên hệ của bạn, nhưng sự tồn tại của nó thì không.

Static batching lãng phí phần lớn thứ nó giành được

Liên kết đến mục: Static batching lãng phí phần lớn thứ nó giành được

Cách ngây thơ để batch là gom BB request, chạy chúng cùng nhau, và trả về khi tất cả xong. Nhưng chúng không kết thúc cùng lúc: có câu trả lời hai mươi token và có câu trả lời năm trăm. Một batch cố định chạy đến khi phần tử dài nhất hoàn thành, và mọi request đã xong vẫn chiếm slot của nó, đóng góp padding, cho đến lúc đó.

Lấy 64 request với độ lệch độ dài output thực tế — trung vị 18 token, dài nhất 231, tổng cộng 1.874 — và mô phỏng cả hai chính sách ở chi phí mỗi bước đo được cho tám slot:

chính sáchwall clockthroughputlatency trung bình mỗi requestslot-step lãng phí
static batches of 8176.9 s10.6 tok/s83.2 s3,214
continuous, 8 slots109.0 s17.2 tok/s8.1 s0

Throughput cải thiện 1,6x. Latency trung bình cải thiện hơn mười lần, vì dưới static batching, một request hoàn thành trong bốn bước vẫn phải đợi một hàng xóm 231 token trước khi ai đó nghe được gì.

Continuous batching3 là cách sửa, và nó đơn giản đúng như tên gọi: batch không phải một nhóm mà là một tập slot, và một slot vừa rảnh sẽ nhận request tiếp theo trong hàng đợi ngay ở bước kế tiếp. Scheduler làm việc ở độ hạt một token thay vì một request. Mọi serving stack trong production hiện nay đều làm vậy.

Nó có nửa thứ hai, là cache. Các slot đến rồi đi để lại memory cache bị phân mảnh, và đặt trước mỗi slot bằng context tối đa có thể của nó lãng phí phần lớn reservation. PagedAttention4 mượn câu trả lời từ hệ điều hành: lưu cache trong các block kích thước cố định với một bảng block cho mỗi sequence, để cache của một sequence có thể nằm rải rác về mặt vật lý trong khi vẫn liên tục về mặt logic — điều này cũng cho phép hai sequence có prefix chung chia sẻ các block đang giữ prefix đó. Đó là nền tảng của vLLM, và là lý do một serving engine là một memory allocator có gắn transformer.

Nửa còn lại của hóa đơn là chính các trọng số. Nửa tỷ tham số ở bốn byte mỗi cái là 1,98 GB; ở hai byte là 0,99 GB; ở một byte là 0,49 GB. Ít bit hơn mỗi trọng số làm model nhỏ hơn trên đĩa, nhỏ hơn trong memory, và — vì decode bị bandwidth-bound — làm mỗi bước nhanh hơn, vì có ít byte hơn để di chuyển.

Sơ đồ đơn giản nhất là lượng tử hóa symmetric absolute-maximum, và nó vừa trong ba dòng:

quantize.pyPYTHON
qmax  = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax                        
Wq    = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale                                  # dequantized

Chọn một scale để trọng số lớn nhất ánh xạ đến số nguyên lớn nhất, chia, làm tròn, lưu các số nguyên và scale. Tái tạo bằng cách nhân ngược lại. Không có gì tinh vi, và nó hoạt động — cho đến khi không.

Đo trên trọng số thật của model: toàn bộ 168 ma trận projection, 357,8 triệu tham số, sai số tương đối WW^/W\lVert W - \hat{W}\rVert / \lVert W \rVert:

schemesai số tương đối trung bìnhma trận tệ nhất
INT8, một scale cho cả ma trận0.04000.1487
INT8, một scale mỗi hàng output0.01000.0149
INT4, một scale cho cả ma trận0.60260.9931
INT4, một scale mỗi hàng output0.17900.2589
INT4, một scale mỗi nhóm 1280.13230.1992
NF4, một scale mỗi block 640.09520.1205
INT3, một scale mỗi nhóm 1280.30440.4123
INT2, một scale mỗi nhóm 1280.77900.8076

Hàng thứ tư là cú sụp. Sai số tương đối 0,99 trên ma trận tệ nhất nghĩa là bản tái tạo hầu như không giữ lại gì của bản gốc — ma trận đã bị thay bằng nhiễu có cỡ độ lớn gần đúng. Nguyên nhân hiện rõ trong cùng thí nghiệm trên một ma trận đơn:

TEXT
model.layers.12.mlp.down_proj.weight   (896 x 4864)
mean |w| 0.01386   std 0.01822   max |w| 0.43945   max/std 24.1
weights beyond 6 sigma: 692 of 4,358,144   (0.016 %)

Một trọng số trong sáu nghìn nằm ngoài sáu độ lệch chuẩn, và trọng số lớn nhất ở mức 24 độ lệch. Với một scale duy nhất cho cả ma trận, chính một trọng số đó đặt kích thước bước cho toàn bộ 4,3 triệu trọng số. Ở 8 bit có 256 bước và trọng số điển hình vẫn rơi vào một bước có ý nghĩa. Ở 4 bit có 16 bước, bước ngoài cùng dành cho một giá trị hầu như không gì có, và các trọng số bình thường — tức là tất cả — bị làm tròn về hai hoặc ba mức khác nhau.

Mọi thứ sau hàng đó là cùng một cách sửa ở các độ hạt khác nhau: cho scale một lãnh thổ nhỏ hơn. Mỗi hàng output chia sai số cho 3,4; mỗi nhóm 128 trọng số liên tiếp lại chia tiếp. Chi phí là bookkeeping — một scale 16-bit mỗi nhóm 128 là 4+16/128=4.1254 + 16/128 = 4.125 bit mỗi trọng số thay vì 4 — và nó mua lại phần lớn khoảng cách.

NF4 đi từ phía còn lại.5 Các mức không nhất thiết phải cách đều. Trọng số trong một block xấp xỉ phân phối chuẩn, nên chọn mười sáu mức là các quantile của phân phối chuẩn: dày gần zero nơi trọng số thật sự nằm, thưa ở đuôi nơi chúng không nằm. Cùng bốn bit, cùng block scaling, ở block nhỏ hơn — 4,25 bit mỗi trọng số so với 4,125 của group-128 — và sai số đo được giảm từ 0,1323 xuống 0,0952, thấp hơn 28 %. Một phần đến từ block mịn hơn và phần còn lại từ việc đặt các mức nơi khối lượng nằm, và tách hai yếu tố đó sẽ cần hàng thứ ba.

Hộp floating-point của Chương 2 kết thúc bằng một lời hứa: chương này sẽ lượng tử hóa trọng số xuống 8 và 4 bit và tìm thấy một nhúm outlier feature không chịu bị nén. Chúng đây, và chúng giải thích vì sao "cứ làm tròn các số" chưa bao giờ có thể hoạt động trên activations.

Các trọng số ở trên đã cư xử tệ. Activations ở một hạng hoàn toàn khác. Lấy một prompt bình thường dài 84 token, capture residual stream ở mỗi layer, và đo độ lớn lớn nhất mà mỗi trong 896 chiều đạt tới:

layer|h| lớn nhất|h| lớn nhất của chiều trung vịtỷ lệchiều vượt 6x trung vị
16.190.33918x2
41543.481.550996x34
81571.631.4981049x36
121575.031.5461019x34
161579.601.617977x32
201577.982.361668x24
24204.4410.76019x12

Chiều 62 đạt 1.579,6 trong khi chiều trung vị chưa bao giờ vượt 1,6. Nó không phải ngẫu nhiên của một token hay một layer: cùng chiều đó xuất hiện ở layer 4 và vẫn ở layer 20, với giá trị gần như không đổi. Đây là các outlier features,6 và chúng có tính hệ thống — một thuộc tính của model đã train, không phải của input.

Histogram của 896 cực đại theo chiều ở layer 16 làm hình dạng không thể nhầm lẫn:

TEXT
     0 -      1 | ######################################## 254
     1 -      2 | ######################################## 283
     2 -      4 | ######################################## 226
     4 -      8 | ######################################## 93
     8 -     16 | ##################                       18
    16 -     32 | #########                                9
    32 -     64 | #######                                  7
    64 -    128 | #####                                    5
   128 -    256 |                                          0
   256 -    512 |                                          0
   512 -   1024 |                                          0
  1024 -   4096 | #                                        1

Chín trăm chiều xếp gọn dưới 8, hoàn toàn trống trong ba octave, rồi một chiều đơn độc ở tận cuối. Bây giờ lượng tử hóa tensor đó sang INT8 và đếm chuyện gì xảy ra:

schemesai số tương đốimức số nguyên khác nhau được dùng, toàn tensor
một scale cho toàn tensor0.108314 of 256
một scale mỗi token (mỗi hàng)0.0433158
toàn tensor, giữ 1 chiều outlier ở fp320.044248
toàn tensor, giữ 4 chiều outlier ở fp320.027957
toàn tensor, giữ 16 chiều outlier ở fp320.0085102

Mười bốn mức trong 256. Scale được đặt bởi 1.579,6, nên mỗi bước rộng 12,44, và activation điển hình — độ lớn trung vị 0,26, percentile 99 là 2,51 — không có chỗ để hạ cánh. Theo từng chiều, nó còn rõ hơn:

TEXT
single tensor-wide scale = 12.4378
  dim 826 (max |h| = 4.77):  1 distinct level out of 256
  dim 336 (max |h| = 1.62):  1 distinct level out of 256
  dim  96 (max |h| = 0.69):  1 distinct level out of 256

after excluding the top 4 dimensions, scale = 0.5749  (22x smaller)
  dim 826: 8 levels    dim 336: 4 levels    dim  96: 3 levels

Một mức. Cả chiều, mọi token, bị lượng tử hóa thành cùng một số. Tám bit đã được cấp phát và gần như không bit nào được dùng, còn model đọc các activations đó được đưa một hằng số.

Phép đo đó là lý do cho mọi kỹ thuật mà người ta thật sự dùng:

Giữ outlier ở ngoài. LLM.int8()6 phân rã phép nhân ma trận: các chiều có độ lớn cực trị được tính ở 16 bit, mọi thứ còn lại ở INT8, rồi hai nửa được cộng lại. Bảng trên là biên nhận — loại bỏ bốn chiều cắt sai số gần bốn lần. SmoothQuant7 thay vào đó di chuyển khó khăn: chia activations cho một hệ số theo channel và nhân cột trọng số tương ứng với nó, điều này giữ tích không đổi và chuyển outlier khỏi tensor không thể hấp thụ nó sang tensor có thể.

Chọn cách làm tròn, đừng chỉ làm tròn. Không có gì ở trên hỏi ma trận đó dùng để làm gì. GPTQ8 lượng tử hóa từng cột và, sau mỗi cột, điều chỉnh các cột full-precision còn lại để bù cho lỗi đã phạm — tối thiểu hóa lỗi output của layer trên input thật thay vì lỗi trọng số của nó. AWQ9 nhận ra một phần nhỏ channel trọng số quan trọng hơn phần còn lại rất nhiều, tìm chúng từ thống kê activation, và scale chúng lên trước khi lượng tử hóa để chúng rơi vào các mức mịn hơn. Cả hai cần một calibration set; không cái nào cần gradient.

Hiện chi tiết

GGUF, và một định dạng file thì liên quan gì đến chuyện này.

GGUF không phải một phương pháp lượng tử hóa; nó là container mà llama.cpp dùng, và sự nhầm lẫn trong các so sánh gguf vs gptq đến từ việc coi hai thứ này là cùng một loại. GGUF chứa tensors, tokenizer, metadata kiến trúc và chat template trong một file có thể memory-map, và mang bên trong nó một họ các scheme block — những tên như Q4_K_M mã hóa bit mỗi trọng số, kích thước block, và việc một số tensors có được giữ ở precision cao hơn hay không.

Khác biệt kỹ thuật quan trọng: GPTQ và AWQ tạo trọng số được tối ưu cho GPU kernel, trong khi các scheme của GGUF được decode rẻ trên CPU với file được map thay vì load. Đó là lý do cùng một "model 7B 4-bit" danh nghĩa tồn tại ở cả hai thế giới với kích thước khác nhau và chất lượng khác nhau, và là lý do so sánh trung thực không bao giờ là định dạng — mà là phép đo bên dưới, chạy trên tác vụ của chính bạn.

Hầu như mọi bài viết về quantization dừng ở phần trước: nó giải thích phương pháp, trích một tỷ lệ nén, và khẳng định chất lượng "phần lớn được giữ nguyên". Chương 4 nói về việc không tự lừa mình, nên hãy tìm hiểu.

Cùng model, trọng số được lượng tử hóa tại chỗ với từng scheme, rồi ba phép đo: perplexity trên 2.048 token văn xuôi tiếng Anh held-out — ở đây là bản nháp của khóa học này, nên repository thay bằng một cuốn sách public-domain cố định và in một bảng cùng hình dạng với số khác — một bộ 16 câu hỏi factual ngắn có đáp án biết trước dưới greedy decoding, và tỷ lệ token mà model lượng tử hóa đồng ý với model full-precision khi được cho context giống hệt.

schemesai số trọng số trung bìnhperplexitybộ câu hỏiđồng ý với fp32
fp32 (tham chiếu)0.000023.0813/16100.0 %
INT8 per tensor0.040023.5813/16
INT8 per row0.010022.9613/1698.6 %
INT4 per tensor0.6026365,416,0000/16
INT4 per row0.179046.186/1658.3 %
INT4 group 1280.132331.0810/1671.5 %
NF4 block 640.095224.5511/1684.7 %
INT3 group 1280.3044213.090/165.6 %
INT2 group 1280.779026,325,4360/160.0 %

Có bốn điều trong bảng đó đáng nói thẳng.

INT8 làm đúng thì miễn phí. INT8 theo hàng đạt 22,96 so với 23,08 của tham chiếu — khoảng cách một phần hai trăm, là nhiễu và nên được đọc là "giống hệt". Nhiễu nghiêng về phía nào không ổn định: trên corpus public-domain của repository, hai scheme tương tự ra 22,24 so với 22,18: nửa khoảng cách đó, và nghiêng theo hướng ngược lại. Nó đồng ý với model full-precision trên 142 trong 144 token được sinh. Một phần tư memory so với tham chiếu fp32, một nửa so với fp16 mà bạn thật sự triển khai, và không có chi phí phát hiện được. INT8 làm ẩu cũng gần như miễn phí: một scale mỗi ma trận tốn 0,5 điểm perplexity và không mất câu trả lời nào trong battery. Tám bit đủ khoan dung để độ hạt hầu như không quan trọng, và đó chính là lý do người ta khái quát từ INT8 sang INT4 rồi bị đau.

INT4 với một scale mỗi tensor phá hủy model. Perplexity 365 triệu: không phải xuống cấp, mà bị tiêu diệt. Sau đó độ hạt là toàn bộ trò chơi — per-tensor 365.416.000, per-row 46,18, per-group-of-128 31,08, NF4 24,55. Cùng bốn bit mỗi trọng số, nhưng chênh lệch mười lăm triệu lần giữa tệ nhất và tốt nhất.

Perplexity là một dụng cụ thô và battery còn thô hơn. Giữa NF4 và INT4 group-128, khoảng cách perplexity là 6,5 điểm và battery khác một câu hỏi — và khoảng tin cậy của Chương 4 nói rằng một câu trong mười sáu không phân biệt được gì. Có một minh chứng sắc hơn cả khoảng đó: chạy cùng battery với stock repetition penalty của model tắt đi, đúng nghĩa greedy decoding, và hai hàng đó đổi chỗ. Một câu trong mười sáu không phải hiệu ứng nhỏ, mà là không có hiệu ứng. Cảnh báo của Chương 8 cũng áp dụng: perplexity chỉ so sánh được giữa các model dùng chung tokenizer, nên một con số từ bài viết của người khác không thể so với của bạn.

Cột đồng ý là sắc nhất trong ba cột, và gần như miễn phí: chạy model full-precision greedily, rồi hỏi model lượng tử hóa, ở mỗi vị trí, nó sẽ chọn gì với cùng prefix. Nó có 144 quan sát độc lập thay vì 16, không cần ground truth, và suy giảm mượt nơi battery suy giảm theo bậc. Nó cũng chính xác là đại lượng phần tiếp theo cần.

Đây là lời hứa Chương 1 đã đưa ra về chương này, đến đúng hẹn: toán học nói model 4-bit là có thể, còn kỹ thuật quyết định nó có dùng được hay không.

Chương 12 đã công bố điều này và để hóa đơn ở đây.

Ý tưởng đi thẳng từ tách biệt prefill/decode. Xác minh một chuỗi γ\gamma token được đề xuất tốn một forward pass trên γ\gamma vị trí — một phép nhân ma trận-ma trận, hầu như không đắt hơn pass trên một vị trí. Vì vậy:

Một model nhỏ, rẻ sinh γ\gamma token ứng viên theo kiểu autoregressive.

Model lớn chạy một forward pass trên toàn bộ γ\gamma ứng viên cùng lúc, tạo ra thứ nó sẽ nói ở mỗi vị trí.

Giữ prefix dài nhất mà hai bên đồng ý, cộng thêm token mà model lớn cung cấp miễn phí tại bất đồng đầu tiên. Bỏ phần còn lại và bắt đầu lại.

Phân phối output không đổi. Với greedy decoding điều đó hiển nhiên — một token chỉ được chấp nhận nếu target sẽ sinh nó. Với sampling cần một quy tắc chấp nhận đã sửa đổi, và Leviathan et al. chứng minh phân phối kết quả đúng bằng phân phối của target.10 Đây là tối ưu chính xác thứ hai trong chương này.

Vì vậy mọi thứ phụ thuộc vào acceptance rate α\alpha, thứ đo được — nó là cột đồng ý ở trên, đó là lý do nó được tính ở đó. Dùng từng model lượng tử hóa làm draft cho target full-precision, trên 144 vị trí được sinh:

draft modelacceptancechuỗi được chấp nhận dài nhấttoken kỳ vọng mỗi target pass, γ=4\gamma = 4
fp32 (chính target)100.0 %485.00
INT8 per row98.6 %484.86
NF4 block 6484.7 %203.69
INT4 group 12871.5 %132.85
INT4 per row58.3 %72.24
INT3 group 1285.6 %21.06
INT2 group 1280.0 %01.00

Số token được chấp nhận kỳ vọng mỗi verification pass, với độ dài draft γ\gamma, là

E[tokens]=1αγ+11α\mathbb{E}[\text{tokens}] = \frac{1 - \alpha^{\gamma+1}}{1 - \alpha}

và speedup ròng chia con số đó cho chi phí riêng của draft, một phần cc của target mỗi token:

acceptancec=0.05c=0.05, γ=4\gamma=4c=0.1c=0.1, γ=4\gamma=4c=0.2c=0.2, γ=4\gamma=4c=0.1c=0.1, γ=8\gamma=8
30 %1.19x1.02x0.79x0.79x
50 %1.61x1.38x1.08x1.11x
70 %2.31x1.98x1.54x1.78x
90 %3.41x2.93x2.28x3.40x

Mục in đậm là điều cần nhớ: speculative decoding có thể làm generation chậm hơn. Ở acceptance 30 % với draft tốn một phần năm target, bạn trả tiền cho năm forward pass và giữ 1,4 token. Cột cuối là cái bẫy còn lại — draft dài hơn chỉ giúp khi acceptance cao, vì đuôi của một dự đoán γ\gamma token hầu như không bao giờ được chạm tới. Ở acceptance 90 %, γ=8\gamma = 8 đáng 3,40x và ở 30 % nó đáng 0,79x: cùng một cấu hình, thắng hay thua tùy vào một con số đo trên traffic của bạn.

Quantization thu nhỏ một model bằng cách lưu cùng một hàm trong ít bit hơn. Distillation thu nhỏ nó bằng cách train một model nhỏ hơn bắt chước một model lớn hơn11 — một ý tưởng có trước deep learning gần một thập kỷ.12

Phần tinh tế là cái gì student học từ đó. Không phải đáp án đúng: nó đã có thể được train trực tiếp trên đáp án đó. Điều teacher thêm vào là toàn bộ phân phối. Hỏi model thứ gì theo sau một cụm từ và nhìn qua argmax:

TEXT
"She poured the milk into the"
  ' jug' 0.1355   ' cup' 0.1051   ' bowl' 0.0605   ' large' 0.0380   ' milk' 0.0360

Hard label nói jug và không gì khác. Soft label nói jug, và cũng nói rằng cup gần như tốt bằng, bowl hợp lý, và large — một tính từ, một tiếp nối ngữ pháp hoàn toàn khác — vẫn còn sống. Đó là lập luận gốc: đây là số 7, nhưng nó trông khá giống số 1, và sự giống nhau là thông tin mà hard label vứt đi.

Đó cũng là lý do distillation dùng temperature. Chia logits cho TT trước softmax làm phẳng phân phối và nâng trọng số tương đối của các lựa chọn bám sát: trên cụm từ này, tỷ lệ giữa token top và token thứ ba giảm từ 2,24 ở T=1T = 1 xuống 1,50 ở T=2T = 2 — căn bậc hai của số đầu, đúng như việc chia logits cho hai làm với một tỷ lệ. Cùng thứ tự, nhưng nhiều attention của loss hơn đặt vào các near miss. Gradient của student mang sự bất định của teacher chứ không chỉ phán quyết của nó.

Mọi thứ trong chương này giờ là một tổng:

memory=N×bytes per weightfixed+T×2LHkvdhead×bytesgrows with every token+runtime overheadcall it 1.5 GB\text{memory} = \underbrace{N \times \text{bytes per weight}}_{\text{fixed}} + \underbrace{T \times 2 L H_{kv} d_{\text{head}} \times \text{bytes}}_{\text{grows with every token}} + \underbrace{\text{runtime overhead}}_{\text{call it 1.5 GB}}

trong đó TTtổng token thường trú trên tất cả request đồng thời. Áp dụng nó: các hàng 7B và 70B giả định 8 key-value head dimension 128, hàng 13B dùng full multi-head attention với 40 head, đúng cách các thế hệ model đó được xây — và điều đó lộ ra.

8 GB

modelprecisiontrọng sốcòn trống sau overheadcontext token vừa
7Bfp1613.0 GBkhông vừa
7Bint86.5 GBkhông vừa
7Bint4 (g128)3.4 GB3.1 GB25,710
13Bint4 (g128)6.2 GB0.3 GB337
70Bint4 (g128)33.6 GBkhông vừa

16 GB

modelprecisiontrọng sốcòn trống sau overheadcontext token vừa
7Bfp1613.0 GB1.5 GB11,972
7Bint86.5 GB8.0 GB65,378
7Bint4 (g128)3.4 GB11.1 GB91,246
13Bint812.1 GB2.4 GB3,136
13Bint4 (g128)6.2 GB8.3 GB10,822

24 GB

modelprecisiontrọng sốcòn trống sau overheadcontext token vừa
7Bfp1613.0 GB9.5 GB77,508
7Bint86.5 GB16.0 GB130,914
7Bint4 (g128)3.4 GB19.1 GB156,782
13Bint812.1 GB10.4 GB13,622
13Bint4 (g128)6.2 GB16.3 GB21,308
70Bint4 (g128)33.6 GBkhông vừa

Nhìn hàng 13B trong bảng 8 GB. Trọng số vừa — 6,2 GB trên 8 — nên theo cách nói thông thường, một model 13B "chạy trên card 8 GB". Nó có 337 token context, không phải một cuộc trò chuyện mà chỉ vừa đủ một prompt. "Nó có vừa không" là câu hỏi sai. Câu đúng là "với bao nhiêu context, và cho bao nhiêu user cùng lúc".

Cũng hãy nhìn hai hàng int8 trong bảng 16 GB. 7B có 65.378 token và 13B có 3.136 — chênh lệch hai mươi lần từ 5,6 GB trọng số thêm, vì 13B ở đây có multi-head attention và cache của nó tốn 800 KB mỗi token so với 128 KB của 7B. Hai model kích thước tương tự, một cái không dùng được cho context dài, vì một lý do không xuất hiện trong headline của model card nào.

Mười ba chương trước, đây là một perceptron với hai trọng số và một bias. Giờ nó là một transformer đã được thiết kế, train, aligned, học cách tiêu compute cho câu hỏi khó, và được serve với chi phí đo được trên mỗi token — không còn hộp nào bên trong chưa mở.

Chuyện kết thúc ở đây, và kết thúc có chủ ý.

Chương 14 bắt đầu với model ở một nơi khác. Không trong process của bạn, không trong memory của bạn, không trong một biến bạn có thể print: trên một máy bạn không quản trị, sau một API key, một port và một hóa đơn. Mọi thứ được đo ở đây vẫn đang xảy ra — prefill vẫn chạy trước token đầu tiên, cache vẫn lớn lên theo cuộc trò chuyện, batch bạn đang ở vẫn thuộc về người khác và vẫn quyết định latency của bạn — nhưng từ giờ trở đi bạn quan sát nó qua một stream Server-Sent Events, một finish_reason, và một HTTP 429 với header Retry-After. Các câu hỏi thay đổi theo điểm nhìn: không phải gradient này được tính thế nào mà là vì sao hóa đơn của tôi tăng gấp ba. Ngôn ngữ cũng thay đổi, và Chương 14 giải thích quy tắc đó thay vì tuyên bố nó — đến đây code giữ trọng số, gradients, logits và byte tokenizer; từ đó trở đi nó giữ một kết nối, một retry, một cancellation và state tích lũy. Mười ba chương phía sau bạn không bị vứt bỏ khi băng qua ranh giới. Chúng là mô tả của thứ đang chạy ở phía bên kia port.


Có hai phần lược bỏ có chủ ý. FlashAttention (Dao et al., arXiv:2205.14135) không phải một attention khác — nó tính cùng một hàm bằng cách chia tile phép toán để ma trận score n×nn \times n không bao giờ được ghi ra memory, đó là lý do 67 MB trong bảng thứ hai của chương này trong thực tế nhỏ hơn phép tính gợi ý. Và bản thân các kernels được giao lại: bài giảng 10 của CS336 Stanford bao phủ inference systems ở độ sâu mà chương này không cố làm, còn repository llama.cpp và đặc tả GGUF là nguồn chính cho phía CPU.

  1. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). Bài báo phần lớn là một lập luận về memory-bandwidth, và đọc lên đúng như vậy.

  2. Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). Bao gồm công thức uptraining chuyển đổi một checkpoint multi-head hiện có, đó là lý do GQA lan nhanh đến vậy.

  3. Yu, G.-I., Jeong, J. S., Kim, G.-W., Kim, S. and Chun, B.-G. Orca: A Distributed Serving System for Transformer-Based Generative Models. OSDI 2022. Giới thiệu scheduling cấp iteration — continuous batching — và selective batching.

  4. Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. Bài báo làm nền cho vLLM; §3 là phép so sánh với hệ điều hành ở dạng đầy đủ.

  5. Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 được định nghĩa ở §3; mười sáu giá trị mức dùng trong phép đo ở trên là các giá trị bài báo này suy ra.

  6. Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). Phân tích outlier-feature ở §4 là nguồn của hiện tượng được đo ở trên, bao gồm phát hiện rằng outlier xuất hiện có hệ thống ở scale lớn. 2

  7. Xiao, G., Lin, J., Seznec, M., Wu, H., Demouth, J. and Han, S. SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models. arXiv:2211.10438 (2022).

  8. Frantar, E., Ashkboos, S., Hoefler, T. and Alistarh, D. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 (2022).

  9. Lin, J. et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978 (2023).

  10. Leviathan, Y., Kalman, M. and Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192 (2022). Định lý 1 là chứng minh rằng phân phối output không đổi; Chen et al. (arXiv:2302.01318) công bố cùng ý tưởng một cách độc lập.

  11. Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). Temperature và lập luận "dark knowledge".

  12. Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation, sớm hơn chín năm, cho ensembles thay vì transformers.


Tạo bởi

David Vicente Campos

Nhà sáng lập NeuraLIA Labs & Đồng sáng lập MyRealFood

Tôi là kỹ sư máy tính tốt nghiệp Đại học León. Tôi đồng sáng lập MyRealFood, nơi tôi, với vai trò CTO, đã xây dựng ứng dụng mà hàng triệu người đã dùng để ăn uống lành mạnh hơn, và tôi sáng lập NeuraLIA Labs, nơi tôi xây dựng các sản phẩm AI. Ở đây, tôi viết về những gì tôi đã phải hiểu trong quá trình đó, theo cách mà tôi ước đã có ai đó giải thích cho mình.

Tìm hiểu thêm về tác giả

Xuất bản bởi NeuraLIA Labs.

Nhận bài viết mới trong hộp thư

Tin AI, hướng dẫn và cập nhật sản phẩm — một email ngắn khi chúng tôi có nội dung đáng để bạn đọc.

Thích nhắn tin hơn? Vẫn nội dung đó, ở đây:Cộng đồng WhatsApp (mở trong tab mới)Kênh Telegram (mở trong tab mới)

Mục lục khóa học

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineeringĐọc 16 phút

Kỹ thuật ngữ cảnh cho tác nhân AI dài hạn

Tác nhân chạy lâu không thất bại chỉ vì cửa sổ nhỏ. Chúng thất bại khi tệp, đầu ra công cụ và lịch sử cũ lấn át nhiệm vụ mà tác nhân phải hoàn thành.

Sẵn sàng để LIA chọn giúp bạn chưa?

Xây dựng cùng mọi mô hình AI ở một nơi — bắt đầu miễn phí ngay hôm nay.