تخطَّ إلى المحتوى
13/30الفصل 13 من 30

جعل inference رخيصًا: KV cache وbatching والتكميم

النموذج نفسه يجيب بالمخرجات نفسها خلال 8.8 ثوانٍ أو 78.9. ثم INT4 مقاس بثلاث طرق، لا مجرد ادعاء.

في هذه الصفحة

النموذج نفسه، على الجهاز نفسه، يجيب عن السؤال نفسه باستخدام 48 token نفسها. المخرجان متطابقان token مقابل token — تم التحقق من ذلك، لا افتراضه.

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)

تغيّرت وسيطة واحدة: use_cache=False. لا شيء في النموذج أو prompt أو sampling أو الحساب مختلف، والتشغيل الثاني ليس أدق مقابل هذا العناء. إنه أبطأ تسع مرات بلا مقابل.

هذا هو شكل هذا الفصل. كل ما فيه — cache، وbatch، والأوزان المكمّمة — محاولة للتوقف عن الدفع مقابل عمل لا يغيّر الإجابة، أو لمعرفة تكلفة إجابة أرخص. رسّخ الفصل 10 قائمة أسعار التدريب. وهذه قائمة أسعار الجانب الذي تدفع له إلى الأبد: نموذج منشور ينفق تقريبًا 2N2N FLOPs مقابل كل token يصدره، في كل طلب، لبقية عمره.

لتوليد token، يأخذ decoder-only transformer التسلسل كله حتى الآن، ويمرره عبر كل طبقة، ويقرأ توزيع الاحتمالات من الموضع الأخير. ثم يضيف token المختارة ويفعل ذلك مرة أخرى. هذا الوصف صحيح، وهو ما يفعله التشغيل البطيء.

لكنه أيضًا مبذّر على نحو هائل، والسبب هو causal mask من الفصل 9. تُحسب متجهات key وvalue للموضع 7 من إدخال الموضع 7 والمواضع التي قبله. عندما يصل الموضع 8، لا يستطيع الموضع 7 رؤيته — فهذا معنى causal — لذلك تكون key وvalue للموضع 7 الأرقام نفسها تمامًا كما كانت من قبل. ومع ذلك يعيد التشغيل البطيء حسابها في كل خطوة.

لذا خزّنها. هذا المخزن هو key-value cache، أهم تحسين منفرد في تشغيل نماذج اللغة:

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)

انظر إلى ما يُغذّى إلى النموذج داخل الحلقة: nxt، token واحدة. ليس التسلسل. query الخاصة بـtoken الجديدة تعمل attention على كل key مخزّنة، والـkeys المخزّنة لم تكن لتتغير أصلًا. هذا ليس تقريبًا — ففحص التطابق أعلاه هو بيت القصيد. cache لا يقايض الجودة بالسرعة؛ بل يحذف حسابًا زائدًا.

لرؤية التوسع بوضوح، أزل transformer وقِس رأس attention واحدًا مع d=64d = 64، خطوة واحدة من التوليد محسوبة بالطريقتين:

tokens في contextإعادة حساب كل شيءمع cacheالنسبةمصفوفة الدرجات
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

العمود الأيمن هو السبب. إعادة الحساب تبني مصفوفة attention الكاملة n×nn \times n في كل خطوة — وهي O(n2)O(n^2) من صندوق الترميز التقاربي في الفصل 9، تُدفع مرة لكل token. مع cache تبني صفًا 1×n1 \times n بدلًا من ذلك: عند 4,096 tokens، لدينا 67 MB من الدرجات مقابل 16 KB.

عدّ عمليات multiply-accumulate بدلًا من المللي ثانية يزيل الجهاز من الحجة. لتوليد TT tokens من بداية باردة:

tokens مولّدةمع cacheإعادة الحسابالنسبة
1282.6 M192.0 M73x
51223.1 M7.36 G318x
2048293.7 M392.6 G1,336x

في كل خطوة تكون النسخة ذات cache خطية في context، أما غير المخزّنة فتربيعية؛ وعند جمعها على توليد كامل، O(T2)O(T^2) مقابل O(T3)O(T^3)، مع نسبة تنمو بلا حد. الفرق التساعي في البداية قيس على 48 tokens — أقصر من الصف الأول في ذلك الجدول.

يغيّر cache أيضًا ما يجب أن يكون في الذاكرة. على GPU حاسوب محمول بسعة 8 GB يولّد 256 tokens في fp16، مع أخذ ذروة المخصّص وطرح الأوزان المقيمة:

ذروة ذاكرة العمل
مع cache21.8 MB
إعادة الحساب181.7 MB

ذاكرة أكثر بـ8.3 مرات، تُنفق لإنتاج tokens نفسها ببطء أكبر. هذا هو الوعد الذي قُطع في الفصل 5، لكنه يصل من اتجاه غير متوقع: هناك، كان reverse-mode autodiff مضطرًا لإبقاء كل وسيط حيًا من أجل backward pass، وكانت activations تهيمن على ذاكرة التدريب. في inference لا توجد backward pass ولا شيء للاحتفاظ به من أجلها — لذلك ما يهيمن على الذاكرة بدلًا من ذلك هو cache، وهو اختيار مقصود لا تكلفة لا مفر منها.

