Accelerate documentation
تنفيذ تراكم التدرجات باستخدام Accelerate
تنفيذ تراكم التدرجات باستخدام Accelerate
تُعد تدرّج التراكم تقنية تتيح لك التدريب على أحجام دفع أكبر مما يمكن لجهازك عادةً استيعابه في الذاكرة. يتم ذلك عن طريق تراكم التدرّجات عبر عدة دفعات، وعدم تنفيذ خطوة المُحسِّن إلا بعد اكتمال عدد معين من الدفعات.
على الرغم من أن كود تراكم التدرجات القياسي تقنيًا يعمل بشكل جيد في الإعداد الموزع، إلا أنه ليس الطريقة الأكثر كفاءة للقيام بذلك وقد تواجه تباطؤات كبيرة!
في هذا الشرح التعليمي، ستتعلم كيفية إعداد تراكم التدرجات بسرعة وتنفيذه باستخدام الأدوات المتوفرة في Accelerate، حيث يمكن أن يقتصر الأمر على إضافة سطر واحد جديد فقط من الكود!
سيستخدم هذا المثال حلقة تدريب PyTorch بسيطة جداً تقوم بتراكم التدرجات كل دفعتين:
device = "cuda"
model.to(device)
gradient_accumulation_steps = 2
for index, batch in enumerate(training_dataloader):
inputs, targets = batch
inputs = inputs.to(device)
targets = targets.to(device)
outputs = model(inputs)
loss = loss_function(outputs, targets)
loss = loss / gradient_accumulation_steps
loss.backward()
if (index + 1) % gradient_accumulation_steps == 0:
optimizer.step()
scheduler.step()
optimizer.zero_grad()تحويله إلى Accelerate
أولاً، سيتم تحويل الكود المعروض سابقاً لاستخدام Accelerate دون الحاجة إلى مساعد تجميع التدرجات الخاص:
+ from accelerate import Accelerator
+ accelerator = Accelerator()
+ model, optimizer, training_dataloader, scheduler = accelerator.prepare(
+ model, optimizer, training_dataloader, scheduler
+ )
for index, batch in enumerate(training_dataloader):
inputs, targets = batch
- inputs = inputs.to(device)
- targets = targets.to(device)
outputs = model(inputs)
loss = loss_function(outputs, targets)
loss = loss / gradient_accumulation_steps
+ accelerator.backward(loss)
if (index+1) % gradient_accumulation_steps == 0:
optimizer.step()
scheduler.step()
optimizer.zero_grad()في حالته الحالية، لن يؤدي هذا الكود إلى تجميع التدرجات بكفاءة بسبب عملية تُسمى مزامنة التدرجات. اقرأ المزيد حول ذلك في دليل المفاهيم!
اترك Accelerate يتولى تراكم التدرجات
كل ما تبقّى الآن هو أن نترك Accelerate يتولى تجميع التدرجات نيابةً عنا. للقيام بذلك، يجب عليك تمرير معامل gradient_accumulation_steps إلى Accelerator، يحدد عدد الخطوات التي يجب تنفيذها قبل كل استدعاء لـstep()، وكيفية ضبط الخسارة تلقائياً أثناء الاستدعاء لـbackward():
from accelerate import Accelerator
- accelerator = Accelerator()
+ accelerator = Accelerator(gradient_accumulation_steps=2)بديلًا عن ذلك، يمكنك تمرير معامل gradient_accumulation_plugin إلى __init__ لكائن Accelerator، مما يتيح لك تخصيص سلوك تراكم التدرجات بشكل أكبر.
اقرأ المزيد حول ذلك في وثائق GradientAccumulationPlugin.
من هنا يمكنك استخدام مدير السياق accumulate() من داخل حلقة التدريب لديك لتنفيذ تراكم التدرجات تلقائيًا! كل ما عليك فعله هو تغليفه حول الجزء التدريبي الكامل من كودنا:
- for index, batch in enumerate(training_dataloader):
+ for batch in training_dataloader:
+ with accelerator.accumulate(model):
inputs, targets = batch
outputs = model(inputs)يمكنك إزالة جميع الفحوصات الخاصة برقم الخطوة وضبط الخسارة:
- loss = loss / gradient_accumulation_steps
accelerator.backward(loss)
- if (index+1) % gradient_accumulation_steps == 0:
optimizer.step()
scheduler.step()
optimizer.zero_grad()كما ترون، يستطيع Accelerator تتبع رقم الدفعة التي تعمل عليها، وسيعرف تلقائيًا ما إذا كان يجب الانتقال عبر المُحسِّن المُعدّ وكيفية ضبط الخسارة.
عادةً، عند استخدام تراكم التدرجات، تحتاج إلى تعديل عدد الخطوات لتعكس التغيير في إجمالي الدفعات التي تتدرب عليها. يقوم Accelerate بذلك تلقائيًا لك افتراضيًا. في الخلفية، نقوم بإنشاء
GradientAccumulationPluginمُهيأ للقيام بذلك.
يتم مزامنة state.GradientState مع مُحمِّل البيانات النشط الذي يتم التكرار عليه. وبناءً على ذلك، يفترض بشكل مبسّط أنه عند الوصول إلى نهاية مُحمِّل البيانات، ستتم المزامنة بالكامل وسيُنفَّذ خطوة. لتعطيل هذا السلوك، اضبط
sync_with_dataloaderعلىFalseفيGradientAccumulationPlugin:from accelerate import Accelerator from accelerate.utils import GradientAccumulationPlugin plugin = GradientAccumulationPlugin(sync_with_dataloader=False) accelerator = Accelerator(..., gradient_accumulation_plugin=plugin)
الكود النهائي
فيما يلي التنفيذ النهائي لأداء تراكم التدرجات باستخدام Accelerate
from accelerate import Accelerator
accelerator = Accelerator(gradient_accumulation_steps=2)
model, optimizer, training_dataloader, scheduler = accelerator.prepare(
model, optimizer, training_dataloader, scheduler
)
for batch in training_dataloader:
with accelerator.accumulate(model):
inputs, targets = batch
outputs = model(inputs)
loss = loss_function(outputs, targets)
accelerator.backward(loss)
optimizer.step()
scheduler.step()
optimizer.zero_grad()من المهم أن يتم تنفيذ عملية تمرير/رجوع واحدة فقط داخل مدير السياق
with accelerator.accumulate(model).
للتعرف على المزيد حول ما تخفيه هذه الآلية، اقرأ دليل مفهوم مزامنة التدرجات
مثال مكتفٍ ذاتياً
إليك مثالًا مكتفيًا ذاتيًا يمكنك تشغيله لرؤية تراكم التدرجات قيد العمل مع Accelerate:
import torch
import copy
from accelerate import Accelerator
from accelerate.utils import set_seed
from torch.utils.data import TensorDataset, DataLoader
# seed
set_seed(0)
# define toy inputs and labels
x = torch.tensor([1., 2., 3., 4., 5., 6., 7., 8.])
y = torch.tensor([2., 4., 6., 8., 10., 12., 14., 16.])
gradient_accumulation_steps = 4
per_device_batch_size = len(x) // gradient_accumulation_steps
# define dataset and dataloader
dataset = TensorDataset(x, y)
dataloader = DataLoader(dataset, batch_size=per_device_batch_size)
# define model, optimizer and loss function
class SimpleLinearModel(torch.nn.Module):
def __init__(self):
super(SimpleLinearModel, self).__init__()
self.weight = torch.nn.Parameter(torch.zeros((1, 1)))
def forward(self, inputs):
return inputs @ self.weight
model = SimpleLinearModel()
model_clone = copy.deepcopy(model)
criterion = torch.nn.MSELoss()
model_optimizer = torch.optim.SGD(model.parameters(), lr=0.02)
accelerator = Accelerator(gradient_accumulation_steps=gradient_accumulation_steps)
model, model_optimizer, dataloader = accelerator.prepare(model, model_optimizer, dataloader)
model_clone_optimizer = torch.optim.SGD(model_clone.parameters(), lr=0.02)
print(f"initial model weight is {model.weight.mean().item():.5f}")
print(f"initial model weight is {model_clone.weight.mean().item():.5f}")
for i, (inputs, labels) in enumerate(dataloader):
with accelerator.accumulate(model):
inputs = inputs.view(-1, 1)
print(i, inputs.flatten())
labels = labels.view(-1, 1)
outputs = model(inputs)
loss = criterion(outputs, labels)
accelerator.backward(loss)
model_optimizer.step()
model_optimizer.zero_grad()
loss = criterion(x.view(-1, 1) @ model_clone.weight, y.view(-1, 1))
model_clone_optimizer.zero_grad()
loss.backward()
model_clone_optimizer.step()
print(f"w/ accumulation, the final model weight is {model.weight.mean().item():.5f}")
print(f"w/o accumulation, the final model weight is {model_clone.weight.mean().item():.5f}")initial model weight is 0.00000
initial model weight is 0.00000
0 tensor([1., 2.])
1 tensor([3., 4.])
2 tensor([5., 6.])
3 tensor([7., 8.])
w/ accumulation, the final model weight is 2.04000
w/o accumulation, the final model weight is 2.04000تراكم التدرجات على عينات تدريب ذات أحجام متغيرة
كما نُقِط في هذا منشور المدونة، الذي يُبرز خطأً شائعاً يحدث عند تنفيذ تراكم التدرجات على عينات تدريب ذات أحجام متغيرة:
[…] لتجميع التدرجات في المهام على مستوى الرموز مثل تدريب نموذج اللغة السببي، يجب حساب الخسارة الصحيحة عن طريق قسمة إجمالي الخسارة عبر جميع الدفعات في خطوة تجميع التدرجات على إجمالي عدد جميع الرموز غير الحشو في تلك الدفعات. وهذا ليس مماثلًا لمتوسط قيم الخسارة لكل دفعة.
بعبارة أخرى، يجب إجراء بعض التعديلات على الدوال الخاسرة التي تعمل على مستوى الرموز.
احسب الأهداف التي تساهم فعليًا في دالة الخسارة. بالنسبة لنماذج اللغة السببية في Transformers التي تُزاح التسميات داخليًا،
مرّر التسميات غير المُزاحة واحسب (labels[..., 1:] != -100).sum(): فالتسمية الأولى لا يوجد لها تنبؤ سابق.
لا تُزح تلك التسميات مرة أخرى في مُجمِّع البيانات. يستخدم مثال التراكم التلقائي التوليفي هذا الاتفاق؛ وقد تستخدم دوال الخسارة الأخرى محاذاة مختلفة للأهداف.
كود الهيكل
from accelerate import Accelerator
import math
import contextlib
gradient_accumulation_steps = 2
accelerator = Accelerator(gradient_accumulation_steps=gradient_accumulation_steps)
model, optimizer, training_dataloader, scheduler = accelerator.prepare(
model, optimizer, training_dataloader, scheduler
)
training_iterator = iter(training_dataloader)
num_samples_in_epoch = len(training_dataloader)
remainder = num_samples_in_epoch % gradient_accumulation_steps
remainder = remainder if remainder != 0 else gradient_accumulation_steps
total_updates = math.ceil(num_samples_in_epoch / gradient_accumulation_steps)
total_batched_samples = 0
for update_step in range(total_updates):
# In order to correctly the total number of non-padded tokens on which we'll compute the cross-entropy loss
# we need to pre-load the full local batch - i.e the next per_device_batch_size * accumulation_steps samples
batch_samples = []
num_batches_in_step = gradient_accumulation_steps if update_step != (total_updates - 1) else remainder
for _ in range(num_batches_in_step):
batch_samples += [next(training_iterator)]
# get local num items in batch
num_items_in_batch = sum([(batch["labels"].ne(-100)).sum() for batch in batch_samples])
# to compute it correctly in a multi-device DDP training, we need to gather the total number of items in the full batch.
num_items_in_batch = accelerator.gather(num_items_in_batch).sum().item()
for i, batch in enumerate(batch_samples):
# if we perform gradient accumulation in a multi-devices set-up, we want to avoid unnecessary communications when accumulating
# cf: https://muellerzr.github.io/blog/gradient_accumulation.html
if (i < len(batch_samples) - 1 and accelerator.num_processes > 1):
ctx = model.no_sync
else:
ctx = contextlib.nullcontext
total_batched_samples += 1
with ctx():
inputs, targets = batch
outputs = model(inputs)
loss = loss_function(outputs, targets) # the loss function should sum over samples rather than averaging
# We multiply by num_processes because the DDP calculates the average gradient across all devices whereas dividing by num_items_in_batch already takes into account all devices
# Same reason for gradient_accumulation_steps, but this times it's Accelerate that calculate the average gradient across the accumulated steps
loss = (loss * gradient_accumulation_steps * accelerator.num_processes) / num_items_in_batch
accelerator.backward(loss)
# Sync gradients and perform optimization steps once every gradient_accumulation_steps
optimizer.step()
scheduler.step()
optimizer.zero_grad()مثال ذاتي التضمين لنموذج اللغة التكاملي
import torch
import copy
from accelerate import Accelerator
from accelerate.utils import set_seed
from accelerate.logging import get_logger
from torch.utils.data import Dataset, DataLoader
import math
import contexlib
# seed
set_seed(0)
logger = get_logger(__name__)
class MyDataset(Dataset):
def __init__(self, num_samples):
super().__init__()
self.len = num_samples
def __getitem__(self, index):
input_ids = torch.arange(1, index+2, dtype=torch.float32)
labels = torch.remainder(input_ids, 2)
return {"input_ids": input_ids, "labels": labels}
def __len__(self):
return self.len
def collate_fn(features):
input_ids = torch.nn.utils.rnn.pad_sequence([f["input_ids"] for f in features], batch_first=True, padding_value=-100)
labels = torch.nn.utils.rnn.pad_sequence([f["labels"] for f in features], batch_first=True, padding_value=-100)
return {"input_ids": input_ids[..., None], "labels": labels[..., None]}
# define toy inputs and labels
gradient_accumulation_steps = 2
per_device_batch_size = 4
# define accelerator
accelerator = Accelerator(gradient_accumulation_steps=gradient_accumulation_steps)
# define dataset and dataloader
# for this toy example, we'll compute gradient descent over one single global batch
dataset = MyDataset(per_device_batch_size*gradient_accumulation_steps*accelerator.num_processes)
dataloader = DataLoader(dataset, batch_size=per_device_batch_size, collate_fn=collate_fn)
# define model, model_optimizer and loss function
model = torch.nn.Linear(1, 2, bias=False)
model_clone = copy.deepcopy(model)
criterion = torch.nn.CrossEntropyLoss(reduction="sum") # must sum over samples rather than averaging
model_optimizer = torch.optim.SGD(model.parameters(), lr=0.08)
logger.warning(f"initial model weight is {model.weight.detach().cpu().squeeze()}")
logger.warning(f"initial model clone weight is {model_clone.weight.detach().cpu().squeeze()}")
# prepare artifacts - accelerator handles device placement and dataloader splitting
model, model_optimizer = accelerator.prepare(model, model_optimizer)
dataloader = accelerator.prepare_data_loader(dataloader, device_placement=True)
training_iterator = iter(dataloader)
num_samples_in_epoch = len(dataloader)
remainder = num_samples_in_epoch % gradient_accumulation_steps
remainder = remainder if remainder != 0 else gradient_accumulation_steps
total_gradient_updates = math.ceil(num_samples_in_epoch / gradient_accumulation_steps)
total_batched_samples = 0
for update_step in range(total_gradient_updates):
# In order to correctly the total number of non-padded tokens on which we'll compute the cross-entropy loss
# we need to pre-load the full local batch - i.e the next per_device_batch_size * accumulation_steps samples
batch_samples = []
num_batches_in_step = gradient_accumulation_steps if update_step != (total_gradient_updates - 1) else remainder
for _ in range(num_batches_in_step):
batch_samples += [next(training_iterator)]
# get local num items in batch
local_num_items_in_batch = sum([(batch["labels"].ne(-100)).sum() for batch in batch_samples])
logger.warning(f"Step {update_step} - Device {accelerator.process_index} - num items in the local batch {local_num_items_in_batch}", main_process_only=False)
# to compute it correctly in a multi-device DDP training, we need to gather the total number of items in the full batch.
num_items_in_batch = accelerator.gather(local_num_items_in_batch).sum().item()
logger.warning(f"Total num items {num_items_in_batch}")
for i, batch in enumerate(batch_samples):
inputs, labels = batch["input_ids"], batch["labels"]
total_batched_samples += 1
# if we perform gradient accumulation in a multi-devices set-up, we want to avoid unnecessary communications when accumulating
# cf: https://muellerzr.github.io/blog/gradient_accumulation.html
if (i < len(batch_samples) - 1 and accelerator.num_processes > 1):
ctx = model.no_sync
else:
ctx = contextlib.nullcontext
with ctx():
outputs = model(inputs)
loss = criterion(outputs.view(-1, 2), labels.view(-1).to(torch.int64))
# We multiply by num_processes because the DDP calculates the average gradient across all devices whereas dividing by num_items_in_batch already takes into account all devices
# Same reason for gradient_accumulation_steps, but this times it's Accelerate that calculate the average gradient across the accumulated steps
loss = (loss * gradient_accumulation_steps * accelerator.num_processes) / num_items_in_batch
accelerator.backward(loss)
model_optimizer.step()
model_optimizer.zero_grad()
logger.warning(f"Device {accelerator.process_index} - w/ accumulation, the final model weight is {accelerator.unwrap_model(model).weight.detach().cpu().squeeze()}", main_process_only=False)
# We know do the same operation but on a single device and without gradient accumulation
if accelerator.is_main_process:
# prepare one single entire batch
dataloader = DataLoader(dataset, batch_size=len(dataset), collate_fn=collate_fn)
full_batch_without_accum = next(iter(dataloader))
total_inputs, total_labels = full_batch_without_accum["input_ids"], full_batch_without_accum["labels"]
model_clone_optimizer = torch.optim.SGD(model_clone.parameters(), lr=0.08)
# train the cloned model
loss = torch.nn.CrossEntropyLoss(reduction="mean")(model_clone(total_inputs).view(-1, 2), total_labels.view(-1).to(torch.int64))
model_clone_optimizer.zero_grad()
loss.backward()
model_clone_optimizer.step()
# We should have the same final weights.
logger.warning(f"w/o accumulation, the final model weight is {model_clone.weight.detach().cpu().squeeze()}")
النتائج على جهاز واحد - حيث تم ضبط خطوات تراكم التدرج على 1 وحجم الدفعة على 8:
initial model weight is tensor([-0.0075, 0.5364])
initial model clone weight is tensor([-0.0075, 0.5364])
Step 0 - Device 0 - num items in the local batch 36
Total num items 36
Device 0 - w/ accumulation, the final model weight is tensor([0.0953, 0.4337])
w/o accumulation, the final model weight is tensor([0.0953, 0.4337])النتائج على إعداد يتكون من جهازين - حيث تم ضبط خطوات تراكم التدرج على 2 وحجم الدفعة على 4.
initial model weight is tensor([-0.0075, 0.5364])
initial model clone weight is tensor([-0.0075, 0.5364])
Step 0 - Device 0 - num items in the local batch 52
Step 0 - Device 1 - num items in the local batch 84
Total num items 136
Device 1 - w/ accumulation, the final model weight is tensor([0.2117, 0.3172])
Device 0 - w/ accumulation, the final model weight is tensor([0.2117, 0.3172])
w/o accumulation, the final model weight is tensor([0.2117, 0.3172])للمضي قدمًا:
يمكنك العثور على مثال كامل لسكربت تدريب حقيقي في مجلد الأمثلة عند المسار accelerate/examples/by_feature/gradient_accumulation_for_autoregressive_models.py.
عند تشغيله على عدة تكوينات تدريب بحجم دفعة إجمالي ثابت يساوي 32، نحصل على المخطط التالي:

لاحظ أن خسائر التدريب متطابقة تمامًا حتى خطوة التدريب 20. ويحدث الانحراف الطفيف بعد هذه خطوة التدريب في نهاية الحقبة الأولى تمامًا، وذلك لأن مُحمِّل البيانات، بشكل افتراضي، يُكرّر العينات الموجودة في بداية مجموعة البيانات عندما لا يقسم حجم الدفعة الإجمالي مجموعة البيانات بشكل صحيح.
Update on GitHub