تشغيل الضبط
درس 4 من 6
4تشغيل الضبط
الصورة الكبيرةأول تشغيل ضبط ينتهي غالبًا برسالة «نفدت الذاكرة» (Out of memory). هذا الدرس عن الطريق من هناك إلى منحنى خسارة تستطيع قراءته: أين تذهب الذاكرة، وأي إعداد تغيّر أولًا، وكيف تعرف من المنحنى أن التدريب يسير جيدًا أو يحتاج إيقافًا.
ماذا تشاهد في الرسم: خسارة التدريب تواصل النزول، لكن التحقّق يصعد بعد الخط المتقطّع: هنا بدأ الحفظ. أفضل نموذج عند ذلك الخط، وload_best_model_at_end يحتفظ به تلقائيًّا حتى لو استمر التدريب بعده.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
تشغيل الضبط مثل طبخ وصفة لأول مرة في فرن لا تعرفه: قد تحترق في البداية لأن الحرارة عالية، أو لا تنضج لأن الوقت قصير. تراقب الطبخة (منحنى الخسارة) وتعدّل إعدادًا واحدًا في كل مرة حتى تعرف أيها السبب.
أخطاء الذاكرة تُعالج بحجم دفعة أصغر مع تراكم الميل gradient accumulation، وطول تسلسل أقصر، وgradient checkpointing. تبدأ بمعدل تعلّم صغير مع إحماء warmup، وتسجل خسارة التدريب والتحقق، وتحفظ نقاط تفتيش لترجع لأفضلها.
قبل أن تبدأ
🪡LoRA
تعرف أن LoRA يجمّد الأوزان ويدرّب محوّلات صغيرة، فلا تدرّجات للأوزان المجمّدة.
🩺تشخيص التدريب
قرأت منحنيات الخسارة من قبل، وتعرف شكل الإفراط في التعلّم.
الفكرة الجوهرية
أول تشغيل ضبط ينتهي غالبًا بنفاد الذاكرة. في هذا الدرس ترى أين تذهب الذاكرة، ثم كيف تصغّر الدفعة بلا خسارة، ثم الإعدادات التي تغيّرها أولًا، ثم كيف تقرأ المنحنى.
1أين تذهب الذاكرة
الذاكرة في التدريب تذهب إلى أربعة أشياء: الأوزان، وتدرّجاتها، وحالة المحسّن (Adam يحفظ رقمين لكل معامل)، والتنشيطات المحفوظة للمرور الخلفي.
انظر إلى الرسم: لنموذج 7B، الضبط الكامل يحتاج 84 جيجابايت قبل التنشيطات، وLoRA نحو 14، وQLoRA نحو 4.
def gb(n_bytes):
return n_bytes / 1e9
P = 7e9 # a 7B model
lora = 40e6 # trainable LoRA parameters, about 0.6%
full = P * 2 + P * 2 + P * 8 # bf16 weights + bf16 grads + Adam's two fp32 states
lora_bf16 = P * 2 + lora * (2 + 8)
qlora = P * 0.5 + lora * (2 + 8) # base weights in 4 bits
print(f"full fine-tuning: {gb(full):6.1f} GB (+ activations)")
print(f"LoRA on bf16 base: {gb(lora_bf16):6.1f} GB (+ activations)")
print(f"QLoRA on 4-bit base:{gb(qlora):6.1f} GB (+ activations)")
الضبط الكامل لنموذج 7B يحتاج 84 غيغابايت قبل التنشيطات، أي أكثر من أكبر بطاقة شائعة. LoRA يزيل التدرّجات وحالة المحسّن لكل الأوزان المجمّدة فيصير 14 غيغابايت. وQLoRA يضغط النموذج الأساسي نفسه إلى 4 بتات فيصير 4 غيغابايت تقريبًا. هذا ما جعل الضبط ممكنًا على بطاقة واحدة عادية.
2الدفعة وتجميع التدرّجات
التنشيطات تتناسب مع حجم الدفعة، فهي أول ما تخفّضه. دفعة 1 أو 2 لكل بطاقة أمر طبيعي في ضبط النماذج الكبيرة.
انظر إلى الرسم: أربع دفعات صغيرة من 8 تُجمع تدرّجاتها، ثم خطوة محسّن واحدة. هذه دفعة فعلية من 32 بذاكرة دفعة من 8.
تدرّج دفعة من 32 مثالًا، مقابل جمع تدرّجات أربع دفعات من 8.
import numpy as np
rng = np.random.default_rng(0)
X = rng.normal(size=(32, 3))
y = X @ np.array([1.0, -2.0, 0.5]) + rng.normal(scale=0.1, size=32)
w = np.zeros(3)
def grad(Xb, yb, w):
return 2 * Xb.T @ (Xb @ w - yb) / len(yb) # mean squared error gradient
big = grad(X, y, w) # one batch of 32
accum = np.zeros(3)
for i in range(4): # four micro-batches of 8
accum += grad(X[i * 8:(i + 1) * 8], y[i * 8:(i + 1) * 8], w) / 4 # divide by the number of steps
print("batch of 32: ", big.round(4))
print("4 x 8, accumulated: ", accum.round(4))
print("identical:", bool(np.allclose(big, accum)))
متطابقان تمامًا، بشرط القسمة على عدد الخطوات (/ 4)، لأن كل تدرّج صغير متوسط على 8 لا مجموع. نسيان هذه القسمة يجعل التدرّج أكبر بأربع مرات، كأنك ضاعفت معدّل التعلّم أربع مرات دون أن تدري. المكتبات تتولّاها عنك، وهذا سبب أن ترى خسارة مقسومة على gradient_accumulation_steps في كود التدريب اليدوي.
3الإعدادات التي تغيّرها أولًا
ثلاثة إعدادات أولًا: معدّل التعلّم (1e-4 إلى 3e-4 مع LoRA)، وعدد الحقب (1 إلى 3)، والدفعة الفعلية (16 إلى 64 عبر التجميع). اترك الباقي على الافتراضي حتى تعمل هذه.
ثم bf16، فهو يقسم ذاكرة الأوزان والتنشيطات على اثنين بلا مشاكل الفيض التي في fp16. وإن بقيت دفعة من 1 أكبر من الذاكرة، فشغّل gradient checkpointing.
from trl import SFTConfig, SFTTrainer
config = SFTConfig(
output_dir="runs/lora-r16-lr2e-4",
per_device_train_batch_size=2,
gradient_accumulation_steps=16, # effective batch = 2 x 16 = 32
learning_rate=2e-4,
num_train_epochs=2,
bf16=True,
gradient_checkpointing=True,
max_length=1024, # chosen after printing the length distribution
eval_strategy="steps", eval_steps=50,
save_strategy="steps", save_steps=50,
load_best_model_at_end=True, # keep the checkpoint with the lowest validation loss
logging_steps=10,
)
trainer = SFTTrainer(model=model, args=config, train_dataset=train, eval_dataset=val)
trainer.train()
اسم مجلّد الإخراج يحمل الإعدادات التي تغيّرها، فتعرف من الاسم وحده ما الفرق بين تشغيلين. وload_best_model_at_end مع eval_steps يحفظ تلقائيًّا النقطة ذات أدنى خسارة تحقّق، فلا يضرّك إن استمرّ التدريب بعد بداية الحفظ.
4قراءة المنحنى
خسارة التدريب تنخفض بسلاسة، وخسارة التحقّق تتبعها. إن صعد التحقّق والتدريب ينزل فهذا حفظ، فأوقف وخذ أفضل نقطة تفتيش.
انظر إلى الرسم: كل 50 خطوة تنظر إلى الخسارتين معًا، وإلى عيّنة من مخرجات النموذج بعينك.
كيف يعمل، خطوة بخطوة: كيف تصل من نفاد الذاكرة إلى تشغيل ناجح
تطبع توزيع أطوال الأمثلة بالرموز، وتختار الحد الأقصى للطول بحيث لا تُقطع الإجابات.
تخفّض الدفعة لكل بطاقة وترفع تجميع التدرّجات، فتبقى الدفعة الفعلية كما هي.
تفعّل bf16، ثم نقاط التفتيش للتدرّج إن لم تكفِ الدفعة 1.
تراقب خسارة التدريب والتحقّق معًا وعيّنة من المخرجات، وتأخذ نقطة التفتيش عند أدنى خسارة تحقّق.
أعمق قليلًا
تقنية أخرى توفّر ذاكرة كبيرة: نقاط التفتيش للتدرّج (Gradient checkpointing). بدل حفظ كل التنشيطات للتمرير الخلفي، يحفظ بعضها ويعيد حساب الباقي عند الحاجة. التدريب يبطؤ نحو 30%، والذاكرة تنخفض كثيرًا. فعّلها حين لا تكفي الدفعة 1.
مثال محلول
الذاكرة في التدريب تذهب إلى أربعة أشياء: الأوزان، وتدرّجاتها، وحالة المحسّن (Adam يحفظ رقمين لكل معامل)، والتنشيطات المحفوظة للتمرير الخلفي. الثلاثة الأولى تُحسب بالضرب، والرابعة تكبر مع حجم الدفعة وطول التسلسل.
جرّب بلا كود
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| خسارة تدريب قريبة من الصفر تعني نجاحًا. | في الحقبة الثالثة غالبًا تعني الحفظ. انظر إلى خسارة التحقّق وإلى عيّنة من المخرجات بعينك. |
| تجميع التدرّجات تقريب للدفعة الكبيرة. | النتيجة مطابقة رياضيًّا، بشرط أن تقسم على عدد الخطوات. نسيان القسمة يضاعف التدرّج كأنك رفعت معدّل التعلّم. |
| تقصير الحد الأقصى للطول حل آمن لنفاد الذاكرة. | إن كانت الأمثلة أطول منه تُقطع إجاباتها، فيتعلّم النموذج إجابات مبتورة. |
جرّب بنفسك
نفدت الذاكرة في أول خطوة. ما أول ما تجرّبه؟
التنشيطات تتناسب مع الدفعة، وتجميع التدرّجات يحافظ على نفس الرياضيات. بعدها جرّب bf16 ونقاط تفتيش التدرّج.
خسارة التدريب تنخفض، وخسارة التحقّق تصعد منذ الخطوة 400. ماذا تفعل؟
النموذج بدأ يحفظ التدريب بعد الخطوة 400. أفضل نموذج عندك هو عند تلك النقطة، والتدريب بعدها يضرّ.
بطاقات مراجعة سريعة
bf16 يقسم ذاكرة الأوزان والتنشيطات على اثنين ويسرّع الحساب، دون مشاكل الفيض التي في fp16. استخدمه إن دعمته بطاقتك.تحقق من فهمك
دفعتك 2 والتجميع 16، فقرّرت رفع الدفعة إلى 8 لأن الذاكرة تكفي الآن. ماذا تغيّر في التجميع؟
تخفّضه إلى 4، حتى تبقى الدفعة الفعلية 32. إن أبقيته 16 تصير الدفعة الفعلية 128، فيتغيّر سلوك التدريب ويحتاج معدّل تعلّم مختلفًا، ولن تقارن التشغيلين بعدل. الدفعة الأكبر لكل بطاقة تسرّع التدريب فقط، ولا يجب أن تغيّر نتيجته.
لماذا يحمل اسم مجلد التشغيل مثل runs/lora-r16-lr2e-4 الإعدادات التي تغيّرها؟
حتى يخبرك الاسم وحده كيف يختلف تشغيلان، فتقارن المنحنيات دون أن تفتح الإعدادات، ولا تخلط بين تجربتين بعد أسابيع.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.