انظر مجددًا إلى التشغيل السريع: تصرّفت token الأولى فيه على نحو مختلف عن السبع والأربعين الأخرى.

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

كلّف prompt نحو 25.6 ms لكل token، وكل token مولّدة كلّفت 166 ms. النموذج نفسه، العتاد نفسه، الأوزان نفسها، فرق سداسي لكل token — وبالاتجاه الذي لا يتوقعه معظم الناس. prompt هو الجزء الرخيص. ينقسم التوليد إلى مرحلتين بفيزياء مختلفة فعلًا:

تمريرة أمامية واحدة على prompt كله. تُعالَج كل token بالتوازي، لذلك تُحمّل كل مصفوفة أوزان من الذاكرة مرة واحدة وتُضرب في مصفوفة من مئات متجهات tokens — ضرب مصفوفة-مصفوفة، مع الكثير من الحساب لكل بايت منقول، وهذا ما صُمّم GPU لأجله. Prefill compute-bound، وتكلفته تقريبًا خطية في طول prompt.

تمريرة أمامية واحدة لكل token، batch من واحد وتسلسل من واحد. لا تزال كل مصفوفة أوزان تُحمّل كاملة من الذاكرة، وتُضرب في متجه واحد — ضرب مصفوفة-متجه، مع حساب قليل جدًا لكل بايت منقول. Decode memory-bandwidth-bound، وتكلفته لكل token لا تعتمد إلا قليلًا على طول context.

يمكن قياس النصفين. Prefill، تمريرة واحدة على PP tokens:

prompt tokensالثوانيms لكل token
160.351521.97
320.525416.42
641.049116.39
1281.655212.93
2563.096512.10

Decode، token واحدة أمام cache من CC:

tokens مخزّنةms لـtoken واحدة
16110.05
6497.57
256108.53
1024103.86

اقرأ الجدول الثاني مرتين. الانتقال من 16 tokens من context إلى 1,024 — تاريخ أطول بأربع وستين مرة ليجري attention عليه — لم يغيّر تكلفة الخطوة بأي مقدار قابل للقياس. attention على cache عمل حقيقي، لكنه يتضاءل أمام التكلفة الثابتة لسحب نصف مليار وزن عبر ناقل الذاكرة لإنتاج متجه واحد. تلك التكلفة الثابتة هي سبب كل ما في القسم التالي.

هاتان المرحلتان هما أصل الرقمين اللذين يبلّغ عنهما كل نظام serving. Time to first token هو في جوهره prefill، وينمو مع prompt، ولذلك تبدو المحادثة الطويلة بطيئة في البداية. Tokens per second هي 1/decode step1/\text{decode step}، وهي ثابتة تقريبًا، ولذلك يتدفق الرد بعدها بسلاسة. محادثة تبدأ ببطء ثم تُبث بسلاسة ليست خدعة عرض. إنها هذان الجدولان.

يقايض cache الحساب بالذاكرة، والذاكرة التي يطلبها ليست صغيرة. لكل token في context، تحتفظ كل طبقة بمتجه key ومتجه value لكل رأس key-value:

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}

الرقم 2 هو من أجل keys وvalues؛ وكل ما عداه من بنية النموذج. بالنسبة للنموذج المقاس في هذا الفصل كله — 24 طبقة، و14 query heads، و2 key-value heads، وبُعد الرأس 64 — في fp16 يكون ذلك 2×24×2×64×2=12,2882 \times 24 \times 2 \times 64 \times 2 = 12{,}288 بايت لكل token.

للمعادلات في هذا المجال عادة أن تخطئ بعامل اثنين، لذلك افحصها أمام المخصّص بدل تصديقها:

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

مطابق تمامًا، ويبقى مطابقًا عبر كل شكل جُرّب:

batchcontextcache المقاسالمتوقعذروة ذاكرة العمل
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

تستحق الصفوف الثلاثة الأخيرة نظرة ثانية. اثنان وثلاثون مستخدمًا مع 2,048 tokens لكل منهم، أربعة وستون مع 1,024، ومئة وثمانية وعشرون مع 512 — cache تساوي 768 MB في كل حالة، لأن الحالات الثلاث كلها تحتفظ بـ65,536 tokens. يعتمد cache فقط على العدد الإجمالي لـtokens المقيمة، لا على كيفية توزيعها بين المستخدمين. هذه الحقيقة هي أساس قسم batching.

قدّم الفصل 9 multi-query وgrouped-query attention وأجّل السبب إلى هذا الفصل. السبب هو تلك المعادلة، وبالتحديد HkvH_{kv} فيها.

في multi-head attention القياسي، لكل query head رؤوس key وvalue خاصة به. النموذج هنا لديه 14 query heads؛ ومع multi-head attention كامل، كان cache الخاص به سيكون 2×24×14×64×2=86,0162 \times 24 \times 14 \times 64 \times 2 = 86{,}016 بايت لكل token — 84 KB بدلًا من 12 KB، أكثر بسبع مرات بالضبط، وهي نسبة query heads إلى key-value heads.

