Accelerate documentation
Gradyan senkronizasyonu
Gradyan senkronizasyonu
PyTorch’un dağıtık modülü, sistemdeki tüm GPU’lar arasında gidip gelerek iletişim kurarak çalışır.
Bu iletişim zaman alır ve ddp modülü kullanılırken tüm süreçlerin birbirlerinin durumlarını bilmesini sağlamak, belirli tetikleme noktalarında gerçekleşir.
Bu tetikleme noktaları, özellikle forward() ve backward() yöntemleri, PyTorch modeline eklenir.
Bu, model DistributedDataParallel ile sarıldığında gerçekleşir:
import torch.nn as nn
from torch.nn.parallel import DistributedDataParallel
model = nn.Linear(10, 10)
ddp_model = DistributedDataParallel(model)Accelerate'ta bu dönüşüm, prepare() çağrılırken modelinizi iletmeniz durumunda otomatik olarak gerçekleşir.
+ from accelerate import Accelerator
+ accelerator = Accelerator()
import torch.nn as nn
- from torch.nn.parallel import DistributedDataParallel
model = nn.Linear(10,10)
+ model = accelerator.prepare(model)Gradyan biriktirmedeki yavaşlama
Artık PyTorch'un, dağıtık bir yapılandırma içinde eğitim sırasında PyTorch modelinizin forward ve backward yöntemlerine kancalar (hook) eklediğini anlıyorsunuz. Peki bu, kodunuzun yavaşlamasına nasıl yol açabilir?
DDP (distributed data parallel) içinde, işlemlerin gerçekleştirilme ve çalıştırılma sırasının belirli noktalarda belirli bir şekilde olması beklenir ve bunların devam edebilmek için yaklaşık aynı anda gerçekleşmesi gerekir.
En doğrudan örnek, model parametrelerini optimizer.step() üzerinden güncellediğinizde ortaya çıkar.
Gradyan biriktirme yapılmadığında, modelin tüm örneklerinin, bir sonraki veri grubuna geçmeden önce
güncellenmiş gradyanlarının hesaplanması, toplanması ve güncellenmesi gerekir.
Gradyan biriktirme yapılırken, n kayıp gradyanı biriktirilir ve n gruba ulaşılana kadar
optimizer.step() atlanır. Tüm eğitim süreçlerinin yalnızca optimizer.step() çağrıldığında
senkronize olması gerektiğinden, eğitim adımınızda herhangi bir değişiklik yapmadan, bu gereksiz süreçler arası
iletişim önemli bir yavaşlamaya neden olabilir.
Bu ek yükten nasıl kaçınabilirsiniz?
Yavaşlama sorununu çözme
Bu topluluklarda eğitim sırasında model parametre güncellemelerini atladığınız için, optimizer.step() gerçekten çağrılana kadar gradyanlarının senkronize edilmesine gerek yoktur.
PyTorch, bunun ne zaman yapılması gerektiğini otomatik olarak anlayamaz, ancak DDP'ye dönüştürdükten sonra modelinize eklenen no_sync bağlam yöneticisi aracılığıyla bu konuda yardımcı olacak bir araç sunar.
Bu bağlam yöneticisi altında, .backward() çağrıldığında PyTorch gradyanları senkronize etmeyi atlar ve bu bağlam yöneticisinin dışındaki ilk .backward() çağrısı senkronizasyonu tetikler. Aşağıda bir örnek bulunuyor:
ddp_model, dataloader, optimizer = accelerator.prepare(model, dataloader, optimizer)
for index, batch in enumerate(dataloader):
inputs, targets = batch
# Trigger gradient synchronization on the last batch
if index != (len(dataloader) - 1):
with ddp_model.no_sync():
# Gradients only accumulate
outputs = ddp_model(inputs)
loss = loss_func(outputs)
accelerator.backward(loss)
else:
# Gradients finally sync
outputs = ddp_model(inputs)
loss = loss_func(outputs)
accelerator.backward(loss)
optimizer.step()Accelerate'ta bunu, eğitim cihazı ne olursa olsun çağrılabilen bir API haline getirmek için (ancak dağıtık bir sistemde değilseniz hiçbir şey yapmayabilir!), ddp_model.no_sync yerine no_sync() kullanılır ve aynı şekilde çalışır:
ddp_model, dataloader, optimizer = accelerator.prepare(model, dataloader, optimizer)
for index, batch in enumerate(dataloader):
inputs, targets = batch
# Trigger gradient synchronization on the last batch
if index != (len(dataloader)-1):
- with ddp_model.no_sync():
+ with accelerator.no_sync(model):
# Gradients only accumulate
outputs = ddp_model(inputs)
loss = loss_func(outputs, targets)
accelerator.backward(loss)
else:
# Gradients finally sync
outputs = ddp_model(inputs)
loss = loss_func(outputs)
accelerator.backward(loss)
optimizer.step()
optimizer.zero_grad()Bekleyeceğiniz üzere, accumulate() fonksiyonu, mevcut parti numarasını takip ederek bu koşullu kontrolü sarar ve size nihai gradyan biriktirme API'sini sunar:
ddp_model, dataloader, optimizer = accelerator.prepare(model, dataloader, optimizer)
for batch in dataloader:
with accelerator.accumulate(model):
optimizer.zero_grad()
inputs, targets = batch
outputs = model(inputs)
loss = loss_function(outputs, targets)
accelerator.backward(loss)
optimizer.step()
optimizer.zero_grad()Sonuç olarak, API seçimi söz konusu olduğunda accelerator.accumulate veya accelerator.no_sync kullanmanız gerekir.
Tam olarak ne kadar bir yavaşlama var ve yapabileceğiniz basit hatalar
Gerçekçi bir örnek oluşturmak için aşağıdaki kurulumu göz önünde bulundurun:
- İki tek GPU'lu T4 düğümü ve iki GPU'lu bir düğüm
- Her GPU bir T4'tür ve GCP üzerinde barındırılır
- Kullanılan betik, NLP Example betiğinin bir değişikliğidir
- GPU başına batch boyutu 16'dır ve gradyanlar her 4 adımda birikimlendirilir
Tüm betikler bu depoda mevcuttur.
Gradyan senkronizasyonu ve GPU iletişimine dikkat edilmezse, bu GPU'ların gereksiz dönemlerde birbirleriyle iletişim kurması sırasında çok fazla zaman harcanabilir.
Ne kadar?
Referans:
- Çizgi: Burada tartışılan senkronizasyon uygulamalarından hiçbiri kullanılmaz
no_syncyanlış kullanımı:no_syncyalnızcabackwardçağrısının etrafında,forwardçağrısının etrafında değilno_sync:no_syncdesenini doğru şekilde kullanmaaccumulate: accumulate() fonksiyonunun doğru kullanımı
Aşağıda, tek düğüm ve çift düğüm kurulumlarında her bir yapılandırma için 29 veri grubu üzerinde ortalama saniye/batch değerleri verilmiştir:
| Taban | no_sync yanlış | no_sync | accumulate | |
|---|---|---|---|---|
| Çok Düğümlü | 2±0.01s | 2.13±0.08s | 0.91±0.11s | 0.91±0.11s |
| Tek Düğüm | 0.50±0.01s | 0.50±0.01s | 0.41±0.015s | 0.41±0.015s |
Gördüğünüz gibi, gradyan senkronizasyonunu nasıl kurduğunuz konusunda dikkatli olmazsanız, eğitim sırasında 2 katından daha fazla yavaşlamayla karşılaşabilirsiniz!
Her şeyin düzgün yapıldığından emin olmak konusunda endişeliyseniz, Accelerator nesnesine gradient_accumulation_steps veya gradient_accumulation_plugin parametresini ileterek accumulate() fonksiyonunu kullanmanızı ve bu işlemin Accelerate tarafından sizin için halledilmesini şiddetle öneririz.
no_sync, FSDP kullanıldığında ek GPU belleği gerektirir
Gradyanların senkronize edilmemesinin FSDP eğitimi sırasında olumsuz etkileri olabileceğini unutmayın. torch içinde uyarıldığı gibi, FSDP için no_sync bağlam yöneticisi ek bellek gerektirecektir.
Dolayısıyla FSDP kullanılırken bellek yoğun durumlarda, no_sync işlevini devre dışı bırakmak için GradientAccumulationPlugin içinde sync_each_batch değerini True olarak ayarlamayı öneririz.
Aşağıdaki örnekte, Mixtral’ı (47B parametre) 8 A100-80GB GPU üzerinde ince ayarlıyoruz. no_sync etkinleştirildiğinde, mütevazı bir gradient_accumulation_steps=2 değeriyle bile hızla bellek yetersizliği (OOM) ile karşılaştığımızı görüyoruz. Bu durum yine, FSDP’nin no_sync özelliğinden kaynaklanan ek bellek yüklerinden kaynaklanmaktadır. Ancak no_sync, sync_each_batch=True ile devre dışı bırakıldığında, gradient_accumulation_steps=16 için bellek tüketimi, gradient_accumulation_steps=1 için olan seviyeye geri döner.
| Model | no_sync (accum=1) | no_sync (accum=2) | no_sync devre dışı (accum=16) |
|---|---|---|---|
| mixtral 8x7B | 69G | OOM | 69G |
Update on GitHub
no_sync'in devre dışı bırakılması, bu kılavuzun önceki bölümlerinde açıklandığı gibi, ek veri senkronizasyonları nedeniyle yavaşlamaya yol açar.