جعل inference رخيصًا: KV cache وbatching والتكميم
النموذج نفسه يجيب بالمخرجات نفسها خلال 8.8 ثوانٍ أو 78.9. ثم INT4 مقاس بثلاث طرق، لا مجرد ادعاء.
في هذه الصفحة
النموذج نفسه، على الجهاز نفسه، يجيب عن السؤال نفسه باستخدام 48 token نفسها. المخرجان متطابقان 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)تغيّرت وسيطة واحدة: use_cache=False. لا شيء في النموذج أو prompt أو sampling أو الحساب مختلف، والتشغيل الثاني ليس أدق مقابل هذا العناء. إنه أبطأ تسع مرات بلا مقابل.
هذا هو شكل هذا الفصل. كل ما فيه — cache، وbatch، والأوزان المكمّمة — محاولة للتوقف عن الدفع مقابل عمل لا يغيّر الإجابة، أو لمعرفة تكلفة إجابة أرخص. رسّخ الفصل 10 قائمة أسعار التدريب. وهذه قائمة أسعار الجانب الذي تدفع له إلى الأبد: نموذج منشور ينفق تقريبًا FLOPs مقابل كل token يصدره، في كل طلب، لبقية عمره.
أين ذهب وقت التشغيل الثاني
رابط إلى القسم: أين ذهب وقت التشغيل الثانيلتوليد token، يأخذ decoder-only transformer التسلسل كله حتى الآن، ويمرره عبر كل طبقة، ويقرأ توزيع الاحتمالات من الموضع الأخير. ثم يضيف token المختارة ويفعل ذلك مرة أخرى. هذا الوصف صحيح، وهو ما يفعله التشغيل البطيء.
لكنه أيضًا مبذّر على نحو هائل، والسبب هو causal mask من الفصل 9. تُحسب متجهات key وvalue للموضع 7 من إدخال الموضع 7 والمواضع التي قبله. عندما يصل الموضع 8، لا يستطيع الموضع 7 رؤيته — فهذا معنى causal — لذلك تكون key وvalue للموضع 7 الأرقام نفسها تمامًا كما كانت من قبل. ومع ذلك يعيد التشغيل البطيء حسابها في كل خطوة.
لذا خزّنها. هذا المخزن هو key-value cache، أهم تحسين منفرد في تشغيل نماذج اللغة:
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 واحدًا مع ، خطوة واحدة من التوليد محسوبة بالطريقتين:
| tokens في context | إعادة حساب كل شيء | مع cache | النسبة | مصفوفة الدرجات |
|---|---|---|---|---|
| 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 الكاملة في كل خطوة — وهي من صندوق الترميز التقاربي في الفصل 9، تُدفع مرة لكل token. مع cache تبني صفًا بدلًا من ذلك: عند 4,096 tokens، لدينا 67 MB من الدرجات مقابل 16 KB.
عدّ عمليات multiply-accumulate بدلًا من المللي ثانية يزيل الجهاز من الحجة. لتوليد tokens من بداية باردة:
| tokens مولّدة | مع cache | إعادة الحساب | النسبة |
|---|---|---|---|
| 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 |
في كل خطوة تكون النسخة ذات cache خطية في context، أما غير المخزّنة فتربيعية؛ وعند جمعها على توليد كامل، مقابل ، مع نسبة تنمو بلا حد. الفرق التساعي في البداية قيس على 48 tokens — أقصر من الصف الأول في ذلك الجدول.
يغيّر cache أيضًا ما يجب أن يكون في الذاكرة. على GPU حاسوب محمول بسعة 8 GB يولّد 256 tokens في fp16، مع أخذ ذروة المخصّص وطرح الأوزان المقيمة:
| ذروة ذاكرة العمل | |
|---|---|
| مع cache | 21.8 MB |
| إعادة الحساب | 181.7 MB |
ذاكرة أكثر بـ8.3 مرات، تُنفق لإنتاج tokens نفسها ببطء أكبر. هذا هو الوعد الذي قُطع في الفصل 5، لكنه يصل من اتجاه غير متوقع: هناك، كان reverse-mode autodiff مضطرًا لإبقاء كل وسيط حيًا من أجل backward pass، وكانت activations تهيمن على ذاكرة التدريب. في inference لا توجد backward pass ولا شيء للاحتفاظ به من أجلها — لذلك ما يهيمن على الذاكرة بدلًا من ذلك هو cache، وهو اختيار مقصود لا تكلفة لا مفر منها.
prefill وdecode آلتان مختلفتان
رابط إلى القسم: prefill وdecode آلتان مختلفتانانظر مجددًا إلى التشغيل السريع: تصرّفت token الأولى فيه على نحو مختلف عن السبع والأربعين الأخرى.
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 هو الجزء الرخيص. ينقسم التوليد إلى مرحلتين بفيزياء مختلفة فعلًا:
Prefill
رابط إلى القسم: Prefillتمريرة أمامية واحدة على prompt كله. تُعالَج كل token بالتوازي، لذلك تُحمّل كل مصفوفة أوزان من الذاكرة مرة واحدة وتُضرب في مصفوفة من مئات متجهات tokens — ضرب مصفوفة-مصفوفة، مع الكثير من الحساب لكل بايت منقول، وهذا ما صُمّم GPU لأجله. Prefill compute-bound، وتكلفته تقريبًا خطية في طول prompt.
Decode
رابط إلى القسم: Decodeتمريرة أمامية واحدة لكل token، batch من واحد وتسلسل من واحد. لا تزال كل مصفوفة أوزان تُحمّل كاملة من الذاكرة، وتُضرب في متجه واحد — ضرب مصفوفة-متجه، مع حساب قليل جدًا لكل بايت منقول. Decode memory-bandwidth-bound، وتكلفته لكل token لا تعتمد إلا قليلًا على طول context.
يمكن قياس النصفين. Prefill، تمريرة واحدة على tokens:
| 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 من :
| tokens مخزّنة | ms لـtoken واحدة |
|---|---|
| 16 | 110.05 |
| 64 | 97.57 |
| 256 | 108.53 |
| 1024 | 103.86 |
اقرأ الجدول الثاني مرتين. الانتقال من 16 tokens من context إلى 1,024 — تاريخ أطول بأربع وستين مرة ليجري attention عليه — لم يغيّر تكلفة الخطوة بأي مقدار قابل للقياس. attention على cache عمل حقيقي، لكنه يتضاءل أمام التكلفة الثابتة لسحب نصف مليار وزن عبر ناقل الذاكرة لإنتاج متجه واحد. تلك التكلفة الثابتة هي سبب كل ما في القسم التالي.
هاتان المرحلتان هما أصل الرقمين اللذين يبلّغ عنهما كل نظام serving. Time to first token هو في جوهره prefill، وينمو مع prompt، ولذلك تبدو المحادثة الطويلة بطيئة في البداية. Tokens per second هي ، وهي ثابتة تقريبًا، ولذلك يتدفق الرد بعدها بسلاسة. محادثة تبدأ ببطء ثم تُبث بسلاسة ليست خدعة عرض. إنها هذان الجدولان.
cache هي الفاتورة أيضًا
رابط إلى القسم: cache هي الفاتورة أيضًايقايض cache الحساب بالذاكرة، والذاكرة التي يطلبها ليست صغيرة. لكل token في context، تحتفظ كل طبقة بمتجه key ومتجه value لكل رأس key-value:
الرقم 2 هو من أجل keys وvalues؛ وكل ما عداه من بنية النموذج. بالنسبة للنموذج المقاس في هذا الفصل كله — 24 طبقة، و14 query heads، و2 key-value heads، وبُعد الرأس 64 — في fp16 يكون ذلك بايت لكل token.
للمعادلات في هذا المجال عادة أن تخطئ بعامل اثنين، لذلك افحصها أمام المخصّص بدل تصديقها:
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مطابق تمامًا، ويبقى مطابقًا عبر كل شكل جُرّب:
| batch | context | cache المقاس | المتوقع | ذروة ذاكرة العمل |
|---|---|---|---|---|
| 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 |
تستحق الصفوف الثلاثة الأخيرة نظرة ثانية. اثنان وثلاثون مستخدمًا مع 2,048 tokens لكل منهم، أربعة وستون مع 1,024، ومئة وثمانية وعشرون مع 512 — cache تساوي 768 MB في كل حالة، لأن الحالات الثلاث كلها تحتفظ بـ65,536 tokens. يعتمد cache فقط على العدد الإجمالي لـtokens المقيمة، لا على كيفية توزيعها بين المستخدمين. هذه الحقيقة هي أساس قسم batching.
من أين يأتي MQA وGQA
رابط إلى القسم: من أين يأتي MQA وGQAقدّم الفصل 9 multi-query وgrouped-query attention وأجّل السبب إلى هذا الفصل. السبب هو تلك المعادلة، وبالتحديد فيها.
في multi-head attention القياسي، لكل query head رؤوس key وvalue خاصة به. النموذج هنا لديه 14 query heads؛ ومع multi-head attention كامل، كان cache الخاص به سيكون بايت لكل 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,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 |
أوزان ذلك النموذج نفسه هي 13.0 GB في fp16، وهو الرقم في الجدول في نهاية هذا الفصل. لذلك عند context من 128,000-token، cache مستخدم واحد أكبر من النموذج. هذا هو الحساب الذي يحوّله الفصل 16 إلى مال، ولهذا ليست المحادثة الطويلة بطيئة فحسب — بل تحتل شريحة ثابتة من آلة ما طوال حياة الطلب.
Batching: الرقم الذي يصعد والرقم الذي يهبط
رابط إلى القسم: Batching: الرقم الذي يصعد والرقم الذي يهبطDecode مقيّد بالذاكرة: تُسحب الأوزان عبر الناقل لإنتاج token واحدة، وتظل وحدات الحساب خاملة. إذن ضع عملًا أكثر في الخطوة نفسها. شغّل عدة طلبات معًا، والأوزان التي تُقرأ مرة واحدة تخدمها كلها. مقاسًا على النموذج نفسه، مع احتفاظ كل طلب بـcache من 64-token وفك token واحدة:
| batch | زمن التأخير لكل خطوة | throughput | زمن التأخير مقابل 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 |
اقرأ العمودين الأيمنين معًا، لأنهما الفكرة كلها. الانتقال من طلب واحد إلى ستة عشر يضاعف throughput بمقدار 6.0 ويضاعف انتظار أي طلب فردي بمقدار 2.67. جعل batch الخادم أفضل وكل مستخدم أسوأ.
هذا ليس عيبًا يمكن ضبطه بعيدًا؛ إنه المقايضة نفسها، ولها اسم في كل جانب. Latency هو ما يختبره شخص ينتظر ردًا. Throughput هو ما تُقسّم عليه الفاتورة. لا يوجد إعداد يحسّن الاثنين معًا.
لاحظ أيضًا أين يتوقف. من 16 إلى 32، يكسب throughput نسبة 9 % بينما يكاد latency يتضاعف: توقفت الخطوة عن كونها memory-bound وأصبحت compute-bound، وبعد تلك الركبة لا يشتري batch شيئًا. لكل نشر ركبة كهذه؛ يجب قياس موضعها لديك، لكن وجودها لا يحتاج إلى قياس.
Static batching يهدر معظم ما يكسبه
رابط إلى القسم: Static batching يهدر معظم ما يكسبهالطريقة الساذجة لعمل batch هي جمع طلبًا، وتشغيلها معًا، والإرجاع عندما تنتهي كلها. لكنها لا تنتهي معًا: بعض الردود عشرون token وبعضها خمسمئة. يستمر batch ثابت حتى ينتهي أطول عضو فيه، وكل طلب منتهٍ يظل يحتل خانته، مساهمًا في padding، حتى ذلك الحين.
خذ 64 طلبًا بتفاوت واقعي في أطوال المخرجات — الوسيط 18 tokens، والأطول 231، والمجموع 1,874 — وحاكِ السياستين بتكلفة الخطوة المقاسة لثماني خانات:
| السياسة | الزمن الجداري | throughput | متوسط latency لكل طلب | 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. ويتحسن متوسط latency بأكثر من عشر مرات، لأنه في static batching يظل طلب انتهى في أربع خطوات ينتظر جارًا من 231-token قبل أن يسمع أحد شيئًا عنه.
Continuous batching3 هو الإصلاح، وهو بسيط كما يبدو: batch ليس مجموعة بل مجموعة خانات، والخانة التي تتحرر تستقبل الطلب التالي في الطابور في الخطوة التالية مباشرة. يعمل المجدول على حبيبة token واحدة بدل طلب واحد. كل stack serving في الإنتاج يفعل ذلك الآن.
وله نصف ثانٍ، وهو cache. الخانات التي تأتي وتذهب تترك ذاكرة cache مجزأة، وحجز أقصى context ممكن لكل خانة يهدر معظم الحجز. PagedAttention4 يستعير الجواب من أنظمة التشغيل: خزّن cache في كتل ثابتة الحجم مع جدول كتل لكل تسلسل، بحيث يمكن أن يكون cache التسلسل مبعثرًا ماديًا بينما يظل متصلًا منطقيًا — وهذا يسمح أيضًا لتسلسلين لهما prefix مشترك بمشاركة الكتل التي تحتويه. هذا ما يُبنى عليه vLLM، ولهذا يكون محرك serving مخصّص ذاكرة ومعه transformer ملحق.
Quantization، وأول ما يسوء
رابط إلى القسم: Quantization، وأول ما يسوءالنصف الآخر من الفاتورة هو الأوزان نفسها. نصف مليار parameter بأربعة بايتات لكل منها يساوي 1.98 GB؛ وببايتين، 0.99 GB؛ وببايت واحد، 0.49 GB. عدد أقل من bits لكل وزن يصغّر النموذج على القرص، ويصغّره في الذاكرة، و— لأن decode مقيّد بعرض النطاق — يجعل كل خطوة أسرع، إذ توجد بايتات أقل لنقلها.
أبسط مخطط هو تكميم absolute-maximum متماثل، ويتسع في ثلاثة أسطر:
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، وخطأ نسبي :
| المخطط | متوسط الخطأ النسبي | أسوأ مصفوفة |
|---|---|---|
| INT8، scale واحد للمصفوفة كلها | 0.0400 | 0.1487 |
| INT8، scale واحد لكل صف إخراج | 0.0100 | 0.0149 |
| INT4، scale واحد للمصفوفة كلها | 0.6026 | 0.9931 |
| INT4، scale واحد لكل صف إخراج | 0.1790 | 0.2589 |
| INT4، scale واحد لكل مجموعة من 128 | 0.1323 | 0.1992 |
| NF4، scale واحد لكل كتلة من 64 | 0.0952 | 0.1205 |
| INT3، scale واحد لكل مجموعة من 128 | 0.3044 | 0.4123 |
| INT2، scale واحد لكل مجموعة من 128 | 0.7790 | 0.8076 |
الصف الرابع هو الانهيار. خطأ نسبي 0.99 في أسوأ مصفوفة يعني أن إعادة البناء لا تحتفظ عمليًا بأي شيء من الأصل — لقد استُبدلت المصفوفة بضجيج له المقدار الصحيح تقريبًا. يظهر السبب في التجربة نفسها على مصفوفة واحدة:
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 يساوي bits لكل وزن بدلًا من 4 — ويستعيد معظم الفجوة.
NF4 يهاجم المسألة من الجانب الآخر.5 لا يلزم أن تكون المستويات متباعدة بالتساوي. الأوزان داخل كتلة موزعة تقريبًا طبيعيًا، لذلك اختر المستويات الستة عشر ككوانتايلز لتوزيع طبيعي: كثيفة قرب الصفر حيث توجد الأوزان فعلًا، ومتباعدة في الأطراف حيث لا توجد. bits الأربعة نفسها، وblock scaling نفسه، عند كتلة أصغر — 4.25 bits لكل وزن مقابل 4.125 لمجموعة 128 — وينخفض الخطأ المقاس من 0.1323 إلى 0.0952، أي أقل بـ28 %. جزء من ذلك هو الكتلة الأدق، والباقي وضع المستويات حيث توجد الكتلة الاحتمالية، وفصل الاثنين يحتاج صفًا ثالثًا.
features الشاذة
رابط إلى القسم: features الشاذةانتهى صندوق floating-point في الفصل 2 بوعد: أن هذا الفصل سيكمّم الأوزان إلى 8 و4 bits ويجد حفنة من features الشاذة التي ترفض الانضغاط. ها هي، وهي تشرح لماذا لم يكن «قرّب الأرقام فقط» لينجح أبدًا على activations.
كانت الأوزان أعلاه سيئة السلوك. أما activations فهي في دوري آخر. خذ prompt عاديًا من 84-token، والتقط residual stream في كل طبقة، وقِس أكبر مقدار تصل إليه كل بُعد من الأبعاد الـ896:
| الطبقة | أكبر |h| | أكبر |h| لبُعد الوسيط | النسبة | أبعاد فوق 6x الوسيط |
|---|---|---|---|---|
| 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 |
يصل البُعد 62 إلى 1,579.6 بينما لا يتجاوز بُعد الوسيط 1.6 أبدًا. ليست مصادفة token واحدة أو طبقة واحدة: البُعد نفسه موجود عند الطبقة 4 ولا يزال موجودًا عند الطبقة 20، بالقيمة نفسها تقريبًا. هذه هي outlier features،6 وهي منهجية — خاصية للنموذج المدرّب، لا للمدخل.
يوضح histogram لتلك القيم العظمى لكل بُعد من الأبعاد الـ896 عند الطبقة 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تسعمئة بُعد في كومة مرتبة تحت 8، ولا شيء إطلاقًا لثلاثة octaves، ثم بُعد واحد وحيد في الطرف البعيد. الآن كمّم ذلك tensor إلى INT8 واحسب ما يحدث:
| المخطط | الخطأ النسبي | مستويات الأعداد الصحيحة المميزة المستخدمة، tensor كامل |
|---|---|---|
| scale واحد لـtensor كله | 0.1083 | 14 من 256 |
| scale واحد لكل token (لكل صف) | 0.0433 | 158 |
| tensor كامل، بُعد شاذ واحد محفوظ في fp32 | 0.0442 | 48 |
| tensor كامل، 4 أبعاد شاذة محفوظة في fp32 | 0.0279 | 57 |
| tensor كامل، 16 بُعدًا شاذًا محفوظًا في fp32 | 0.0085 | 102 |
أربعة عشر مستوى من أصل 256. ضُبط scale بواسطة 1,579.6، لذلك عرض كل خطوة 12.44، والـactivation النموذجي — مقدار الوسيط 0.26، والمئين التاسع والتسعون 2.51 — ليس لديه مكان يهبط فيه. على مستوى البُعد يكون الأمر أوضح:
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 فعليًا، مقاسًا
رابط إلى القسم: ما تكلفه quantization فعليًا، مقاسًاتتوقف معظم المقالات عن quantization عند القسم السابق: تشرح الطريقة، وتقتبس نسبة ضغط، وتؤكد أن الجودة «محفوظة إلى حد كبير». كان الفصل 4 عن عدم خداع نفسك، فلنكتشف إذن.
النموذج نفسه، أوزان مكمّمة في موضعها بكل مخطط، ثم ثلاثة قياسات: perplexity على 2,048 tokens من نثر إنجليزي محجوز — هنا، مسودة هذه الدورة، ولذلك يستبدل المستودع كتابًا ثابتًا في الملكية العامة ويطبع جدولًا بالشكل نفسه مع أرقام مختلفة — وبطارية من 16 سؤالًا واقعيًا قصيرًا بإجابات معروفة تحت greedy decoding، ونسبة tokens التي يتفق فيها النموذج المكمّم مع نموذج الدقة الكاملة عند إعطائه context مطابقًا.
| المخطط | متوسط خطأ الوزن | perplexity | بطارية الأسئلة | يتفق مع fp32 |
|---|---|---|---|---|
| fp32 (مرجع) | 0.0000 | 23.08 | 13/16 | 100.0 % |
| INT8 لكل tensor | 0.0400 | 23.58 | 13/16 | — |
| INT8 لكل صف | 0.0100 | 22.96 | 13/16 | 98.6 % |
| INT4 لكل tensor | 0.6026 | 365,416,000 | 0/16 | — |
| INT4 لكل صف | 0.1790 | 46.18 | 6/16 | 58.3 % |
| INT4 مجموعة 128 | 0.1323 | 31.08 | 10/16 | 71.5 % |
| NF4 كتلة 64 | 0.0952 | 24.55 | 11/16 | 84.7 % |
| INT3 مجموعة 128 | 0.3044 | 213.09 | 0/16 | 5.6 % |
| INT2 مجموعة 128 | 0.7790 | 26,325,436 | 0/16 | 0.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 ممكن، والهندسة تقرر ما إذا كان قابلًا للاستخدام.
Speculative decoding
رابط إلى القسم: Speculative decodingأعلن الفصل 12 هذا وترك الفاتورة هنا.
تأتي الفكرة مباشرة من انقسام prefill/decode. التحقق من تسلسل مقترح من tokens يكلّف تمريرة أمامية واحدة على مواضع — ضرب مصفوفة-مصفوفة، أغلى بالكاد من تمريرة على واحد. إذن:
نموذج صغير ورخيص يولّد tokens مرشحة autoregressively.
Verify
رابط إلى القسم: Verifyيشغّل النموذج الكبير تمريرة أمامية واحدة على كل المرشحين دفعة واحدة، منتجًا ما كان سيقوله في كل موضع.
Accept
رابط إلى القسم: Acceptاحتفظ بأطول prefix يتفق عليه الاثنان، إضافة إلى token التي يوفرها النموذج الكبير مجانًا عند أول اختلاف. تخلّص من الباقي وابدأ من جديد.
يبقى توزيع المخرجات دون تغيير. مع greedy decoding هذا واضح — لا تُقبل token إلا إذا كان الهدف سينتجها. ومع sampling يتطلب الأمر قاعدة قبول معدلة، ويثبت Leviathan وآخرون أن التوزيع الناتج هو توزيع الهدف تمامًا.10 هذا هو التحسين الدقيق الثاني في هذا الفصل.
لذلك يتوقف كل شيء على معدل القبول ، وهو قابل للقياس — إنه عمود الاتفاق أعلاه، ولذلك حُسب هناك. باستخدام كل نموذج مكمّم كـdraft لهدف الدقة الكاملة، عبر 144 موضعًا مولّدًا:
| draft model | القبول | أطول سلسلة مقبولة | tokens متوقعة لكل تمريرة هدف، |
|---|---|---|---|
| fp32 (الهدف نفسه) | 100.0 % | 48 | 5.00 |
| INT8 لكل صف | 98.6 % | 48 | 4.86 |
| NF4 كتلة 64 | 84.7 % | 20 | 3.69 |
| INT4 مجموعة 128 | 71.5 % | 13 | 2.85 |
| INT4 لكل صف | 58.3 % | 7 | 2.24 |
| INT3 مجموعة 128 | 5.6 % | 2 | 1.06 |
| INT2 مجموعة 128 | 0.0 % | 0 | 1.00 |
عدد tokens المتوقع قبولها لكل تمريرة تحقق، عند طول draft ، هو
والتسريع الصافي يقسم ذلك على تكلفة draft نفسه، وهي كسر من الهدف لكل token:
| القبول | ، | ، | ، | ، |
|---|---|---|---|---|
| 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 أن يجعل التوليد أبطأ. عند قبول 30 % مع draft يكلّف خمس الهدف، تدفع مقابل خمس تمريرات أمامية وتحتفظ بـ1.4 tokens. العمود الأخير هو الفخ الآخر — draft أطول لا يساعد إلا عندما يكون القبول عاليًا، لأن ذيل تخمين من -token نادرًا ما يُبلَغ. عند قبول 90 % تكون بقيمة 3.40x، وعند 30 % بقيمة 0.79x: التكوين نفسه، ربح أو خسارة حسب رقم مقاس على حركة مرورك.
Distillation، وما تحمله soft label
رابط إلى القسم: Distillation، وما تحمله soft labelQuantization يصغّر نموذجًا بتخزين الدالة نفسها بعدد bits أقل. Distillation يصغّره بتدريب نموذج أصغر على تقليد نموذج أكبر11 — وهي فكرة تسبق deep learning بنحو عقد.12
الجزء الدقيق هو ما يتعلم منه الطالب. ليس الإجابة الصحيحة: كان يمكن تدريبه عليها مباشرة. ما يضيفه المعلّم هو التوزيع كله. اسأل النموذج ماذا يلي عبارة وانظر إلى ما بعد argmax:
"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 على قبل softmax تسطّح التوزيع وترفع الوزن النسبي للوصيفين: في هذه العبارة، تنخفض النسبة بين أعلى token والثالثة من 2.24 عند إلى 1.50 عند — الجذر التربيعي للأولى، وهذا ما تفعله قسمة logits على اثنين بالنسبة إلى نسبة. الترتيب نفسه، لكن مزيد من attention الخسارة على الإخفاقات القريبة. gradient الطالب يحمل لا يقين المعلّم، لا حكمه فقط.
ما الذي يتسع في 8 و16 و24 GB
رابط إلى القسم: ما الذي يتسع في 8 و16 و24 GBكل شيء في هذا الفصل أصبح الآن مجموعًا واحدًا:
حيث هي إجمالي tokens المقيمة عبر كل الطلبات المتزامنة. عند تطبيقها: صفوف 7B و70B تفترض 8 key-value heads بأبعاد 128، وصف 13B يفترض multi-head attention كاملًا مع 40 رأسًا، وهي الطريقة التي بُنيت بها تلك الأجيال من النماذج — وهذا يظهر.
8 GB
| model | precision | weights | المتاح بعد overhead | context tokens التي تتسع |
|---|---|---|---|---|
| 7B | fp16 | 13.0 GB | لا يتسع | — |
| 7B | int8 | 6.5 GB | لا يتسع | — |
| 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 | لا يتسع | — |
16 GB
| model | precision | weights | المتاح بعد overhead | context tokens التي تتسع |
|---|---|---|---|---|
| 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 | المتاح بعد overhead | context tokens التي تتسع |
|---|---|---|---|---|
| 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 | لا يتسع | — |
انظر إلى صف 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 بحيث لا تُكتب مصفوفة الدرجات إلى الذاكرة أبدًا، ولهذا تكون 67 MB في الجدول الثاني من هذا الفصل أصغر عمليًا مما توحي به الحسابات. أما kernels نفسها فمفوّضة: المحاضرة 10 من Stanford CS336 تغطي أنظمة inference بالعمق الذي لا يحاول هذا النص بلوغه، ومستودع llama.cpp ومواصفة GGUF هما المصدران الأساسيان لجانب CPU.
المراجع
رابط إلى القسم: المراجع-
Shazeer, N. Fast Transformer Decoding: One Write-Head is All You Need. arXiv:1911.02150 (2019). الورقة في معظمها حجة عن عرض نطاق الذاكرة، وتُقرأ كذلك. ↩
-
Ainslie, J. et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245 (2023). تتضمن وصفة uptraining التي تحوّل checkpoint قائمًا متعدد الرؤوس، ولهذا انتشر 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. تقدّم 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. الورقة التي بُني عليها vLLM؛ القسم §3 هو تشبيه أنظمة التشغيل كاملًا. ↩
-
Dettmers, T., Pagnoni, A., Holtzman, A. and Zettlemoyer, L. QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314 (2023). يُعرّف NF4 في §3؛ قيم المستويات الستة عشر المستخدمة في القياس أعلاه هي التي تستنتجها هذه الورقة. ↩
-
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
-
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). المبرهنة 1 هي إثبات أن توزيع المخرجات لا يتغير؛ نشر Chen وآخرون (arXiv:2302.01318) الفكرة نفسها مستقلين. ↩
-
Hinton, G., Vinyals, O. and Dean, J. Distilling the Knowledge in a Neural Network. arXiv:1503.02531 (2015). درجة الحرارة وحجة «dark knowledge». ↩
-
Buciluă, C., Caruana, R. and Niculescu-Mizil, A. Model Compression. KDD 2006. Distillation، قبل تسع سنوات، للـensembles بدل transformers. ↩