Multi-query attention1 يأخذ هذا إلى الحد الأقصى: كل query heads تشترك في key-value head واحد. Grouped-query attention2 هو التسوية التي انتصرت — حفنة من key-value heads، يشترك كل منها بين مجموعة من query heads — لأن خسارة جودة MQA كانت حقيقية وخسارة GQA ليست كذلك. لا يشتري أي منهما حسابًا. إنهما موجودان لقسمة تلك المعادلة على عدد صحيح، وانتشرا عبر الصناعة في اللحظة التي جعلت فيها السياقات الطويلة cache هو القيد الحاسم.

وهذا يحدث بسرعة. بالنسبة إلى نموذج من فئة 7B مع 32 طبقة و8 key-value heads بأبعاد 128، يكون cache 128 KB لكل token في fp16:

context tokensمستخدم واحد8 مستخدمين64 مستخدمًا
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

أوزان ذلك النموذج نفسه هي 13.0 GB في fp16، وهو الرقم في الجدول في نهاية هذا الفصل. لذلك عند context من 128,000-token، cache مستخدم واحد أكبر من النموذج. هذا هو الحساب الذي يحوّله الفصل 16 إلى مال، ولهذا ليست المحادثة الطويلة بطيئة فحسب — بل تحتل شريحة ثابتة من آلة ما طوال حياة الطلب.

Batching: الرقم الذي يصعد والرقم الذي يهبط

رابط إلى القسم: Batching: الرقم الذي يصعد والرقم الذي يهبط

Decode مقيّد بالذاكرة: تُسحب الأوزان عبر الناقل لإنتاج token واحدة، وتظل وحدات الحساب خاملة. إذن ضع عملًا أكثر في الخطوة نفسها. شغّل عدة طلبات معًا، والأوزان التي تُقرأ مرة واحدة تخدمها كلها. مقاسًا على النموذج نفسه، مع احتفاظ كل طلب بـcache من 64-token وفك token واحدة:

batchزمن التأخير لكل خطوةthroughputزمن التأخير مقابل 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

اقرأ العمودين الأيمنين معًا، لأنهما الفكرة كلها. الانتقال من طلب واحد إلى ستة عشر يضاعف throughput بمقدار 6.0 ويضاعف انتظار أي طلب فردي بمقدار 2.67. جعل batch الخادم أفضل وكل مستخدم أسوأ.

هذا ليس عيبًا يمكن ضبطه بعيدًا؛ إنه المقايضة نفسها، ولها اسم في كل جانب. Latency هو ما يختبره شخص ينتظر ردًا. Throughput هو ما تُقسّم عليه الفاتورة. لا يوجد إعداد يحسّن الاثنين معًا.

لاحظ أيضًا أين يتوقف. من 16 إلى 32، يكسب throughput نسبة 9 % بينما يكاد latency يتضاعف: توقفت الخطوة عن كونها memory-bound وأصبحت compute-bound، وبعد تلك الركبة لا يشتري batch شيئًا. لكل نشر ركبة كهذه؛ يجب قياس موضعها لديك، لكن وجودها لا يحتاج إلى قياس.

الطريقة الساذجة لعمل batch هي جمع BB طلبًا، وتشغيلها معًا، والإرجاع عندما تنتهي كلها. لكنها لا تنتهي معًا: بعض الردود عشرون token وبعضها خمسمئة. يستمر batch ثابت حتى ينتهي أطول عضو فيه، وكل طلب منتهٍ يظل يحتل خانته، مساهمًا في padding، حتى ذلك الحين.

خذ 64 طلبًا بتفاوت واقعي في أطوال المخرجات — الوسيط 18 tokens، والأطول 231، والمجموع 1,874 — وحاكِ السياستين بتكلفة الخطوة المقاسة لثماني خانات:

السياسةالزمن الجداريthroughputمتوسط latency لكل طلبslot-steps مهدرة
static batches of 8176.9 s10.6 tok/s83.2 s3,214
continuous, 8 slots109.0 s17.2 tok/s8.1 s0

يتحسن throughput بـ1.6x. ويتحسن متوسط latency بأكثر من عشر مرات، لأنه في static batching يظل طلب انتهى في أربع خطوات ينتظر جارًا من 231-token قبل أن يسمع أحد شيئًا عنه.

Continuous batching3 هو الإصلاح، وهو بسيط كما يبدو: batch ليس مجموعة بل مجموعة خانات، والخانة التي تتحرر تستقبل الطلب التالي في الطابور في الخطوة التالية مباشرة. يعمل المجدول على حبيبة token واحدة بدل طلب واحد. كل stack serving في الإنتاج يفعل ذلك الآن.

وله نصف ثانٍ، وهو cache. الخانات التي تأتي وتذهب تترك ذاكرة cache مجزأة، وحجز أقصى context ممكن لكل خانة يهدر معظم الحجز. PagedAttention4 يستعير الجواب من أنظمة التشغيل: خزّن cache في كتل ثابتة الحجم مع جدول كتل لكل تسلسل، بحيث يمكن أن يكون cache التسلسل مبعثرًا ماديًا بينما يظل متصلًا منطقيًا — وهذا يسمح أيضًا لتسلسلين لهما prefix مشترك بمشاركة الكتل التي تحتويه. هذا ما يُبنى عليه vLLM، ولهذا يكون محرك serving مخصّص ذاكرة ومعه transformer ملحق.

