التكميم والتسريع
درس 3 من 6
3التكميم والتسريع
الصورة الكبيرةالتكميم (Quantisation) يخزّن أوزان النموذج بدقة أقل: 8 بتات أو 4 بدل 16 أو 32. الذاكرة تنخفض للنصف أو الربع، والسرعة ترتفع غالبًا، مقابل خسارة صغيرة في الدقة. وكلمة «صغيرة» هنا ليست وعدًا، بل رقم تقيسه على مهمتك قبل أن تقبله.
ماذا تشاهد في الرسم: الإطار يمرّ على الأوزان صفًّا صفًّا: لكل صف مقياس يجعل أكبر قيمة فيه 127، ثم تُقرّب القيم لأقرب عدد صحيح. ما يضيع هو كسور التقريب، وهي صغيرة في 8 بتات وأكبر بكثير في 4.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
التكميم مثل حفظ صورة بجودة أقل قليلًا: تأخذ نصف المساحة، ولا تكاد العين تلاحظ الفرق. لكن «تكاد» ليست «لا»، فتقيس قبل أن تقبل: هل ما زال النموذج يجيب بنفس الجودة على أسئلتك المهمة؟
التكميم يحوّل الأوزان من float16 إلى int8 أو int4، فينخفض استهلاك الذاكرة ويزيد التسريع على العتاد الداعم. وطرق مثل GPTQ وAWQ تقلل الخسارة، وتقيس الأثر على مجموعة التقييم نفسها مع الزمن والذاكرة.
قبل أن تبدأ
⏱️الزمن والتكلفة
تعرف كيف تقيس p95 والإنتاجية، وهما ما يحاول التكميم تحسينه.
📏هل تحسّن فعلًا
تعرف المقارنة على مجموعة مجمّدة وقراءة الحالات التي تغيّرت.
الفكرة الجوهرية
التكميم يخزّن الأوزان بدقة أقل. تبدأ هنا بـ bf16، ثم تكمّم مصفوفة بيدك، ثم تقيس الأثر على مهمة حقيقية، ثم تحمّل نموذجًا لغويًا مكمّمًا.
1دقة أقل: bf16
كل وزن في fp32 يأخذ 4 بايتات. bf16 يأخذ 2 بلا خسارة تُذكر تقريبًا، وهو اليوم الافتراضي لأي نموذج على بطاقة حديثة.
انظر إلى الرسم: مصفوفة 1024 في 1024 بكل دقة. كل خطوة للأسفل تقسم الذاكرة على 2.
2التكميم: مقياس وتقريب
لكل صف من الأوزان أكبر قيمة مطلقة. نقسم الصف على مقياس يجعل هذه القيمة 127 في 8 بتات، ونقرّب إلى أقرب عدد صحيح.
انظر إلى الرسم: عند الاستخدام نضرب في المقياس مرة أخرى. ما يضيع هو كسور التقريب.
import numpy as np
rng = np.random.default_rng(0)
W = rng.normal(scale=0.05, size=(1024, 1024)).astype(np.float32)
def quantize(w, bits):
levels = 2 ** (bits - 1) - 1 # 127 for int8, 7 for int4
scale = np.abs(w).max(axis=1, keepdims=True) / levels # one scale per row
q = np.round(w / scale).clip(-levels, levels)
return q, scale
for bits in [8, 4]:
q, scale = quantize(W, bits)
restored = q * scale
err = np.abs(W - restored).mean() / np.abs(W).mean()
size = W.size * bits / 8 / 1e6
print(f"int{bits}: {size:.2f} MB instead of {W.nbytes / 1e6:.2f} MB, mean relative error {err:.1%}")
8 بتات: ربع الحجم بخطأ أقل من 1%. 4 بتات: ثُمن الحجم بخطأ 15% في الأوزان. لكن خطأ الأوزان ليس ما يهمّ، بل خطأ المخرجات في مهمتك، وقد يكون أصغر بكثير لأن الأخطاء العشوائية تلغي بعضها جزئيًّا. هذا ما نقيسه في المثال التالي.
3قِس على مهمتك
خطأ الأوزان ليس ما يهم. يهمّ خطأ الناتج على مهمتك، ونقيسه برقمين: الدقة، ونسبة الأجوبة المطابقة للنموذج الأصلي.
انظر إلى الرسم: 8 بتات تطابق كل شيء، و4 بتات تطابق تقريبًا، و2 بت تنهار.
مصنّف أرقام مدرّب، ونكمّم أوزانه إلى 8 و4 و2 بت، ونقيس الدقة ونسبة الإجابات المطابقة للأصل.
import numpy as np
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
X, y = load_digits(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0)
model = LogisticRegression(max_iter=2000).fit(X_tr / 16, y_tr)
W, b = model.coef_.copy(), model.intercept_
def predict(weights):
return np.argmax(X_te / 16 @ weights.T + b, axis=1)
def quantize(w, bits):
levels = 2 ** (bits - 1) - 1
scale = np.abs(w).max(axis=1, keepdims=True) / levels
return np.round(w / scale).clip(-levels, levels) * scale
base = predict(W)
print(f"float32: accuracy {np.mean(base == y_te):.3f}")
for bits in [8, 4, 2]:
p = predict(quantize(W, bits))
print(f"int{bits}: accuracy {np.mean(p == y_te):.3f} same answer as float32 on {np.mean(p == base):.1%} of inputs")
8 بتات: نفس الدقة ونفس الإجابة على كل المدخلات. 4 بتات: خسارة 0.7 نقطة، وإجابة مختلفة في نحو 2% من المدخلات. 2 بت: انهيار. لاحظ أن خطأ الأوزان عند 4 بتات كان 15%، لكن خطأ المهمة أقل من نقطة. ولاحظ أيضًا نسبة التطابق: حتى حين تتشابه الدقة، الـ2% المختلفة قد تكون حالات مهمة، فاقرأها.
الخسارة تظهر أولًا في المهام الصعبة: الحساب والاستدلال الطويل واللغات الأقل تمثيلًا. المهام البسيطة تتأثّر أقل.
4نموذج لغوي مكمّم في الواقع
مع النماذج اللغوية لا تكتب التكميم بيدك. مكتبة bitsandbytes تحمّل النموذج بأربع بتات مباشرة، و7 مليارات وزن تنزل من نحو 15 غيغابايت إلى نحو 5.
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
quant = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype="bfloat16")
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct", quantization_config=quant)
print(model.get_memory_footprint() / 1e9, "GB") # about 5 GB instead of about 15 GB in bf16
nf4 نوع 4 بتات مصمّم لتوزيع أوزان الشبكات العصبية، وcompute_dtype يجعل الحساب نفسه بـbf16 بعد فكّ الضغط. شغّل المجموعة المجمّدة على هذا النموذج وعلى نسخة bf16، وضع الرقمين في جدول قبل أن تقرّر.
كيف يعمل، خطوة بخطوة: كيف يُكمَّم صف من الأوزان
تجد أكبر قيمة مطلقة في الصف.
تقسمها على 127 لـ8 بتات أو 7 لـ4 بتات، فهذا مقياس الصف.
تقسم كل وزن على المقياس وتقرّبه لأقرب عدد صحيح، وتحفظ الأعداد والمقياس.
وقت الاستخدام تضرب الأعداد في المقياس، وتقيس أثر الفرق على مهمتك لا على الأوزان.
مثال محلول
الفكرة: كل صف من الأوزان له أكبر قيمة مطلقة. نقسم الصف على مقياس يجعل هذه القيمة 127 (لـ8 بتات)، ونقرّب لأقرب عدد صحيح. عند الاستخدام نضرب في المقياس مرة أخرى. ما ضاع هو كسور التقريب.
جرّب بلا كود
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| خطأ 15% في الأوزان يعني خسارة 15% في الدقة. | في مصنّف الأرقام كان خطأ الأوزان بـ4 بتات 15% وخسارة الدقة أقل من نقطة، لأن الأخطاء العشوائية يلغي بعضها بعضًا جزئيًّا. |
| 4 بتات آمنة لأن الجميع يستخدمها. | الخسارة تختلف بين النماذج والمهام واللغات، والعربية قد تتأثّر أكثر. قِس على مجموعتك المجمّدة. |
| نفس الدقة الكلّية تعني نفس النموذج. | بـ4 بتات تغيّرت الإجابة على نحو 2% من المدخلات. قد تكون حالات مهمّة، فاقرأها وقِس الشرائح. |
جرّب بنفسك
أي تكميم تبدأ به لنموذج في الإنتاج؟
كل خطوة توفّر أكثر وتخاطر أكثر. تتوقّف عند أول خطوة تحقّق هدف الذاكرة أو الزمن بخسارة مقبولة.
الدقة بعد التكميم انخفضت 0.5 نقطة فقط. ماذا تفحص أيضًا؟
المتوسط قد يخفي انهيارًا في شريحة صغيرة مهمة. قراءة الحالات المتغيّرة تكشف نمط الخسارة.
بطاقات مراجعة سريعة
bf16 بدل fp32 يقسم الذاكرة على اثنين بلا خسارة تُذكر تقريبًا، وهو الافتراضي اليوم لأي نموذج على بطاقة حديثة.تحقق من فهمك
النموذج المكمَّم بـ4 بتات يعطي نفس الدقة الكلّية، لكن إجاباته على الأسئلة العربية الطويلة ساءت. كيف لم يظهر هذا في الدقة الكلّية؟
لأن الأسئلة العربية الطويلة نسبة صغيرة من المجموعة، فخسارتها تذوب في المتوسط، وربما عوّضها تحسّن عشوائي في أسئلة أخرى. لهذا تقيس بحسب الشرائح المهمّة (اللغة، والطول، ونوع المهمة)، وتقرأ الحالات التي تغيّرت بدل الاكتفاء بالمتوسط.
لماذا لكل صف مقياس خاص به بدل مقياس واحد للمصفوفة كلها؟
لأن صفًّا فيه قيمة كبيرة واحدة يفرض مقياسًا كبيرًا، فتضيع دقة الصفوف ذات القيم الصغيرة. مقياس لكل صف يجعل كل صف يستخدم كل المستويات المتاحة.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.