ทำ Inference ให้ถูกลง: KV cache, Batching และ Quantization
โมเดลเดียวกันตอบเหมือนกันทุก token ใน 8.8 และ 78.9 วินาที แล้ววัด INT4 สามแบบ ไม่ใช่แค่อ้าง
ในหน้านี้
โมเดลเดียวกัน บนเครื่องเดียวกัน ตอบคำถามเดียวกันด้วย 48 token เดิม ผลลัพธ์ทั้งสองเหมือนกันทุก token — ตรวจแล้ว ไม่ได้สมมติ
with a key-value cache: 8.85 s ( 6.01 tokens/second)
without a key-value cache: 78.95 s ( 0.60 tokens/second)มี argument เดียวที่เปลี่ยน: use_cache=False ไม่มีอะไรเกี่ยวกับโมเดล, prompt, sampling หรือเลขคณิตที่ต่างออกไป และการรันครั้งที่สองก็ไม่ได้แม่นยำขึ้นให้คุ้มกับความยุ่งยาก มันช้ากว่าเก้าเท่าโดยไม่ได้อะไรกลับมา
นี่คือรูปทรงของบทนี้ ทุกอย่างในบทนี้ — cache, batch, weights ที่ถูก quantized — คือความพยายามหยุดจ่ายให้กับงานที่ไม่ได้เปลี่ยนคำตอบ หรือหาว่าคำตอบที่ถูกลงต้องแลกกับอะไร บทที่ 10 วางบัญชีราคาของ training ไว้แล้ว นี่คือบัญชีราคาของฝั่งที่คุณต้องจ่ายตลอดไป: โมเดลที่ deploy แล้วใช้ประมาณ FLOPs ต่อทุก token ที่มันปล่อยออกมา ในทุก request ไปตลอดอายุของมัน
เวลาของการรันครั้งที่สองหายไปไหน
ลิงก์ไปยังส่วน: เวลาของการรันครั้งที่สองหายไปไหนในการ generate token หนึ่ง token decoder-only transformer จะรับทั้ง sequence จนถึงตอนนั้น รันผ่านทุก layer แล้วอ่าน probability distribution จากตำแหน่งสุดท้าย จากนั้น append token ที่เลือกเข้าไป แล้วทำซ้ำ คำอธิบายนี้ถูกต้อง และนี่คือสิ่งที่การรันแบบช้าทำ
แต่มันก็สิ้นเปลืองอย่างมหาศาลด้วย และเหตุผลคือ causal mask จาก บทที่ 9 key และ value vectors ของตำแหน่ง 7 คำนวณจาก input ของตำแหน่ง 7 และตำแหน่งก่อนหน้า เมื่อตำแหน่ง 8 มาถึง ตำแหน่ง 7 มองไม่เห็นมัน — นั่นคือความหมายของ causal — ดังนั้น key และ value ของตำแหน่ง 7 จึงเป็น ตัวเลขชุดเดิมทุกประการ การรันแบบช้ากลับคำนวณมันซ้ำอยู่ดี ในทุก step
ดังนั้นก็เก็บมันไว้ ที่เก็บนี้คือ key-value cache ซึ่งเป็นการ optimize ที่มีผลที่สุดอย่างเดียวในการ serving โมเดลภาษา:
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)ดูสิ่งที่ป้อนเข้าโมเดลใน loop: nxt, หนึ่ง token ไม่ใช่ทั้ง sequence query ของ token ใหม่ attend กับ key ทุกตัวที่ cached ไว้ และ cached keys เหล่านั้นก็ไม่มีวันเปลี่ยนอยู่แล้ว นี่ ไม่ใช่ approximation — การตรวจว่า output เหมือนกันด้านบนคือประเด็นสำคัญ cache ไม่ได้แลกคุณภาพกับความเร็ว มันลบเลขคณิตที่ซ้ำซ้อนออกไป
เพื่อดู scaling ให้ชัด ลอก transformer ออกไป แล้วจับเวลา attention head เดียวที่มี โดยคำนวณ generation หนึ่ง step ทั้งสองวิธี:
| token ใน context | คำนวณใหม่ทั้งหมด | มี cache | ratio | score matrix |
|---|---|---|---|---|
| 128 | 0.59 ms | 0.062 ms | 10x | 65,536 B vs 512 B |
| 256 | 1.20 ms | 0.163 ms | 7x | 262,144 B vs 1,024 B |
| 512 | 7.03 ms | 0.078 ms | 90x | 1,048,576 B vs 2,048 B |
| 1024 | 17.31 ms | 0.114 ms | 152x | 4,194,304 B vs 4,096 B |
| 2048 | 59.83 ms | 0.214 ms | 279x | 16,777,216 B vs 8,192 B |
| 4096 | 236.18 ms | 0.284 ms | 832x | 67,108,864 B vs 16,384 B |
คอลัมน์ขวาสุดคือสาเหตุ การคำนวณใหม่สร้าง attention matrix เต็ม ทุก step — จากกล่อง asymptotic notation ของบทที่ 9 ที่จ่ายหนึ่งครั้งต่อ token เมื่อมี cache คุณสร้างแถว แทน: ที่ 4,096 token คือ scores 67 MB เทียบกับ 16 KB
การนับ multiply-accumulates แทนมิลลิวินาทีทำให้เอาเครื่องออกจากข้อโต้แย้งได้ เพื่อ generate token จาก cold start:
| token ที่ generated | มี cache | คำนวณใหม่ | ratio |
|---|---|---|---|
| 128 | 2.6 M | 192.0 M | 73x |
| 512 | 23.1 M | 7.36 G | 318x |
| 2048 | 293.7 M | 392.6 G | 1,336x |
ต่อ step เวอร์ชัน cached เป็นเชิงเส้นตาม context ส่วนแบบไม่มี cache เป็นกำลังสอง เมื่อรวมทั้ง generation คือ เทียบกับ โดย ratio โตขึ้นไม่สิ้นสุด ความต่างเก้าเท่าในตอนเปิดบทวัดจาก 48 token — ยังสั้นกว่าแถวแรกของตารางนั้น
cache ยังเปลี่ยนสิ่งที่ต้องอยู่ใน memory ด้วย บน laptop GPU 8 GB ที่ generate 256 token ใน fp16 โดยเอา peak ของ allocator แล้วลบ resident weights:
| peak working memory | |
|---|---|
| มี cache | 21.8 MB |
| คำนวณใหม่ | 181.7 MB |
ใช้ memory มากกว่า 8.3 เท่า เพื่อผลิต token เดิมให้ช้าลง นี่คือคำสัญญาจาก บทที่ 5 ที่มาถึงจากทิศทางที่ไม่คาดคิด: ที่นั่น reverse-mode autodiff ต้องเก็บ intermediate ทุกตัวไว้ให้ backward pass และ activations ครอง memory ของ training ใน inference ไม่มี backward pass และไม่มีอะไรต้องเก็บไว้เพื่อมัน — ดังนั้นสิ่งที่ครอง memory แทนคือ cache และมันเป็นการเลือกโดยตั้งใจ ไม่ใช่ต้นทุนที่หลีกเลี่ยงไม่ได้
Prefill และ decode เป็นคนละเครื่องกัน
ลิงก์ไปยังส่วน: Prefill และ decode เป็นคนละเครื่องกันดูการรันเร็วอีกครั้ง: token แรกของมันมีพฤติกรรมไม่เหมือนอีกสี่สิบเจ็ด token
prefill, 40 prompt tokens : 1.0224 s -> 25.6 ms per token
decode, 47 steps : 0.1665 s mean per stepprompt ใช้ 25.6 ms ต่อ token และ token ที่ generated แต่ละตัวใช้ 166 ms โมเดลเดียวกัน hardware เดียวกัน weights เดียวกัน ต่างกันหกเท่าต่อ token — และทิศทางกลับกับที่คนส่วนใหญ่คาด prompt คือส่วนที่ ถูก Generation แยกเป็นสอง phase ที่มีฟิสิกส์ต่างกันจริง ๆ:
Prefill
ลิงก์ไปยังส่วน: Prefillforward pass หนึ่งครั้งบน prompt ทั้งหมด ทุก token ถูกประมวลผลพร้อมกัน ดังนั้น weight matrix แต่ละตัวถูกโหลดจาก memory ครั้งเดียว แล้วคูณกับ matrix ของ token vectors หลายร้อยตัว — matrix-matrix product ที่มีเลขคณิตจำนวนมากต่อ byte ที่เคลื่อนย้าย ซึ่งเป็นสิ่งที่ GPU ถูกสร้างมาเพื่อทำ Prefill เป็น compute-bound และต้นทุนของมันประมาณเชิงเส้นตามความยาว prompt
Decode
ลิงก์ไปยังส่วน: Decodeforward pass หนึ่งครั้งต่อ token, batch ขนาดหนึ่ง และ sequence ขนาดหนึ่ง weight matrix ทุกตัวยังคงถูกโหลดจาก memory ทั้งหมด แล้วคูณกับ vector เดียว — matrix-vector product ที่แทบไม่มีเลขคณิตต่อ byte ที่เคลื่อนย้าย Decode เป็น memory-bandwidth-bound และต้นทุนต่อ token แทบไม่ขึ้นกับความยาวของ context
ทั้งสองครึ่งวัดได้ Prefill หนึ่ง pass บน token:
| prompt tokens | วินาที | ms ต่อ token |
|---|---|---|
| 16 | 0.3515 | 21.97 |
| 32 | 0.5254 | 16.42 |
| 64 | 1.0491 | 16.39 |
| 128 | 1.6552 | 12.93 |
| 256 | 3.0965 | 12.10 |
Decode หนึ่ง token เทียบกับ cache ของ :
| cached tokens | ms สำหรับหนึ่ง token |
|---|---|
| 16 | 110.05 |
| 64 | 97.57 |
| 256 | 108.53 |
| 1024 | 103.86 |
อ่านตารางที่สองสองรอบ การเพิ่ม context จาก 16 token เป็น 1,024 — history มากขึ้นหกสิบสี่เท่าให้ attend — เปลี่ยนต้นทุนของ step ไปแบบที่วัดไม่ได้ attention กับ cache เป็นงานจริง แต่ถูกกลบด้วยต้นทุนคงที่ของการลาก weights ครึ่งพันล้านตัวผ่าน memory bus เพื่อผลิต vector หนึ่งตัว ต้นทุนคงที่นี้คือเหตุผลของทุกอย่างในส่วนถัดไป
สอง phase นี้คือต้นกำเนิดของตัวเลขสองตัวที่ serving system ทุกระบบรายงาน Time to first token โดยแก่นแล้วคือ prefill และมันโตตาม prompt ซึ่งเป็นเหตุผลที่บทสนทนายาว ๆ รู้สึกเริ่มช้า Tokens per second คือ และมันประมาณคงที่ จึงเป็นเหตุผลที่คำตอบหลังจากนั้นไหลสม่ำเสมอ chat ที่เริ่มช้าแล้ว stream ลื่นไม่ใช่กลเม็ดการ render มันคือตารางสองตารางนี้
cache ก็คือบิลด้วย
ลิงก์ไปยังส่วน: cache ก็คือบิลด้วยcache แลกเลขคณิตกับ memory และ memory ที่มันต้องการไม่เล็กเลย สำหรับทุก token ใน context ทุก layer เก็บ key vector หนึ่งตัวและ value vector หนึ่งตัวต่อ key-value head:
เลข 2 คือสำหรับ keys และ values; ที่เหลือทั้งหมดคือ architecture สำหรับโมเดลที่วัดตลอดบทนี้ — 24 layers, 14 query heads, 2 key-value heads, head dimension 64 — ใน fp16 เท่ากับ bytes ต่อ token
Formula ในวงการนี้มักคลาดเคลื่อนด้วย factor of two ดังนั้นตรวจเทียบกับ allocator แทนที่จะเชื่อมัน:
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ตรงเป๊ะ และยังตรงเป๊ะในทุก shape ที่ลอง:
| batch | context | measured cache | predicted | peak working memory |
|---|---|---|---|---|
| 1 | 512 | 6.0 MB | 6.0 MB | 15.4 MB |
| 1 | 16,384 | 192.0 MB | 192.0 MB | 207.3 MB |
| 1 | 65,536 | 768.0 MB | 768.0 MB | 793.7 MB |
| 8 | 4,096 | 384.0 MB | 384.0 MB | 401.5 MB |
| 32 | 2,048 | 768.0 MB | 768.0 MB | 794.2 MB |
| 64 | 1,024 | 768.0 MB | 768.0 MB | 797.0 MB |
| 128 | 512 | 768.0 MB | 768.0 MB | 816.4 MB |
สามแถวสุดท้ายควรดูอีกครั้ง user สามสิบสองคนที่มี 2,048 token ต่อคน, หกสิบสี่คนที่มี 1,024, หนึ่งร้อยยี่สิบแปดคนที่มี 512 — cache เป็น 768 MB ทุกกรณี เพราะทั้งสามถือ token รวม 65,536 ตัว cache ขึ้นกับจำนวน token รวมที่ resident เท่านั้น ไม่ขึ้นกับว่ามันกระจายอยู่ใน user กี่คนอย่างไร ข้อเท็จจริงนี้คือรากฐานของส่วน batching
MQA และ GQA มาจากไหน
ลิงก์ไปยังส่วน: MQA และ GQA มาจากไหนบทที่ 9 แนะนำ multi-query และ grouped-query attention แล้วเลื่อนเหตุผลมาที่บทนี้ เหตุผลคือ formula นั้น และโดยเฉพาะ ในนั้น
standard multi-head attention ให้ query head ทุกตัวมี key และ value heads ของตัวเอง โมเดลนี้มี 14 query heads; ถ้าใช้ full multi-head attention cache ของมันจะเป็น bytes ต่อ token — 84 KB แทนที่จะเป็น 12 KB, มากกว่าเจ็ดเท่าพอดี ซึ่งเป็น ratio ของ query heads ต่อ key-value heads
Multi-query attention1 พาสิ่งนี้ไปสุดทาง: query heads ทั้งหมดแชร์ key-value head เดียว Grouped-query attention2 คือทางประนีประนอมที่ชนะ — key-value heads จำนวนหนึ่ง แต่ละตัวถูกแชร์โดยกลุ่มของ query heads — เพราะ quality loss ของ MQA เป็นเรื่องจริง แต่ของ GQA ไม่ใช่ ทั้งสองไม่ได้ซื้อเลขคณิตใด ๆ มันมีอยู่เพื่อหาร formula นั้นด้วยจำนวนเต็ม และแพร่ไปทั่วอุตสาหกรรมทันทีที่ long context ทำให้ cache เป็น binding constraint
ซึ่งมันเกิดขึ้นเร็วมาก สำหรับโมเดลระดับ 7B ที่มี 32 layers และ 8 key-value heads ขนาด dimension 128 cache คือ 128 KB ต่อ token ใน fp16:
| context tokens | user เดียว | 8 users | 64 users |
|---|---|---|---|
| 4,000 | 0.49 GB | 3.91 GB | 31.2 GB |
| 32,000 | 3.91 GB | 31.25 GB | 250.0 GB |
| 128,000 | 15.62 GB | 125.00 GB | 1,000.0 GB |
| 1,000,000 | 122.07 GB | 976.56 GB | 7,812.5 GB |
weights ของโมเดลนั้นเองคือ 13.0 GB ใน fp16 ตามตัวเลขในตารางท้ายบทนี้ ดังนั้นที่ context 128,000 token cache ของ user คนเดียวใหญ่กว่าโมเดล นี่คือเลขคณิตที่ บทที่ 16 แปลงเป็นเงิน และเป็นเหตุผลว่าบทสนทนายาว ๆ ไม่ได้แค่ช้า — มันครอบครองส่วนตายตัวของเครื่องหนึ่งไว้ตราบใดที่ request ยังมีชีวิต
Batching: ตัวเลขที่เพิ่มขึ้นและตัวเลขที่ลดลง
ลิงก์ไปยังส่วน: Batching: ตัวเลขที่เพิ่มขึ้นและตัวเลขที่ลดลงDecode เป็น memory-bound: weights ถูกลากผ่าน bus เพื่อผลิต token หนึ่งตัว และหน่วยคำนวณนั่งว่าง ดังนั้นใส่งานมากขึ้นใน step เดียวกัน รันหลาย request พร้อมกัน แล้ว weights ที่อ่านครั้งเดียวก็รับใช้ทุก request วัดบนโมเดลเดียวกัน โดยแต่ละ request ถือ cache 64 token และ decode หนึ่ง token:
| batch | latency ต่อ step | throughput | latency vs B=1 |
|---|---|---|---|
| 1 | 0.1286 s | 7.78 tok/s | 1.00x |
| 2 | 0.1839 s | 10.88 tok/s | 1.43x |
| 4 | 0.1909 s | 20.95 tok/s | 1.49x |
| 8 | 0.2781 s | 28.76 tok/s | 2.16x |
| 16 | 0.3430 s | 46.64 tok/s | 2.67x |
| 32 | 0.6302 s | 50.78 tok/s | 4.90x |
อ่านสองคอลัมน์ขวาเทียบกัน เพราะมันคือประเด็นทั้งหมด การเพิ่มจากหนึ่ง request เป็นสิบหกทำให้ throughput คูณ 6.0 และทำให้เวลารอของ request รายตัวคูณ 2.67 batch ทำให้ server ดีขึ้นและ user ทุกคนแย่ลง
นี่ไม่ใช่ bug ที่ tune ให้หายได้ มันคือ trade-off เอง และมีชื่อคนละฝั่ง Latency คือสิ่งที่คนรอคำตอบสัมผัส Throughput คือสิ่งที่ใช้หาร invoice ไม่มี setting ใดทำให้ทั้งสองดีขึ้นพร้อมกัน
สังเกตด้วยว่ามันหยุดตรงไหน จาก 16 เป็น 32 throughput เพิ่ม 9 % ขณะที่ latency เกือบสองเท่า: step เลิกเป็น memory-bound แล้วกลายเป็น compute-bound และพ้น knee นั้น batch ก็ไม่ซื้ออะไรแล้ว ทุก deployment มี knee แบบนี้ ตำแหน่งของมันต้องวัดบนของคุณเอง แต่ การมีอยู่ ของมันไม่ต้องพิสูจน์
Static batching เสียสิ่งที่ชนะไปเกือบหมด
ลิงก์ไปยังส่วน: Static batching เสียสิ่งที่ชนะไปเกือบหมดวิธี naive ในการ batch คือรวบรวม request รันพร้อมกัน แล้ว return เมื่อทุก request เสร็จ แต่พวกมันไม่ได้เสร็จพร้อมกัน: บางคำตอบมียี่สิบ token บางคำตอบมีห้าร้อย fixed batch รันจนสมาชิกที่ยาวที่สุดเสร็จ และ request ที่เสร็จแล้วทุกตัวก็ยังครอบครอง slot ของตัวเอง สร้าง padding จนถึงตอนนั้น
เอา 64 request ที่มี output length เอียงแบบสมจริง — median 18 token, ยาวสุด 231, รวม 1,874 — แล้ว simulate สอง policy ด้วย per-step cost ที่วัดได้สำหรับแปด slot:
| policy | wall clock | throughput | mean latency ต่อ request | wasted slot-steps |
|---|---|---|---|---|
| static batches of 8 | 176.9 s | 10.6 tok/s | 83.2 s | 3,214 |
| continuous, 8 slots | 109.0 s | 17.2 tok/s | 8.1 s | 0 |
Throughput ดีขึ้น 1.6x Mean latency ดีขึ้น มากกว่าสิบเท่า เพราะภายใต้ static batching request ที่เสร็จในสี่ step ยังต้องรอ neighbor 231-token ก่อนที่ใครจะได้ยินอะไร
Continuous batching3 คือทางแก้ และมันง่ายตามชื่อ: batch ไม่ใช่กลุ่ม แต่เป็นชุดของ slots และ slot ที่ว่างจะรับ request ถัดไปใน queue ใน step ถัดไปทันที scheduler ทำงานที่ granularity ของหนึ่ง token แทนที่จะเป็นหนึ่ง request serving stack ทุกตัวใน production ทำแบบนี้แล้วตอนนี้
มันมีครึ่งหลังด้วย ซึ่งคือ cache slots ที่เข้าออกทำให้ cache memory fragmented และการจอง slot แต่ละตัวตาม context สูงสุดที่เป็นไปได้ทำให้เสีย reservation ส่วนใหญ่ PagedAttention4 ยืมคำตอบจาก operating systems: เก็บ cache ใน fixed-size blocks พร้อม block table ต่อ sequence เพื่อให้ cache ของ sequence กระจัดกระจายทาง physical ได้ แต่ยัง contiguous ทาง logical — ซึ่งยังทำให้สอง sequence ที่มี prefix ร่วมกันแชร์ blocks ที่ถือ prefix นั้นได้ด้วย นี่คือสิ่งที่ vLLM สร้างอยู่บนมัน และเหตุผลที่ serving engine คือ memory allocator ที่มี transformer ติดอยู่
Quantization และสิ่งแรกที่พัง
ลิงก์ไปยังส่วน: Quantization และสิ่งแรกที่พังอีกครึ่งของบิลคือ weights เอง parameters ครึ่งพันล้านตัวที่สี่ bytes ต่อตัวคือ 1.98 GB; ที่สอง bytes คือ 0.99 GB; ที่หนึ่ง byte คือ 0.49 GB bits ต่อ weight น้อยลงทำให้โมเดลบน disk เล็กลง ทำให้ใน memory เล็กลง และ — เพราะ decode เป็น bandwidth-bound — ทำให้แต่ละ step เร็วขึ้น เนื่องจากมี bytes ให้ย้ายน้อยลง
scheme ที่ง่ายที่สุดคือ symmetric absolute-maximum quantization และมันใส่ได้ในสามบรรทัด:
qmax = 2 ** (bits - 1) - 1
scale = W.abs().max() / qmax
Wq = torch.round(W / scale).clamp(-qmax - 1, qmax)
W_hat = Wq * scale # dequantizedเลือก scale เพื่อให้ weight ที่ใหญ่ที่สุด map ไปยัง integer ที่ใหญ่ที่สุด หาร ปัดเศษ เก็บ integers และ scale สร้างกลับโดยคูณคืน ไม่มีอะไรฉลาดเป็นพิเศษ และมันใช้ได้ — จนกระทั่งใช้ไม่ได้
วัดบน weights จริงของโมเดล: projection matrices ทั้งหมด 168 ตัว, parameters 357.8 ล้านตัว, relative error :
| scheme | mean relative error | worst matrix |
|---|---|---|
| INT8, หนึ่ง scale สำหรับทั้ง matrix | 0.0400 | 0.1487 |
| INT8, หนึ่ง scale ต่อ output row | 0.0100 | 0.0149 |
| INT4, หนึ่ง scale สำหรับทั้ง matrix | 0.6026 | 0.9931 |
| INT4, หนึ่ง scale ต่อ output row | 0.1790 | 0.2589 |
| INT4, หนึ่ง scale ต่อ group of 128 | 0.1323 | 0.1992 |
| NF4, หนึ่ง scale ต่อ block of 64 | 0.0952 | 0.1205 |
| INT3, หนึ่ง scale ต่อ group of 128 | 0.3044 | 0.4123 |
| INT2, หนึ่ง scale ต่อ group of 128 | 0.7790 | 0.8076 |
แถวที่สี่คือการพังทลาย relative error 0.99 บน worst matrix หมายความว่า reconstruction แทบไม่เหลืออะไรของต้นฉบับเลย — matrix ถูกแทนด้วย noise ที่มี magnitude ประมาณถูกต้อง สาเหตุมองเห็นได้ในการทดลองเดียวกันบน matrix เดียว:
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 %)weight หนึ่งตัวในหกพันอยู่นอกหก standard deviations และตัวใหญ่สุดอยู่ไกล 24 ด้วย scale เดียวสำหรับทั้ง matrix weight หนึ่งตัวนั้นกำหนด step size ให้ทั้งหมด 4.3 ล้านตัว ที่ 8 bits มี 256 steps และ weight ทั่วไปยังลงบน step ที่มีความหมายได้ ที่ 4 bits มี 16 steps โดย step นอกสุดถูกกันไว้สำหรับค่าที่แทบไม่มีใครมี และ weights ปกติ — ซึ่งก็คือทั้งหมด — ถูกปัดไปเป็นเพียงสองหรือสามระดับ
ทุกอย่างหลังแถวนั้นคือการซ่อมแบบเดียวกันที่ granularity ต่างกัน: ให้ scale มีอาณาเขตเล็กลง Per output row หาร error ลง 3.4; per group of 128 consecutive weights หารมันอีก ต้นทุนคือ bookkeeping — scale 16-bit ต่อ group of 128 เท่ากับ bits ต่อ weight แทนที่จะเป็น 4 — และมันซื้อช่องว่างส่วนใหญ่กลับมา
NF4 เข้าหาจากอีกฝั่ง5 levels ไม่จำเป็นต้องห่างเท่ากัน weights ภายใน block กระจายประมาณ normal ดังนั้นเลือกสิบหกระดับเป็น quantiles ของ normal distribution: หนาแน่นใกล้ศูนย์ที่ weights อยู่จริง เบาบางใน tails ที่พวกมันไม่ได้อยู่ สี่ bits เดิม block scaling เดิม ที่ block เล็กกว่า — 4.25 bits ต่อ weight เทียบกับ 4.125 ของ group-128 — และ error ที่วัดได้ลดจาก 0.1323 เป็น 0.0952, ต่ำกว่า 28 % ส่วนหนึ่งมาจาก block ที่ละเอียดกว่า และที่เหลือมาจากการวาง levels ตรงที่มวลอยู่ การแยกสองอย่างนี้ต้องมีแถวที่สาม
outlier features
ลิงก์ไปยังส่วน: outlier featuresกล่อง floating-point ของ บทที่ 2 จบด้วยคำสัญญา: ว่าบทนี้จะ quantize weights เป็น 8 และ 4 bits แล้วพบ feature นอกคอกหยิบมือหนึ่งที่ปฏิเสธการถูกบีบ นี่คือพวกมัน และมันอธิบายว่าทำไม “แค่ปัดตัวเลข” จึงไม่มีทางใช้ได้กับ activations
weights ด้านบนประพฤติตัวแย่แล้ว activations อยู่คนละลีกเลย ใช้ prompt ธรรมดา 84-token จับ residual stream ในแต่ละ layer แล้ววัด magnitude สูงสุดที่แต่ละ dimension จาก 896 dimensions ไปถึง:
| layer | largest |h| | median dimension's largest |h| | ratio | dimensions above 6x the median |
|---|---|---|---|---|
| 1 | 6.19 | 0.339 | 18x | 2 |
| 4 | 1543.48 | 1.550 | 996x | 34 |
| 8 | 1571.63 | 1.498 | 1049x | 36 |
| 12 | 1575.03 | 1.546 | 1019x | 34 |
| 16 | 1579.60 | 1.617 | 977x | 32 |
| 20 | 1577.98 | 2.361 | 668x | 24 |
| 24 | 204.44 | 10.760 | 19x | 12 |
Dimension 62 ไปถึง 1,579.6 ขณะที่ median dimension ไม่เคยเกิน 1.6 มันไม่ใช่ fluke ของ token หนึ่งตัวหรือ layer หนึ่งชั้น: dimension เดิมอยู่ที่ layer 4 และยังอยู่ที่ layer 20 ด้วยค่าเกือบเท่าเดิม นี่คือ outlier features,6 และมันเป็นระบบ — เป็น property ของโมเดลที่ trained แล้ว ไม่ใช่ของ input
histogram ของ maxima ต่อ dimension ทั้ง 896 ตัวที่ layer 16 ทำให้รูปทรงชัดเจนผิดไม่ได้:
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เก้าร้อย dimensions กองเป็นระเบียบต่ำกว่า 8 ไม่มีอะไรเลยตลอดสาม octaves แล้วมี dimension เดียวโดดเดี่ยวอยู่ปลายสุด ตอนนี้ quantize tensor นั้นเป็น INT8 แล้วนับว่าเกิดอะไรขึ้น:
| scheme | relative error | distinct integer levels used, whole tensor |
|---|---|---|
| หนึ่ง scale สำหรับทั้ง tensor | 0.1083 | 14 of 256 |
| หนึ่ง scale ต่อ token (per row) | 0.0433 | 158 |
| ทั้ง tensor, เก็บ 1 outlier dimension ใน fp32 | 0.0442 | 48 |
| ทั้ง tensor, เก็บ 4 outlier dimensions ใน fp32 | 0.0279 | 57 |
| ทั้ง tensor, เก็บ 16 outlier dimensions ใน fp32 | 0.0085 | 102 |
สิบสี่ระดับจาก 256 scale ถูกกำหนดโดย 1,579.6 ดังนั้นทุก step กว้าง 12.44 และ activation ทั่วไป — median magnitude 0.26, ninety-ninth percentile 2.51 — ไม่มีที่ให้ลง ต่อ dimension ยิ่งชัดกว่า:
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หนึ่งระดับ ทั้ง dimension, ทุก token, ถูก quantized เป็นเลขเดียวกัน จัดสรรแปด bits แต่ใช้จริงประมาณศูนย์ และโมเดลที่อ่าน activations เหล่านั้นได้รับ constant
การวัดนั้นคือเหตุผลรองรับทุกเทคนิคที่ผู้คนใช้จริง:
กัน outliers ออกไปจากมัน LLM.int8()6 แยก matrix multiply: dimensions ที่มี magnitude สุดขั้วถูกคำนวณใน 16 bits ส่วนที่เหลือใน INT8 แล้วเอาสองครึ่งมารวมกัน ตารางด้านบนคือใบเสร็จ — เอาออกสี่ dimensions แล้ว error ลดลงเกือบสี่เท่า SmoothQuant7 ย้ายความยากแทน: หาร activations ด้วย factor ต่อ channel และคูณ weight column ที่ตรงกันด้วย factor นั้น ซึ่งทำให้ product ไม่เปลี่ยน และย้าย outlier ออกจาก tensor ที่รับมันไม่ไหวไปยัง tensor ที่รับได้
เลือกวิธีปัดเศษ ไม่ใช่แค่ปัด ไม่มีอะไรด้านบนถามว่า matrix นี้มีไว้ ทำอะไร GPTQ8 quantize ทีละ column และหลังแต่ละ column ปรับ columns ที่ยังเป็น full-precision เพื่อชดเชย error ที่ commit ไปแล้ว — minimising error ของ output ของ layer บน input จริง แทนที่จะเป็น error ของ weights AWQ9 สังเกตว่า weight channels ส่วนน้อยสำคัญกว่าส่วนที่เหลือมาก ค้นหามันจาก activation statistics แล้ว scale มันขึ้นก่อน quantizing เพื่อให้มันลงบน levels ที่ละเอียดกว่า ทั้งสองต้องใช้ calibration set; ไม่มีอันไหนต้องใช้ gradients
แสดงรายละเอียด
GGUF และ file format เกี่ยวอะไรกับเรื่องนี้
GGUF ไม่ใช่วิธี quantization; มันคือ container ที่ llama.cpp ใช้ และความสับสนใน comparison ของ gguf vs gptq มาจากการปฏิบัติต่อสองอย่างนี้ราวกับเป็นสิ่งชนิดเดียวกัน GGUF ถือ tensors, tokenizer, architecture metadata และ chat template ไว้ในไฟล์เดียวที่ memory-mappable ได้ และพา family ของ block schemes ไว้ข้างใน — ชื่ออย่าง Q4_K_M encode bits ต่อ weight, block size และว่ามี tensors บางตัวถูกเก็บไว้ที่ precision สูงกว่าหรือไม่
ความต่างทาง engineering ที่สำคัญ: GPTQ และ AWQ ผลิต weights ที่ optimise สำหรับ GPU kernel ส่วน schemes ของ GGUF decode ได้ถูกบน CPU โดย map file แทนการโหลด นี่คือเหตุผลที่ “4-bit 7B model” ตามชื่อแบบเดียวกันมีอยู่ทั้งสองโลก ด้วยขนาดต่างกันและคุณภาพต่างกัน และเหตุผลที่การเปรียบเทียบที่ซื่อสัตย์ไม่เคยเป็น format — มันคือการวัดด้านล่าง รันบน task ของคุณเอง
quantization มีต้นทุนจริงเท่าไร เมื่อวัดจริง
ลิงก์ไปยังส่วน: quantization มีต้นทุนจริงเท่าไร เมื่อวัดจริงบทความเกี่ยวกับ quantization แทบทุกบทหยุดที่ส่วนก่อนหน้า: อธิบาย method อ้าง compression ratio และยืนยันว่าคุณภาพ “ส่วนใหญ่ยังคงอยู่” บทที่ 4 ว่าด้วยการไม่หลอกตัวเอง ดังนั้นมาหาคำตอบกัน
โมเดลเดียวกัน weights ถูก quantized in place ด้วยแต่ละ scheme จากนั้นวัดสามอย่าง: perplexity บน prose ภาษาอังกฤษ held-out 2,048 token — ที่นี่คือ draft ของคอร์สนี้ ซึ่งเป็นเหตุผลที่ repository แทนด้วยหนังสือ public-domain คงที่และพิมพ์ตารางรูปทรงเดียวกันแต่ตัวเลขต่างกัน — ชุดคำถามข้อเท็จจริงสั้น 16 ข้อพร้อมคำตอบที่รู้ภายใต้ greedy decoding และสัดส่วน token ที่โมเดล quantized เห็นตรงกับโมเดล full-precision เมื่อให้ context เดียวกัน
| scheme | mean weight error | perplexity | question battery | agrees with fp32 |
|---|---|---|---|---|
| fp32 (reference) | 0.0000 | 23.08 | 13/16 | 100.0 % |
| INT8 per tensor | 0.0400 | 23.58 | 13/16 | — |
| INT8 per row | 0.0100 | 22.96 | 13/16 | 98.6 % |
| INT4 per tensor | 0.6026 | 365,416,000 | 0/16 | — |
| INT4 per row | 0.1790 | 46.18 | 6/16 | 58.3 % |
| INT4 group 128 | 0.1323 | 31.08 | 10/16 | 71.5 % |
| NF4 block 64 | 0.0952 | 24.55 | 11/16 | 84.7 % |
| INT3 group 128 | 0.3044 | 213.09 | 0/16 | 5.6 % |
| INT2 group 128 | 0.7790 | 26,325,436 | 0/16 | 0.0 % |
สี่อย่างในตารางนั้นควรพูดให้ชัด
INT8 ที่ทำถูกต้องคือฟรี Per-row INT8 ได้ 22.96 เทียบกับ 23.08 ของ reference — ช่องว่างหนึ่งส่วนในสองร้อย ซึ่งเป็น noise และควรอ่านว่า “เหมือนกัน” noise จะชี้ไปทางไหนไม่เสถียร: บน corpus public-domain ของ repository schemes สองตัวเดิมออกมา 22.24 เทียบกับ 22.18: ระยะห่างครึ่งหนึ่ง และชี้อีกทาง มันเห็นตรงกับโมเดล full-precision บน 142 จาก 144 generated tokens ใช้ memory หนึ่งในสี่เทียบกับ fp32 reference, ครึ่งหนึ่งเทียบกับ fp16 ที่คุณจะ deploy จริง และไม่มีต้นทุนที่ตรวจจับได้ INT8 ที่ทำ ลวก ๆ ก็เกือบฟรีเช่นกัน: หนึ่ง scale ต่อ matrix เสีย perplexity 0.5 points และไม่เสียคำตอบใน battery แปด bits ให้อภัยมากพอที่ granularity แทบไม่สำคัญ ซึ่งเป็นเหตุผลที่ผู้คน generalise จาก INT8 ไป INT4 แล้วเจ็บตัว
INT4 ที่มีหนึ่ง scale ต่อ tensor ทำลายโมเดล Perplexity 365 ล้าน: ไม่ใช่ degraded แต่ annihilated จากนั้น granularity คือทั้งเกม — per-tensor 365,416,000, per-row 46.18, per-group-of-128 31.08, NF4 24.55 สี่ bits ต่อ weight เหมือนกัน แต่ต่างกันสิบห้าล้านเท่าระหว่างแย่สุดกับดีที่สุด
Perplexity เป็นเครื่องมือหยาบ และ battery ก็หยาบกว่า ระหว่าง NF4 กับ group-128 INT4 ช่องว่าง perplexity คือ 6.5 points และ battery ต่างกันหนึ่งคำถาม — และ confidence interval ของบทที่ 4 บอกว่าหนึ่งคำถามจากสิบหกไม่แยกแยะอะไรเลย มี demonstration ที่คมกว่า interval: รัน battery เดิมโดยปิด stock repetition penalty ของโมเดล ซึ่งคือสิ่งที่ greedy decoding หมายถึงจริง ๆ แล้วสองแถวนั้นสลับที่กัน หนึ่งคำถามจากสิบหกไม่ใช่ effect เล็ก มันคือไม่มี effect คำเตือนของ บทที่ 8 ก็ใช้ได้ด้วย: perplexity เปรียบเทียบได้เฉพาะระหว่างโมเดลที่ใช้ tokenizer ร่วมกัน ดังนั้นตัวเลขจาก write-up ของคนอื่นเอามาเทียบกับของคุณไม่ได้
คอลัมน์ agreement คมที่สุดในสามอย่าง และแทบฟรี: รันโมเดล full-precision แบบ greedy แล้วถามโมเดล quantized ที่ทุกตำแหน่งว่ามันจะเลือกอะไรเมื่อให้ prefix เดียวกัน มันมี observations อิสระ 144 จุดแทนที่จะเป็น 16 ไม่ต้องมี ground truth และ degrade อย่างราบรื่นในจุดที่ battery degrade เป็นขั้น ๆ มันยังเป็นปริมาณพอดีที่ส่วนถัดไปต้องใช้
นี่คือคำสัญญาที่ บทที่ 1 ให้ไว้เกี่ยวกับบทนี้ มาถึงตรงเวลา: คณิตศาสตร์บอกว่าโมเดล 4-bit เป็นไปได้ และ engineering ตัดสินว่ามันใช้งานได้หรือไม่
Speculative decoding
ลิงก์ไปยังส่วน: Speculative decodingบทที่ 12 ประกาศเรื่องนี้ไว้และทิ้งบิลไว้ที่นี่
ไอเดียมาจากการแยก prefill/decode ตรง ๆ การ verify sequence ที่ ถูกเสนอ จำนวน token ใช้ forward pass หนึ่งครั้งบน positions — matrix-matrix product ที่แทบไม่แพงกว่าการ pass บนหนึ่งตัว ดังนั้น:
โมเดลเล็กที่ถูกกว่า generate candidate tokens ตัวแบบ autoregressive
Verify
ลิงก์ไปยังส่วน: Verifyโมเดลใหญ่รัน forward pass หนึ่งครั้งบน candidates ทั้ง ตัวพร้อมกัน สร้างสิ่งที่มันจะพูดในแต่ละตำแหน่ง
Accept
ลิงก์ไปยังส่วน: Acceptเก็บ prefix ที่ยาวที่สุดซึ่งทั้งสองเห็นตรงกัน บวก token ที่โมเดลใหญ่ให้มาฟรี ณ จุดแรกที่ไม่ตรงกัน ทิ้งส่วนที่เหลือแล้วเริ่มใหม่
output distribution ไม่เปลี่ยน ด้วย greedy decoding นั่นชัดเจน — token จะถูก accept ก็ต่อเมื่อ target จะผลิตมันเอง ด้วย sampling ต้องใช้ acceptance rule ที่ดัดแปลง และ Leviathan et al. พิสูจน์ว่า distribution ที่ได้ตรงกับ target ทุกประการ10 นี่คือ exact optimisation อย่างที่สองในบทนี้
ดังนั้นทุกอย่างขึ้นกับ acceptance rate ซึ่งวัดได้ — มันคือคอลัมน์ agreement ด้านบน นั่นคือเหตุผลที่คำนวณมันไว้ตรงนั้น ใช้แต่ละโมเดล quantized เป็น draft ให้ target full-precision บน generated positions 144 จุด:
| draft model | acceptance | longest accepted run | expected tokens per target pass, |
|---|---|---|---|
| fp32 (the target itself) | 100.0 % | 48 | 5.00 |
| INT8 per row | 98.6 % | 48 | 4.86 |
| NF4 block 64 | 84.7 % | 20 | 3.69 |
| INT4 group 128 | 71.5 % | 13 | 2.85 |
| INT4 per row | 58.3 % | 7 | 2.24 |
| INT3 group 128 | 5.6 % | 2 | 1.06 |
| INT2 group 128 | 0.0 % | 0 | 1.00 |
จำนวน token ที่คาดว่าจะ accept ต่อ verification pass ที่ draft length คือ
และ net speedup หารด้วยต้นทุนของ draft เอง ซึ่งเป็น fraction ของ target ต่อ token:
| acceptance | , | , | , | , |
|---|---|---|---|---|
| 30 % | 1.19x | 1.02x | 0.79x | 0.79x |
| 50 % | 1.61x | 1.38x | 1.08x | 1.11x |
| 70 % | 2.31x | 1.98x | 1.54x | 1.78x |
| 90 % | 3.41x | 2.93x | 2.28x | 3.40x |
รายการตัวหนาคือสิ่งที่ต้องจำ: speculative decoding อาจทำให้ generation ช้าลงได้ ที่ acceptance 30 % และ draft มีต้นทุนหนึ่งในห้าของ target คุณจ่าย forward pass ห้าครั้งแล้วเก็บได้ 1.4 token คอลัมน์สุดท้ายคือกับดักอีกอย่าง — draft ที่ยาวขึ้นช่วยเฉพาะเมื่อ acceptance สูง เพราะหางของการเดา -token แทบไม่มีวันไปถึง ที่ acceptance 90 % คุ้ม 3.40x และที่ 30 % มันคุ้ม 0.79x: configuration เดียวกัน เป็นกำไรหรือขาดทุนขึ้นกับตัวเลขที่วัดบน traffic ของคุณ
Distillation และสิ่งที่ soft label พกมา
ลิงก์ไปยังส่วน: Distillation และสิ่งที่ soft label พกมาQuantization ย่อโมเดลโดยเก็บ function เดิมด้วย bits ที่น้อยลง Distillation ย่อมันโดย train โมเดลที่เล็กกว่าให้เลียนแบบโมเดลที่ใหญ่กว่า11 — ไอเดียที่มาก่อน deep learning เกือบทศวรรษ12
ส่วนละเอียดอ่อนคือ student เรียนจาก อะไร ไม่ใช่คำตอบที่ถูกต้อง: มัน train จากสิ่งนั้นโดยตรงได้อยู่แล้ว สิ่งที่ teacher เพิ่มให้คือ distribution ทั้งหมด ถามโมเดลว่าอะไรตามหลัง phrase หนึ่ง แล้วมองเลย argmax ไป:
"She poured the milk into the"
' jug' 0.1355 ' cup' 0.1051 ' bowl' 0.0605 ' large' 0.0380 ' milk' 0.0360hard label บอกว่า jug และไม่มีอะไรอื่น soft label บอกว่า jug และยังบอกด้วยว่า cup เกือบดีพอกัน, bowl plausible และ large — adjective ซึ่งเป็น grammatical continuation คนละแบบโดยสิ้นเชิง — ยังมีชีวิตอยู่ นั่นคือข้อโต้แย้งดั้งเดิม: นี่คือเลข 7 แต่ดูคล้ายเลข 1 อยู่ไม่น้อย และความคล้ายนั้นคือข้อมูลที่ hard label โยนทิ้ง
นี่ก็เป็นเหตุผลที่ distillation ใช้ temperature การหาร logits ด้วย ก่อน softmax ทำให้ distribution แบนลง และเพิ่มน้ำหนักสัมพัทธ์ของตัวรองลงมา: บน phrase นี้ ratio ระหว่าง top token กับตัวที่สามลดจาก 2.24 ที่ เป็น 1.50 ที่ — รากที่สองของตัวแรก ซึ่งคือสิ่งที่การหาร logits ด้วยสองทำกับ ratio ลำดับเดิม แต่ loss ให้ attention กับ near misses มากขึ้น gradient ของ student พก uncertainty ของ teacher ไม่ใช่แค่ verdict ของมัน
อะไรใส่ได้ใน 8, 16 และ 24 GB
ลิงก์ไปยังส่วน: อะไรใส่ได้ใน 8, 16 และ 24 GBทุกอย่างในบทนี้ตอนนี้คือผลบวกเดียว:
โดยที่ คือ token รวม ที่ resident ข้าม concurrent requests ทั้งหมด เมื่อนำไปใช้: แถว 7B และ 70B สมมติ 8 key-value heads ขนาด dimension 128 ส่วนแถว 13B ใช้ full multi-head attention ที่มี 40 heads ซึ่งเป็นวิธีที่โมเดล generation เหล่านั้นถูกสร้าง — และมันแสดงให้เห็น
8 GB
| model | precision | weights | free after overhead | context tokens that fit |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | does not fit | — |
| 7B | int8 | 6.5 GB | does not fit | — |
| 7B | int4 (g128) | 3.4 GB | 3.1 GB | 25,710 |
| 13B | int4 (g128) | 6.2 GB | 0.3 GB | 337 |
| 70B | int4 (g128) | 33.6 GB | does not fit | — |
16 GB
| model | precision | weights | free after overhead | context tokens that fit |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | 1.5 GB | 11,972 |
| 7B | int8 | 6.5 GB | 8.0 GB | 65,378 |
| 7B | int4 (g128) | 3.4 GB | 11.1 GB | 91,246 |
| 13B | int8 | 12.1 GB | 2.4 GB | 3,136 |
| 13B | int4 (g128) | 6.2 GB | 8.3 GB | 10,822 |
24 GB
| model | precision | weights | free after overhead | context tokens that fit |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | 9.5 GB | 77,508 |
| 7B | int8 | 6.5 GB | 16.0 GB | 130,914 |
| 7B | int4 (g128) | 3.4 GB | 19.1 GB | 156,782 |
| 13B | int8 | 12.1 GB | 10.4 GB | 13,622 |
| 13B | int4 (g128) | 6.2 GB | 16.3 GB | 21,308 |
| 70B | int4 (g128) | 33.6 GB | does not fit | — |
ดูแถว 13B ในตาราง 8 GB weights ใส่ได้ — 6.2 GB จาก 8 — ดังนั้นตามวิธีพูดทั่วไป โมเดล 13B “รันบนการ์ด 8 GB” มันมี context 337 token ซึ่งไม่ใช่บทสนทนา แต่แทบจะเป็น prompt เท่านั้น “มันใส่ได้ไหม” เป็นคำถามที่ผิด คำถามที่ถูกคือ “ด้วย context เท่าไร และสำหรับ user พร้อมกันกี่คน”
ดูสองแถว int8 ของ 16 GB ด้วย 7B ได้ 65,378 token และ 13B ได้ 3,136 — ต่างกันยี่สิบเท่าจาก weights เพิ่ม 5.6 GB เพราะ 13B ตรงนี้มี multi-head attention และ cache ของมันมีต้นทุน 800 KB ต่อ token เทียบกับ 128 KB ของ 7B โมเดลสองตัวที่ขนาดใกล้กัน ตัวหนึ่งใช้ long context ไม่ได้ ด้วยเหตุผลที่ไม่ปรากฏใน headline ของ model card ใด ๆ
ต่อไปจะไปไหน
ลิงก์ไปยังส่วน: ต่อไปจะไปไหนสิบสามบทก่อน นี่คือ perceptron ที่มี weights สองตัวและ bias หนึ่งตัว ตอนนี้มันเป็น transformer ที่ถูกออกแบบ, trained, aligned, สอนให้ใช้ compute กับคำถามยาก และ served ด้วยต้นทุนต่อ token ที่วัดแล้ว — ไม่มี box ใดในนั้นที่ยังไม่ถูกเปิด
มันจบตรงนี้ และจบอย่างตั้งใจ
บทที่ 14 เริ่มด้วยโมเดลที่อยู่ที่อื่น ไม่ได้อยู่ใน process ของคุณ ไม่ได้อยู่ใน memory ของคุณ ไม่ได้อยู่ใน variable ที่คุณ print ได้: มันอยู่บนเครื่องที่คุณไม่ได้ administer อยู่หลัง API key, port และ bill ทุกอย่างที่วัดตรงนี้ยังเกิดขึ้นอยู่ — prefill ยังรันก่อน token แรก cache ยังโตตามบทสนทนา batch ที่คุณอยู่ในนั้นยังเป็นของคนอื่นและยังตัดสิน latency ของคุณ — แต่จากนี้ไปคุณสังเกตมันผ่าน stream ของ Server-Sent Events, finish_reason และ HTTP 429 ที่มี header Retry-After คำถามเปลี่ยนตาม vantage point: ไม่ใช่ gradient นี้คำนวณอย่างไร แต่เป็น ทำไม invoice ของฉันถึงเพิ่มสามเท่า ภาษาก็เปลี่ยนด้วย และบทที่ 14 อธิบายกฎนั้นแทนที่จะประกาศมัน — ถึงตรงนี้ code ถือ weights, gradients, logits และ bytes ของ tokenizer; จากตรงนั้นเป็นต้นไปมันถือ connection, retry, cancellation และ accumulated state สิบสามบทที่คุณผ่านมาข้างหลังไม่ได้ถูกทิ้งเมื่อข้ามฝั่ง มันคือคำอธิบายของสิ่งที่กำลังรันอยู่หลัง port อีกด้าน
แหล่งที่มาและวิธีการ
ลิงก์ไปยังส่วน: แหล่งที่มาและวิธีการการละไว้สองอย่างเป็นความตั้งใจ FlashAttention (Dao et al., arXiv:2205.14135) ไม่ใช่ attention อีกแบบ — มันคำนวณ function เดิมโดย tile operation เพื่อไม่ให้ score matrix ถูกเขียนลง memory ซึ่งเป็นเหตุผลที่ 67 MB ในตารางที่สองของบทนี้เล็กกว่าในทางปฏิบัติเมื่อเทียบกับที่เลขคณิตบอก และ kernels เองถูกมอบหมายต่อ: lecture 10 ของ CS336 จาก Stanford ครอบคลุม inference systems ในความลึกที่บทนี้ไม่ได้พยายามทำ และ repository llama.cpp กับ GGUF specification คือแหล่งหลักสำหรับฝั่ง CPU
รายการอ้างอิง
ลิงก์ไปยังส่วน: รายการอ้างอิง-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). paper นี้โดยมากคือข้อโต้แย้งเรื่อง memory-bandwidth และอ่านได้แบบนั้น ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). รวม uptraining recipe ที่แปลง checkpoint multi-head ที่มีอยู่ ซึ่งเป็นเหตุผลที่ GQA แพร่เร็วมาก ↩
-
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. แนะนำ iteration-level scheduling — continuous batching — และ selective batching ↩
-
Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. paper ที่ vLLM สร้างอยู่บนมัน; §3 คือ analogy กับ operating systems แบบเต็ม ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). NF4 ถูกนิยามใน §3; ค่าระดับทั้งสิบหกที่ใช้ในการวัดด้านบนคือค่าที่ paper นี้ derive ↩
-
Dettmers, T., Lewis, M., Belkada, Y. and Zettlemoyer, L. LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale. arXiv:2208.07339 (2022). การวิเคราะห์ outlier-feature ใน §4 คือแหล่งที่มาของปรากฏการณ์ที่วัดด้านบน รวมถึงข้อค้นพบว่า outliers เกิดขึ้นอย่างเป็นระบบเมื่อ scale ใหญ่ขึ้น ↩ ↩2
-
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). ↩
-
Frantar, E., Ashkboos, S., Hoefler, T. and Alistarh, D. GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers. arXiv:2210.17323 (2022). ↩
-
Lin, J. et al. AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration. arXiv:2306.00978 (2023). ↩
-
Leviathan, Y., Kalman, M. and Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192 (2022). Theorem 1 คือ proof ว่า output distribution ไม่เปลี่ยน; Chen et al. (arXiv:2302.01318) เผยแพร่ไอเดียเดียวกันอย่างอิสระ ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). temperature และข้อโต้แย้งเรื่อง “dark knowledge” ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation เก้าปีก่อนหน้า สำหรับ ensembles แทนที่จะเป็น transformers ↩