النصف الآخر من الفاتورة هو الأوزان نفسها. نصف مليار parameter بأربعة بايتات لكل منها يساوي 1.98 GB؛ وببايتين، 0.99 GB؛ وببايت واحد، 0.49 GB. عدد أقل من bits لكل وزن يصغّر النموذج على القرص، ويصغّره في الذاكرة، و— لأن decode مقيّد بعرض النطاق — يجعل كل خطوة أسرع، إذ توجد بايتات أقل لنقلها.

أبسط مخطط هو تكميم absolute-maximum متماثل، ويتسع في ثلاثة أسطر:

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

اختر scale بحيث يقابل أكبر وزن أكبر عدد صحيح، اقسم، قرّب، خزّن الأعداد الصحيحة وscale. أعد البناء بالضرب عكسيًا. لا شيء فيه ذكي، وهو يعمل — إلى أن لا يعمل.

مقاسًا على الأوزان الحقيقية للنموذج: كل مصفوفات الإسقاط الـ168، و357.8 مليون parameter، وخطأ نسبي WW^/W\lVert W - \hat{W}\rVert / \lVert W \rVert:

المخططمتوسط الخطأ النسبيأسوأ مصفوفة
INT8، scale واحد للمصفوفة كلها0.04000.1487
INT8، scale واحد لكل صف إخراج0.01000.0149
INT4، scale واحد للمصفوفة كلها0.60260.9931
INT4، scale واحد لكل صف إخراج0.17900.2589
INT4، scale واحد لكل مجموعة من 1280.13230.1992
NF4، scale واحد لكل كتلة من 640.09520.1205
INT3، scale واحد لكل مجموعة من 1280.30440.4123
INT2، scale واحد لكل مجموعة من 1280.77900.8076

الصف الرابع هو الانهيار. خطأ نسبي 0.99 في أسوأ مصفوفة يعني أن إعادة البناء لا تحتفظ عمليًا بأي شيء من الأصل — لقد استُبدلت المصفوفة بضجيج له المقدار الصحيح تقريبًا. يظهر السبب في التجربة نفسها على مصفوفة واحدة:

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 %)

وزن واحد من كل ستة آلاف يقع أبعد من ستة انحرافات معيارية، والأكبر على بُعد 24. مع scale واحد للمصفوفة كلها، ذلك الوزن الواحد يحدد حجم الخطوة لكل الأوزان الـ4.3 مليون. عند 8 bits توجد 256 خطوة ولا يزال الوزن النموذجي يقع على خطوة ذات معنى. عند 4 bits توجد 16 فقط، وتُحجز الأبعد لقيمة لا يملكها شيء تقريبًا، فتتقارب الأوزان العادية — وهي كلها تقريبًا — إلى مستويين أو ثلاثة فقط.

كل ما بعد ذلك الصف هو الإصلاح نفسه بحبيبات مختلفة: امنح scale منطقة أصغر. لكل صف إخراج يقسم الخطأ على 3.4؛ ولكل مجموعة من 128 وزنًا متتاليًا يقسمه مرة أخرى. التكلفة هي bookkeeping — scale من 16-bit لكل مجموعة من 128 يساوي 4+16/128=4.1254 + 16/128 = 4.125 bits لكل وزن بدلًا من 4 — ويستعيد معظم الفجوة.

NF4 يهاجم المسألة من الجانب الآخر.5 لا يلزم أن تكون المستويات متباعدة بالتساوي. الأوزان داخل كتلة موزعة تقريبًا طبيعيًا، لذلك اختر المستويات الستة عشر ككوانتايلز لتوزيع طبيعي: كثيفة قرب الصفر حيث توجد الأوزان فعلًا، ومتباعدة في الأطراف حيث لا توجد. bits الأربعة نفسها، وblock scaling نفسه، عند كتلة أصغر — 4.25 bits لكل وزن مقابل 4.125 لمجموعة 128 — وينخفض الخطأ المقاس من 0.1323 إلى 0.0952، أي أقل بـ28 %. جزء من ذلك هو الكتلة الأدق، والباقي وضع المستويات حيث توجد الكتلة الاحتمالية، وفصل الاثنين يحتاج صفًا ثالثًا.

انتهى صندوق floating-point في الفصل 2 بوعد: أن هذا الفصل سيكمّم الأوزان إلى 8 و4 bits ويجد حفنة من features الشاذة التي ترفض الانضغاط. ها هي، وهي تشرح لماذا لم يكن «قرّب الأرقام فقط» لينجح أبدًا على activations.

كانت الأوزان أعلاه سيئة السلوك. أما activations فهي في دوري آخر. خذ prompt عاديًا من 84-token، والتقط residual stream في كل طبقة، وقِس أكبر مقدار تصل إليه كل بُعد من الأبعاد الـ896:

