Accelerate documentation
التوازي التسلسلي في 🤗 accelerate
التوازي التسلسلي في 🤗 accelerate
ستغطي هذه الدليل أساسيات استخدام توازي التسلسل في 🤗accelerate.
انظر أيضًا إلى التوازي السياقي ذي الصلة الوثيقة.
لماذا التوازي التسلسلي؟
مع ظهور نماذج اللغة الكبيرة، ونماذج الاستدلال مؤخراً، شهدت طول التسلسل نمواً سريعاً. وقد أدى هذا، مقترناً بالتعقيد الذاكري التربيعي للانتباه، إلى الحاجة إلى طرق أكثر كفاءة لتدريب النماذج ذات التسلسلات الطويلة.
عند طول تسلسل يبلغ 128k، فإن المتطلب الذاكري لمصفوفة الانتباه هو 128k * 128k * 2 bytes * num_heads = ~32 GB * num_heads لدقة bf16، بافتراض استخدام تطبيق الانتباه القياسي. ومنحاً، فإن استخدام flash attention أو SDPA، والتي لا تُجسّد أوزان الانتباه هذه، يُقلّص هذا المتطلب بشكل حاد، لكن النمو في المتطلبات الذاكية لا يزال يُعدّ كبيراً.
يتيح لنا توازي التسلسل في Ulysses تقسيم مدخلات حساب الانتباه على طول بُعد التسلسل وحساب الانتباه بشكل طبيعي، ولكن باستخدام جزء فقط من رؤوس الانتباه على كل وحدة معالجة رسومية. وبفضل ذلك، يمكننا تدريب نماذج ذات تسلسلات طويلة، مع بعض الأدوات الإضافية، والتوسع حتى طول تسلسل يتجاوز 15 مليون رمز. للاطلاع على كيفية تعزيز Ulysses SP باستخدام TiledMLP وLiger-Kernel وإزاحة نقاط التفتيش للتفعيل إلى وحدة المعالجة المركزية وبعض الحيل الأخرى، يُرجى الرجوع إلى الورقة البحثية: Arctic Long Sequence Training: Scalable And Efficient Training For Multi-Million Token Sequences.
كيف يختلف Ulysses SP عن FSDP CP
في المستند Context Parallelism يمكنك التعرف على نشر تقنية أخرى تُسمى Context Parallelism، والتي تقطع أيضاً على بُعد التسلسل لكنها تستخدم Ring Attention بدلاً من التقطيع على بُعد الرأس.
تتناول المقالات التالية شرحاً مفصلاً للغاية للفروق بين التقنيتين:
- https://insujang.github.io/2024-01-11/tensor-parallelism-and-sequence-parallelism-detailed-analysis/
- https://huggingface.co/blog/exploding-gradients/ulysses-ring-attention
ملخص سريع مقتبس من إحدى المقالات:
- يتميز Ulysses SP بعبء اتصال منخفض نسبيًا، لكنه مقيد بعدد رؤوس الانتباه (Attention Heads)، وبالتالي فإن له متطلبات معينة لبنية الشبكة (يجب أن يكون عدد رؤوس الانتباه قابلاً للقسمة على عدد وحدات معالجة الرسومات المشاركة في نسخة واحدة). اتصال All-to-all حساس للزمن المستغرق (Latency) ويتطلب DeepSpeed.
- لا تخضع اتصالات الحلقة P2P في FSDP CP Ring-Attention لمتطلبات القسمة المذكورة أعلاه، لكنها تتطلب حجم اتصال أعلى.
وأخيرًا، ينبغي أن يكون من الممكن الجمع بين SP و CP كما هو موضح في الورقة USP: A Unified Sequence Parallelism Approach for Long Context Generative AI لدعم طول تسلسل أطول حتى، على الرغم من أن هذا لم يتم دمجه بعد في 🤗accelerate.
واجهات الخلفية المدعومة للتوازي التسلسلي
حاليًا، الخلفية الوحيدة المتاحة لتوازي التسلسل هي deepspeed، وهي مستمدة من Ulysses SP الحديثة التي تُعد جزءًا من تقنية Arctic Long Sequence Training. كما يتوفر دليل تدريبي في حال رغبتَ في دمجها مباشرةً في كودك الخاص.
كيف تستخدم توازي التسلسل؟
from accelerate.utils import ParallelismConfig, DeepSpeedSequenceParallelConfig
+# Example: 4 GPUs with sp_size=4, dp_shard_size=1
+# Ensure: dp_replicate_size × dp_shard_size × sp_size = 1 × 1 × 4 = 4 GPUs
parallelism_config = ParallelismConfig(
+ sp_backend="deepspeed",
+ sp_size=4,
+ dp_shard_size=1, # Explicit: no data parallelism
+ sp_handler=DeepSpeedSequenceParallelConfig(
+ sp_seq_length_is_variable: true,
+ sp_attn_implementation="sdpa",
+ ),
+ )
accelerator = Accelerator(
...,
parallelism_config=parallelism_config,
)كما هو الحال مع أي ميزة أخرى في 🤗accelerate، يمكنك تفعيل توازي التسلسل أيضًا عن طريق تمرير الأعلام المقابلة إلى accelerate launch. وفي هذه الحالة، لا يوجد أي فرق:
accelerate launch --parallelism-config-sp-size 8 ...
يمكنك أيضًا ضبط
sp_sizeوإعدادات أخرى في أمرaccelerate config، حيث سيتم حفظها في ملف إعداداتaccelerateالخاص بك، فلا يضطر إلى تمريرها في كل مرة تطلق فيها سكربت.
يتكامل توازي التسلسل مع توازي البيانات. ولا يتطلب وحدات معالجة رسومية إضافية. لذا إذا كان لديك 8 وحدات معالجة رسومية يمكنك تنفيذ:
--parallelism-config-dp-shard-size 8 --parallelism-config-sp-size 8. أو يمكنك استخدام فئةParallelismConfigلضبطها برمجياً.مهم: يجب عليك التأكد من أن
dp_replicate_size × dp_shard_size × sp_size = num_processes. على سبيل المثال، مع 8 وحدات معالجة رسومية وsp_size=8، تحتاج إلىdp_shard_size=1(لأن 1 × 1 × 8 = 8). مع 4 وحدات معالجة رسومية وsp_size=2، يمكنك استخدامdp_shard_size=2(لأن 1 × 2 × 2 = 4) لتحقيق التوازي ثنائي الأبعاد.
ALST/Ulysses SP backend configuration
تنفّذ ALST/UlyssesSP توازي التسلسل باستخدام توازي رؤوس الانتباه، كما هو موضح في هذا الورقة. وبسبب البساطة، نعيد استخدام مفهوم وإعداد توازي التسلسل، والذي، من منظور المستخدم، هو نفسه: تُستخدم وحدات معالجة رسومية متعددة لمعالجة دفعة واحدة.
لتوضيح ما أتاحه ALST - فقد مكّننا من التدريب باستخدام bf16 مع 500 ألف رمز على وحدة معالجة رسومية H100 واحدة، و3.7 مليون رمز على عقدة واحدة، و15 مليون رمز على Llama-8B باستخدام أربع عقد فقط. تتيح هذه الميزة في HF Accelerate مكوّنًا واحدًا فقط من بين مكوّنات ALST الثلاثة، لذا فإن طول التسلسل القابل للتحقيق سيكون أقصر. ستحتاج إلى تفعيل TiledMLP، وإفراغ نقاط التفتيش للتفعيل إلى وحدة المعالجة المركزية، وبعض الأشياء الأخرى للحصول على القوة الكاملة لـ ALST. للتفاصيل، يُرجى الرجوع إلى هذا الشرح التعليمي.
لإعداد الخلفية deepspeed:
# Example: 4 GPUs with sp_size=4, dp_shard_size=1
# Ensure: dp_replicate_size × dp_shard_size × sp_size = 1 × 1 × 4 = 4 GPUs
parallelism_config = ParallelismConfig(
sp_backend="deepspeed",
sp_size=4,
dp_shard_size=1, # Explicit: no data parallelism
sp_handler=DeepSpeedSequenceParallelConfig(
sp_seq_length=256,
sp_seq_length_is_variable=True,
sp_attn_implementation="sdpa",
),
)
accelerator = Accelerator(
...,
parallelism_config=parallelism_config,
)sp_backend: اضبطه علىdeepspeedهناsp_sizeهو درجة توازي التسلسل - في المثال أعلاه قيمته 4، وبالتالي سيتم استخدام 4 وحدات معالجة رسومية لمعالجة دفعة واحدة (مع إجراء DP=4 على نفس وحدات المعالجة الرسومية)- تُستخدم
sp_seq_lengthوsp_seq_length_is_variableللتعامل مع أطوال التسلسلات. إذا كانsp_seq_length_is_variable=True، فسيعمل الخلفية مع طول تسلسل قد يتغير بين الدفعات، وفي هذه الحالة يمكن تعيين قيمةsp_seq_lengthإلى أي قيمة قابلة للقسمة على درجة التوازي التسلسلي أو عدم تعيينها على الإطلاق. في هذه الحالة، سيتم اشتقاق متغيرات التسلسل من المدخلات في كل عمليةforward. أما إذا كانتFalse، فيجب أن تتطابقseq_lengthمع بُعد طول التسلسل الخاص بالدفعة، مما سيتطلب حشوها (padding) لتبقى دائمًا بنفس الطول. القيمة الافتراضية هيTrue. sp_attn_implementationهو أحدsdpaأوflash_attention_2أوflash_attention_3. يستخدم هذا التنفيذ المتوازي للتسلسلposition_idsبدلاً منattention_mask، وبالتالي لا يمكن لـeagerالعمل هنا حتى يدعم التعامل معposition_ids. كما يُرجى ملاحظة أنsdpaلا يتعامل بشكل صحيح مع العينات المتعددة المدمجة في عينة واحدة؛ إذ سيقوم بالانتباه إلى العينة بأكملها كوحدة واحدة. أما إذا لم تكن العينات مدمجة، فسيعملsdpaبشكل صحيح. لذلك، يُعد Flash Attention الخيار المثالي لأنه يعمل دائمًا.
بدلاً من تعيين هذه القيم في كائن DeepSpeedSequenceParallelConfig، يمكنك أيضًا استخدام متغيرات البيئة لتحقيق نفس النتيجة - وهي هنا مقابلة لنهاية القائمة أعلاه.
PARALLELISM_CONFIG_SP_BACKENDPARALLELISM_CONFIG_SP_SEQ_LENGTHPARALLELISM_CONFIG_SP_SEQ_LENGTH_IS_VARIABLEPARALLELISM_CONFIG_SP_ATTN_IMPLEMENTATION
إذا لم يُمرَّر في الشيفرة، يمكن تعيين sp_size عبر حجة سطر الأوامر --parallelism_config_sp_size. وكذلك الأمر بالنسبة للحجج الأخرى. يمكنك أيضًا استخدام أسلوب تكوين ملف accelerate config، على سبيل المثال، لجهازَي GPU:
distributed_type: DEEPSPEED
deepspeed_config:
deepspeed_config_file: path/to/ds_config.json
machine_rank: 0
num_machines: 1
num_processes: 2
parallelism_config:
parallelism_config_dp_replicate_size: 1
parallelism_config_dp_shard_size: 1 # Must satisfy: 1 × 1 × 2 = 2 num_processes
parallelism_config_sp_size: 2
parallelism_config_sp_backend: deepspeed
parallelism_config_sp_seq_length_is_variable: true
parallelism_config_sp_attn_implementation: sdpa
كما ذُكر سابقًا، يُطبَّق عادةً توازي تسلسل Ulysses فوق توازي البيانات - حيث تُستخدم نفس الرتب لتغذية تدفقات بيانات فريدة وأداء توازي تسلسل Ulysses في آنٍ واحد. لكن يمكنك أيضًا إنشاء نسخ على النحو التالي:
# Example: 4 GPUs with 2D parallelism (SP=2, DP=2)
# Ensure: dp_replicate_size × dp_shard_size × sp_size = 2 × 1 × 2 = 4 GPUs
parallelism_config = ParallelismConfig(
dp_replicate_size=2,
dp_shard_size=1, # Explicit: no sharding within replicas
sp_size=2,
sp_backend="deepspeed",
sp_handler=DeepSpeedSequenceParallelConfig(...),
)هنا نستخدم 4 وحدات معالجة رسومية (GPU)، مع نسختين من توازٍ التسلسل (sequence parallelism). أما ما يقود توازٍ البيانات هنا فهو Deepspeed-ZeRO.
يرجى ملاحظة أن الكثير من العمليات الخفية تتم داخل UlyssesSPDataLoaderAdapter. يُستخدم في الخلفية، حيث يغلّف كائن DataLoader الأصلي الخاص بك، لكن ينبغي لك أن تكون على دراية به في حال واجهت أي مشاكل. كما يقوم تلقائيًا بحقن shift_labels الصحيحة في قاموس الدفعة (batch)، قبل أن يتم تقسيم الدفعة عبر الرتب المشاركة.
الآن، الجزء المتبقي الوحيد لبدء استخدام ALST/UlyssesSP هو تجميع الخسارة عبر الرتب باستخدام all_gather قابل للاشتقاق للحصول على التدرجات الصحيحة. يقوم الكود التالي بذلك، مع استبعاد أي رموز مقنّعة بـ -100 للحصول على المتوسط الصحيح:
sp_size = parallelism_config.sp_size if parallelism_config is not None else 1
if sp_size > 1:
sp_group = accelerator.torch_device_mesh["sp"].get_group()
sp_world_size = parallelism_config.sp_size
# Normal training loop
for iter, batch in enumerate(dl):
optimizer.zero_grad()
batch = move_to_device(batch, model.device)
# The model automatically receives shift_labels via **kwargs and uses it for loss computation.
# Both standard transformers models and Liger-patched models handle this correctly.
outputs = model(**batch)
loss = outputs.loss
shift_labels = batch["shift_labels"]
if sp_size > 1:
# differentiable weighted per-shard-loss aggregation across ranks
losses_per_rank = torch.distributed.nn.functional.all_gather(loss, group=sp_group)
# special dealing with SFT that has prompt tokens that aren't used in loss computation
good_tokens = (shift_labels != -100).view(-1).sum()
good_tokens_per_rank = torch.distributed.nn.functional.all_gather(
good_tokens, group=sp_group
)
# Skip ranks with zero valid tokens to avoid NaN contamination (NaN * 0 = NaN)
total_loss = sum(
losses_per_rank[rank] * good_tokens_per_rank[rank]
for rank in range(sp_world_size)
if good_tokens_per_rank[rank] > 0
)
total_good_tokens = sum(good_tokens_per_rank)
loss = total_loss / max(total_good_tokens, 1)
if rank == 0: accelerator.print(f"{iter}: {loss=}")
accelerator.log(dict(train_loss=loss, step=iter))
accelerator.backward(loss)
optimizer.step()لاحظ أن النماذج تتعامل تلقائيًا مع shift_labels عندما يكون حاضرًا في الدفعة. يستقبل مسار الأمامي للنموذج shift_labels عبر **kwargs ويمرره إلى دالة الخسارة، التي تحسب الخسارة بشكل صحيح للتوازي التسلسلي. إذا استخدمت Liger Kernel، فإنه يتعامل أيضًا مع shift_labels بسلاسة ويحسب الخسارة بطريقة فعالة جدًا من حيث الذاكرة. يُنصح بشدة باستخدام Liger لطول التسلسلات الطويل، حيث يحرر ذاكرة وحدة معالجة الرسوميات (GPU) من خلال استخدام عمليات مدمجة (مثل حساب logit-loss المدمج الذي لا يُنشئ قطار logits الكامل في الذاكرة أبدًا).
إذا كنت ترغب في الاطلاع على ما قام به HF Accelerate في الخلفية، فيرجى قراءة هذا الشرح التكاملي الكامل.
لمثال على حلقة تدريب باستخدام Accelerate مع تفعيل ALST/UlyssesSP، راجع examples/alst_ulysses_sequence_parallelism.
[!تحذير]
هذا الواجهة البرمجية (API) جديدة إلى حد كبير ولا تزال في مرحلتها التجريبية. وعلى الرغم من أننا نسعى جاهدين لتقديم واجهة برمجية مستقرة، إلا أن بعض الأجزاء الصغيرة من الواجهة البرمجية العامة قد تتغير في المستقبل.
بما أن هذا هو عمق Deepspeed، فإن إعداد Deepspeed المعتاد ينطبق، لذا يمكنك دمج توازي التسلسل مع تفريغ حالات المُحسِّن و/أو الأوزان أيضًا لتحرير المزيد من ذاكرة وحدة معالجة الرسوميات وتمكين طول تسلسل أطول حتى. وقد تم اختبار هذه التقنية لتعمل مع DeepSpeed ZeRO المرحلة 2 و 3.
Update on GitHub