الطبقةأكبر |h|أكبر |h| لبُعد الوسيطالنسبةأبعاد فوق 6x الوسيط
16.190.33918x2
41543.481.550996x34
81571.631.4981049x36
121575.031.5461019x34
161579.601.617977x32
201577.982.361668x24
24204.4410.76019x12

يصل البُعد 62 إلى 1,579.6 بينما لا يتجاوز بُعد الوسيط 1.6 أبدًا. ليست مصادفة token واحدة أو طبقة واحدة: البُعد نفسه موجود عند الطبقة 4 ولا يزال موجودًا عند الطبقة 20، بالقيمة نفسها تقريبًا. هذه هي outlier features،6 وهي منهجية — خاصية للنموذج المدرّب، لا للمدخل.

يوضح histogram لتلك القيم العظمى لكل بُعد من الأبعاد الـ896 عند الطبقة 16 الشكل بلا لبس:

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

تسعمئة بُعد في كومة مرتبة تحت 8، ولا شيء إطلاقًا لثلاثة octaves، ثم بُعد واحد وحيد في الطرف البعيد. الآن كمّم ذلك tensor إلى INT8 واحسب ما يحدث:

المخططالخطأ النسبيمستويات الأعداد الصحيحة المميزة المستخدمة، tensor كامل
scale واحد لـtensor كله0.108314 من 256
scale واحد لكل token (لكل صف)0.0433158
tensor كامل، بُعد شاذ واحد محفوظ في fp320.044248
tensor كامل، 4 أبعاد شاذة محفوظة في fp320.027957
tensor كامل، 16 بُعدًا شاذًا محفوظًا في fp320.0085102

أربعة عشر مستوى من أصل 256. ضُبط scale بواسطة 1,579.6، لذلك عرض كل خطوة 12.44، والـactivation النموذجي — مقدار الوسيط 0.26، والمئين التاسع والتسعون 2.51 — ليس لديه مكان يهبط فيه. على مستوى البُعد يكون الأمر أوضح:

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

مستوى واحد. البُعد كله، كل token، كُمّم إلى الرقم نفسه. خُصصت ثمانية bits واستُخدم تقريبًا صفر منها، والنموذج الذي يقرأ تلك activations يُسلّم ثابتًا.

ذلك القياس هو التبرير لكل تقنية يستخدمها الناس فعليًا:

أبقِ القيم الشاذة خارج الأمر. LLM.int8()6 يفكك ضرب المصفوفات: الأبعاد ذات المقادير المتطرفة تُحسب بـ16 bits، وكل ما عداها في INT8، ثم يُجمع النصفان. الجدول أعلاه هو الإيصال — إزالة أربعة أبعاد تقطع الخطأ بعامل يقارب أربعة. SmoothQuant7 ينقل الصعوبة بدلًا من ذلك: اقسم activations على عامل لكل قناة واضرب عمود الوزن المطابق فيه، فيبقى الناتج دون تغيير وينتقل الشذوذ من tensor لا يستطيع امتصاصه إلى tensor يستطيع ذلك.

اختر التقريب، لا تكتفِ بالتقريب. لا يسأل أي مما سبق عمّا تفعله المصفوفة. GPTQ8 يكمّم عمودًا بعمود، وبعد كل عمود يضبط الأعمدة المتبقية كاملة الدقة لتعويض الخطأ المرتكب بالفعل — فيقلل خطأ إخراج الطبقة على مدخلات حقيقية بدل خطأ أوزانها. AWQ9 يلاحظ أن جزءًا صغيرًا من قنوات الوزن أهم بكثير من البقية، يعثر عليها من إحصاءات activation، ويكبّرها قبل التكميم كي تهبط على مستويات أدق. كلاهما يحتاج مجموعة معايرة؛ ولا يحتاج أي منهما gradients.

عرض التفاصيل

GGUF، وما علاقة صيغة ملف بكل هذا.

GGUF ليس طريقة تكميم؛ إنه الحاوية التي يستخدمها llama.cpp، والالتباس في مقارنات gguf vs gptq يأتي من التعامل مع الاثنين كأنهما من النوع نفسه. يحتوي GGUF على tensors، وtokenizer، وبيانات metadata للبنية، وchat template في ملف واحد قابل للـmemory mapping، ويحمل عائلة من مخططات الكتل داخله — أسماء مثل Q4_K_M ترمز إلى bits لكل وزن، وحجم الكتلة، وما إذا كانت بعض tensors محفوظة بدقة أعلى.

الفارق الهندسي المهم: GPTQ وAWQ ينتجان أوزانًا محسّنة لـGPU kernel، بينما تُفك مخططات GGUF بتكلفة منخفضة على CPU مع ملف mapped بدل تحميله. ولهذا يوجد «نموذج 7B بـ4-bit» الاسمي نفسه في العالمين بأحجام مختلفة وجودة مختلفة، ولهذا لا تكون المقارنة الصادقة هي الصيغة أبدًا — بل القياس أدناه، مشغّلًا على مهمتك أنت.

تتوقف معظم المقالات عن quantization عند القسم السابق: تشرح الطريقة، وتقتبس نسبة ضغط، وتؤكد أن الجودة «محفوظة إلى حد كبير». كان الفصل 4 عن عدم خداع نفسك، فلنكتشف إذن.

النموذج نفسه، أوزان مكمّمة في موضعها بكل مخطط، ثم ثلاثة قياسات: perplexity على 2,048 tokens من نثر إنجليزي محجوز — هنا، مسودة هذه الدورة، ولذلك يستبدل المستودع كتابًا ثابتًا في الملكية العامة ويطبع جدولًا بالشكل نفسه مع أرقام مختلفة — وبطارية من 16 سؤالًا واقعيًا قصيرًا بإجابات معروفة تحت greedy decoding، ونسبة tokens التي يتفق فيها النموذج المكمّم مع نموذج الدقة الكاملة عند إعطائه context مطابقًا.

المخططمتوسط خطأ الوزنperplexityبطارية الأسئلةيتفق مع fp32
fp32 (مرجع)0.000023.0813/16100.0 %
INT8 لكل tensor0.040023.5813/16
INT8 لكل صف0.010022.9613/1698.6 %
INT4 لكل tensor0.6026365,416,0000/16
INT4 لكل صف0.179046.186/1658.3 %
INT4 مجموعة 1280.132331.0810/1671.5 %
NF4 كتلة 640.095224.5511/1684.7 %
INT3 مجموعة 1280.3044213.090/165.6 %
INT2 مجموعة 1280.779026,325,4360/160.0 %

تستحق أربعة أمور في ذلك الجدول أن تُقال بوضوح.

INT8 إذا نُفّذ جيدًا فهو مجاني. يسجل INT8 لكل صف 22.96 مقابل 23.08 للمرجع — فجوة جزء واحد من مئتين، وهي ضجيج ويجب قراءتها كـ«مطابقة». اتجاه الضجيج غير ثابت: على corpus الملكية العامة في المستودع، يخرج المخططان نفسيهما 22.24 مقابل 22.18: نصف تلك المسافة، وبالاتجاه الآخر. يتفق مع نموذج الدقة الكاملة في 142 من 144 tokens مولّدة. ربع الذاكرة مقابل مرجع fp32، ونصفها مقابل fp16 الذي ستنشره فعليًا، ودون تكلفة قابلة للكشف. INT8 المنفّذ بإهمال شبه مجاني أيضًا: scale واحد لكل مصفوفة يكلّف 0.5 نقطة perplexity ولا أي إجابة من البطارية. ثمانية bits متسامحة بما يكفي بحيث لا تكاد الحبيبية تهم، وهذا بالضبط سبب تعميم الناس من INT8 إلى INT4 ثم تضررهم.

INT4 مع scale واحد لكل tensor يدمّر النموذج. Perplexity 365 مليونًا: ليس متدهورًا، بل مبادًا. تصبح الحبيبية هي اللعبة كلها — لكل tensor 365,416,000، ولكل صف 46.18، ولكل مجموعة من 128 مقدار 31.08، وNF4 مقدار 24.55. bits الأربعة نفسها لكل وزن، وفارق قدره خمسة عشر مليون مرة بين الأسوأ والأفضل.

Perplexity أداة خشنة والبطارية أخشن. بين NF4 وINT4 مجموعة 128، فجوة perplexity هي 6.5 نقاط، والبطارية تختلف بسؤال واحد — وفاصل الثقة في الفصل 4 يقول إن سؤالًا واحدًا من ستة عشر لا يميز شيئًا على الإطلاق. هناك برهان أوضح من الفاصل: شغّل البطارية نفسها مع إيقاف عقوبة التكرار الافتراضية للنموذج، وهو ما يعنيه greedy decoding فعليًا، فتتبادل هاتان الصفان الأماكن. سؤال واحد من ستة عشر ليس أثرًا صغيرًا، بل لا أثر. ينطبق تحذير الفصل 8 أيضًا: perplexity لا تقارن إلا بين نماذج تشترك في tokenizer، لذلك لا يمكن مقارنة رقم من كتابة شخص آخر برقمك.

عمود الاتفاق هو الأدق بين الثلاثة، وهو شبه مجاني: شغّل نموذج الدقة الكاملة بطريقة greedy، ثم اسأل النموذج المكمّم، في كل موضع، ماذا كان سيختار مع prefix نفسه. لديه 144 مشاهدة مستقلة بدل 16، ولا يحتاج ground truth، ويتدهور بسلاسة حيث تتدهور البطارية بقفزات. وهو أيضًا بالضبط الكمية التي يحتاجها القسم التالي.

هذا هو الوعد الذي قطعه الفصل 1 بشأن هذا الفصل، وقد وصل في موعده: الرياضيات تقول إن نموذج 4-bit ممكن، والهندسة تقرر ما إذا كان قابلًا للاستخدام.

أعلن الفصل 12 هذا وترك الفاتورة هنا.

تأتي الفكرة مباشرة من انقسام prefill/decode. التحقق من تسلسل مقترح من γ\gamma tokens يكلّف تمريرة أمامية واحدة على γ\gamma مواضع — ضرب مصفوفة-مصفوفة، أغلى بالكاد من تمريرة على واحد. إذن:

نموذج صغير ورخيص يولّد γ\gamma tokens مرشحة autoregressively.

يشغّل النموذج الكبير تمريرة أمامية واحدة على كل المرشحين γ\gamma دفعة واحدة، منتجًا ما كان سيقوله في كل موضع.

احتفظ بأطول prefix يتفق عليه الاثنان، إضافة إلى token التي يوفرها النموذج الكبير مجانًا عند أول اختلاف. تخلّص من الباقي وابدأ من جديد.

يبقى توزيع المخرجات دون تغيير. مع greedy decoding هذا واضح — لا تُقبل token إلا إذا كان الهدف سينتجها. ومع sampling يتطلب الأمر قاعدة قبول معدلة، ويثبت Leviathan وآخرون أن التوزيع الناتج هو توزيع الهدف تمامًا.10 هذا هو التحسين الدقيق الثاني في هذا الفصل.

لذلك يتوقف كل شيء على معدل القبول α\alpha، وهو قابل للقياس — إنه عمود الاتفاق أعلاه، ولذلك حُسب هناك. باستخدام كل نموذج مكمّم كـdraft لهدف الدقة الكاملة، عبر 144 موضعًا مولّدًا:

draft modelالقبولأطول سلسلة مقبولةtokens متوقعة لكل تمريرة هدف، γ=4\gamma = 4
fp32 (الهدف نفسه)100.0 %485.00
INT8 لكل صف98.6 %484.86
NF4 كتلة 6484.7 %203.69
INT4 مجموعة 12871.5 %132.85
INT4 لكل صف58.3 %72.24
INT3 مجموعة 1285.6 %21.06
INT2 مجموعة 1280.0 %01.00

عدد tokens المتوقع قبولها لكل تمريرة تحقق، عند طول draft γ\gamma، هو

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

والتسريع الصافي يقسم ذلك على تكلفة draft نفسه، وهي كسر cc من الهدف لكل token:

القبولc=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

الخانة الغامقة هي ما يجب تذكره: يمكن لـspeculative decoding أن يجعل التوليد أبطأ. عند قبول 30 % مع draft يكلّف خمس الهدف، تدفع مقابل خمس تمريرات أمامية وتحتفظ بـ1.4 tokens. العمود الأخير هو الفخ الآخر — draft أطول لا يساعد إلا عندما يكون القبول عاليًا، لأن ذيل تخمين من γ\gamma-token نادرًا ما يُبلَغ. عند قبول 90 % تكون γ=8\gamma = 8 بقيمة 3.40x، وعند 30 % بقيمة 0.79x: التكوين نفسه، ربح أو خسارة حسب رقم مقاس على حركة مرورك.

Quantization يصغّر نموذجًا بتخزين الدالة نفسها بعدد bits أقل. Distillation يصغّره بتدريب نموذج أصغر على تقليد نموذج أكبر11 — وهي فكرة تسبق deep learning بنحو عقد.12

الجزء الدقيق هو ما يتعلم منه الطالب. ليس الإجابة الصحيحة: كان يمكن تدريبه عليها مباشرة. ما يضيفه المعلّم هو التوزيع كله. اسأل النموذج ماذا يلي عبارة وانظر إلى ما بعد 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 jug ولا شيء غير ذلك. وتقول soft label jug، وكذلك إن cup كان جيدًا تقريبًا، وbowl معقول، وlarge — صفة، استمرار نحوي مختلف تمامًا — لا يزال حيًا. هذه هي الحجة الأصلية: هذه 7، لكنها تشبه 1 كثيرًا، والتشابه معلومة ترميها hard label بعيدًا.

ولهذا أيضًا يستخدم distillation درجة حرارة. قسمة logits على TT قبل softmax تسطّح التوزيع وترفع الوزن النسبي للوصيفين: في هذه العبارة، تنخفض النسبة بين أعلى token والثالثة من 2.24 عند T=1T = 1 إلى 1.50 عند T=2T = 2 — الجذر التربيعي للأولى، وهذا ما تفعله قسمة logits على اثنين بالنسبة إلى نسبة. الترتيب نفسه، لكن مزيد من attention الخسارة على الإخفاقات القريبة. gradient الطالب يحمل لا يقين المعلّم، لا حكمه فقط.

كل شيء في هذا الفصل أصبح الآن مجموعًا واحدًا:

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}}

حيث TT هي إجمالي tokens المقيمة عبر كل الطلبات المتزامنة. عند تطبيقها: صفوف 7B و70B تفترض 8 key-value heads بأبعاد 128، وصف 13B يفترض multi-head attention كاملًا مع 40 رأسًا، وهي الطريقة التي بُنيت بها تلك الأجيال من النماذج — وهذا يظهر.

8 GB

modelprecisionweightsالمتاح بعد overheadcontext tokens التي تتسع
7Bfp1613.0 GBلا يتسع
7Bint86.5 GBلا يتسع
7Bint4 (g128)3.4 GB3.1 GB25,710
13Bint4 (g128)6.2 GB0.3 GB337
70Bint4 (g128)33.6 GBلا يتسع

16 GB

modelprecisionweightsالمتاح بعد overheadcontext tokens التي تتسع
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

modelprecisionweightsالمتاح بعد overheadcontext tokens التي تتسع
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 GBلا يتسع

انظر إلى صف 13B في جدول 8 GB. الأوزان تتسع — 6.2 GB من 8 — لذلك، بالطريقة المعتادة في الكلام، «يعمل نموذج 13B على بطاقة 8 GB». لديه 337 tokens من context، وهذا ليس محادثة بل بالكاد prompt. «هل يتسع؟» هو السؤال الخطأ. السؤال الصحيح هو «بأي قدر من context، ولأي عدد من المستخدمين في الوقت نفسه؟».

انظر أيضًا إلى صفّي int8 في جدول 16 GB. يحصل 7B على 65,378 tokens ويحصل 13B على 3,136 — فرق عشريني من 5.6 GB من الأوزان الإضافية، لأن 13B هنا يستخدم multi-head attention وتكلفة cache الخاصة به 800 KB لكل token مقابل 128 KB لـ7B. نموذجان بحجم متقارب، أحدهما غير قابل للاستخدام مع context طويل، لسبب لا يظهر في عنوان أي model card.

قبل ثلاثة عشر فصلًا، كان هذا perceptron بوزنين وانحياز. أصبح الآن transformer صُمّم، ودُرّب، ووُفّق، وتعلّم إنفاق compute على الأسئلة الصعبة، ويُخدّم بتكلفة مقاسة لكل token — دون أن يبقى فيه صندوق غير مفتوح.

ينتهي ذلك هنا، وينتهي عن قصد.

يبدأ الفصل 14 والنموذج في مكان آخر. ليس في عمليتك، ولا في ذاكرتك، ولا في متغير يمكنك طباعته: بل على آلة لا تديرها، خلف API key، ومنفذ، وفاتورة. كل ما قيس هنا لا يزال يحدث — لا يزال prefill يعمل قبل أول token، ولا يزال cache ينمو مع المحادثة، ولا يزال batch الذي أنت فيه يخص شخصًا آخر ولا يزال يقرر latency لديك — لكنك من الآن فصاعدًا تراقبه عبر stream من Server-Sent Events، وfinish_reason، وHTTP 429 مع ترويسة Retry-After. تتغير الأسئلة مع زاوية النظر: ليس كيف يُحسب هذا gradient بل لماذا تضاعفت فاتورتي ثلاث مرات. وتتغير اللغة أيضًا، والفصل 14 يشرح تلك القاعدة بدل إعلانها — حتى هنا كان الكود يمسك أوزانًا، وgradients، وlogits، وبايتات tokenizer؛ ومن هناك يمسك اتصالًا، وإعادة محاولة، وإلغاءً، وحالة متراكمة. الفصول الثلاثة عشر خلفك لا تُرمى عند العبور. إنها وصف لما يعمل على الجانب الآخر من المنفذ.


إغفالان مقصودان. FlashAttention (Dao et al., arXiv:2205.14135) ليس attention مختلفًا — إنه يحسب الدالة نفسها بتقسيم العملية إلى tiles بحيث لا تُكتب مصفوفة الدرجات n×nn \times n إلى الذاكرة أبدًا، ولهذا تكون 67 MB في الجدول الثاني من هذا الفصل أصغر عمليًا مما توحي به الحسابات. أما kernels نفسها فمفوّضة: المحاضرة 10 من Stanford CS336 تغطي أنظمة inference بالعمق الذي لا يحاول هذا النص بلوغه، ومستودع llama.cpp ومواصفة GGUF هما المصدران الأساسيان لجانب CPU.

  1. Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). الورقة في معظمها حجة عن عرض نطاق الذاكرة، وتُقرأ كذلك.

  2. Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). تتضمن وصفة uptraining التي تحوّل checkpoint قائمًا متعدد الرؤوس، ولهذا انتشر GQA بهذه السرعة.

  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. تقدّم scheduling على مستوى التكرار — continuous batching — وselective batching.

  4. Kwon, W. et al. Efficient Memory Management for Large Language Model Serving with PagedAttention. arXiv:2309.06180 (2023), SOSP 2023. الورقة التي بُني عليها vLLM؛ القسم §3 هو تشبيه أنظمة التشغيل كاملًا.

  5. Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). يُعرّف NF4 في §3؛ قيم المستويات الستة عشر المستخدمة في القياس أعلاه هي التي تستنتجها هذه الورقة.

  6. 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 هو مصدر الظاهرة المقاسة أعلاه، بما في ذلك نتيجة أن القيم الشاذة تظهر منهجيًا عند scale. 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). المبرهنة 1 هي إثبات أن توزيع المخرجات لا يتغير؛ نشر Chen وآخرون (arXiv:2302.01318) الفكرة نفسها مستقلين.

  11. Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). درجة الحرارة وحجة «dark knowledge».

  12. Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation، قبل تسع سنوات، للـensembles بدل transformers.

هل أنت مستعد لتترك الاختيار لـ LIA؟

ابنِ بكل نماذج الذكاء الاصطناعي في مكان واحد — ابدأ مجانًا اليوم.