فاز 19 · درس 78

خرد کردن وضعیت بهینه‌ساز با ZeRO-1: پیاده‌سازی و تحلیل حافظه

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
پیش‌نیاز
فاز 19 مسیر C، درس‌های 42 تا 49
نمایی مفهومی از بلوک‌های حافظه که به چند بخش مساوی تقسیم شده و میان سرورها پخش می‌شوند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. مراحل مختلف ZeRO
  5. محاسبات ریاضی حافظه با اعداد واقعی
  6. چرا reduce_scatter از ترکیب allreduce-سپس-خردکردن بهتر است؟
  7. پیاده‌سازی
  8. الگوهای کاربردی در محیط عملیاتی
  9. استفاده از ابزارهای واقعی
  10. جمع‌بندی و ادامه‌ی مسیر
  11. تمرین‌ها
  12. اصطلاحات کلیدی
  13. برای مطالعه‌ی بیشتر

آموزش مدل‌های زبانی چندمیلیارد پارامتری چالش بزرگی به نام کمبود حافظه پردازنده گرافیکی (VRAM) به همراه دارد. در آموزش به روش موازی‌سازی داده استاندارد (DDP)، همه اطلاعات شامل وزن‌ها، گرادیان‌ها و وضعیت بهینه‌ساز روی تک‌تک کارت‌ها کپی می‌شوند. وضعیت بهینه‌ساز Adam به‌تنهایی بیش از نیمی از فضای حافظه را می‌بلعد و به مانع اصلی در آموزش مدل‌های بزرگ تبدیل می‌شود.

با توجه به اینکه تهیه و اجاره GPUهای مدرن با حافظه بالا برای توسعه‌دهندگان ایرانی هزینه‌بر و دسترسی به آن‌ها دشوار است، بهینه‌سازی مصرف حافظه اهمیت دوچندانی پیدا می‌کند. با روش‌هایی مثل خرد کردن وضعیت بهینه‌ساز (ZeRO Stage 1)، می‌توان همان مدل‌ها را روی سخت‌افزارهای دردسترس‌تر با ظرفیت حافظه محدودتر به پیش برد.

در این درس یاد می‌گیرید که چگونه با تکنیک ZeRO-1 وضعیت الگوریتم Adam و کپی با دقت float32 را میان کارت‌های مختلف تقسیم کنید، با عملیات reduce_scatter و allgather ترافیک ارتباطی را بدون تغییر نگه دارید و بدون افت سرعت، مصرف حافظه را به شکل خطی کاهش دهید.

الگوریتم Adam برای هر پارامتر دو تخمین گشتاور (moment estimates) ذخیره می‌کند که هر دو از نوع float32 هستند. یک مدل 7 میلیارد پارامتری (7B) حدود 56 گیگابایت وضعیت بهینه‌ساز (optimiser state) به همراه دارد. الگوریتم ZeRO stage 1 این حافظه را بین N رتبه (rank) خرد (shard) می‌کند؛ یعنی هر رتبه فقط مالک 1/N از بهینه‌ساز خواهد بود. پس از گام بهینه‌سازی محلی، قطعه‌های به‌روزشده‌ی پارامترها بازپخش (broadcast) می‌شوند، همه‌ی رتبه‌ها مدل کامل را بازسازی می‌کنند و گام بعدی آغاز می‌شود. دستاورد این کار، کاهش خطی مصرف حافظه در بزرگ‌ترین بخش تخصیص‌یافته در کل استک آموزش است.

اهداف یادگیری

  • خرد کردن وضعیت بهینه‌ساز (گشتاور اول، گشتاور دوم و نسخه‌ی اصلی با دقت fp32) میان N رتبه به‌طوری که سهم هر رتبه 1/N باشد.
  • استفاده از reduce_scatter برای رساندن مجموع گرادیان‌های مربوط به هر قطعه فقط به رتبه‌ی مالک آن، و سپس بهره‌گیری از allgather برای بازپخش مجدد قطعه‌های به‌روزشده‌ی پارامترها.
  • محاسبه‌ی جدول صرفه‌جویی حافظه برای ZeRO-1، ZeRO-2 و ZeRO-3 در مقایسه با DDP معمولی.
  • توجیه انتخاب بین ZeRO-1، ZeRO-2 یا ZeRO-3 بر اساس ابعاد مدل و بودجه‌ی پهنای باند شبکه.

صورت مسئله

روش معمولی DDP همه‌چیز را تکثیر می‌کند: پارامترها، گرادیان‌ها و وضعیت بهینه‌ساز به‌طور کامل روی تک‌تک رتبه‌ها وجود دارند. برای یک مدل 7B با دقت fp16، این موضوع به معنای 14 گیگابایت پارامتر، 14 گیگابایت گرادیان و 56 گیگابایت وضعیت بهینه‌ساز (دو گشتاور float32 الگوریتم Adam) روی هر رتبه است. علاوه‌بر این، آموزش با دقت ترکیبی (Mixed Precision) یک نسخه‌ی اصلی (master copy) از وزن‌ها با دقت float32 به حجم 28 گیگابایت نیز به این ارقام اضافه می‌کند. وضعیت بهینه‌ساز بزرگ‌ترین بخش حافظه را اشغال می‌کند و در عین حال ساده‌ترین بخش برای خرد کردن است؛ زیرا فقط در طول گام به‌روزرسانی پارامترها با آن سروکار داریم، نه در مرحله‌ی رفت (forward) یا برگشت (backward).

روش ZeRO stage 1 وضعیت بهینه‌ساز را خرد می‌کند. هر رتبه 1/N از گشتاورهای Adam را نگه می‌دارد. پس از مرحله‌ی برگشت، به‌جای اجرای allreduce روی کل گرادیان و اِعمال گام به‌روزرسانی به‌صورت محلی، ZeRO از reduce_scatter استفاده می‌کند تا هر رتبه فقط گرادیان‌های جمع‌شده‌ی مربوط به قطعه‌ی خودش را تحویل بگیرد. سپس رتبه، گام بهینه‌ساز را تنها روی قطعه‌ی مربوط به پارامترهای اصلی خود اعمال می‌کند. قطعه‌های به‌روزشده‌ی پارامترها در ادامه از طریق allgather بازپخش می‌شوند تا همه‌ی رتبه‌ها مدل کامل را برای مرحله‌ی رفت بعدی در اختیار داشته باشند. حافظه‌ی مصرفی بهینه‌ساز بر N تقسیم می‌شود. ترافیک شبکه در هر گام دقیقاً معادل DDP است: از نظر پهنای باند مصرفی، یک reduce_scatter به‌اضافه‌ی یک allgather برابر است با یک allreduce. نتیجه این‌که حافظه آزاد می‌شود و نرخ پردازش (throughput) ثابت می‌ماند.

مفاهیم پایه

رفت و برگشت روی مدل کاملتکمیل گرادیان‌ها در هر رتبهreduce_scatter gradsرتبه r شارد گرادیان جمع‌شده r را نگه می‌داردگام Adam روی شارد r با استفاده از وضعیتبهینه‌ساز محلیشارد پارامتر به‌روزشده rallgather param shardsرفت بعدی دوباره مدل کامل را می‌بیند

مراحل مختلف ZeRO

مرحله آنچه خرد می‌شود حافظه به ازای هر رتبه ارتباطات شبکه در هر گام
DDP هیچ‌چیز params + grads + optim 1x allreduce
ZeRO-1 وضعیت بهینه‌ساز params + grads + optim/N 1x reduce_scatter + 1x allgather
ZeRO-2 بهینه‌ساز + گرادیان‌ها params + grads/N + optim/N 1x reduce_scatter + 1x allgather
ZeRO-3 بهینه‌ساز + گرادیان‌ها + پارامترها params/N + grads/N + optim/N 1x allgather در هر لایه + 1x reduce_scatter در هر لایه

مرحله‌ی اول (Stage 1) کم‌هزینه‌ترین برد را به همراه دارد، زیرا وضعیت بهینه‌ساز بیشترین سهم را در بودجه‌ی حافظه دارد. مرحله‌ی دوم (Stage 2) نیازمند منطق تجمیع قطعه‌های گرادیان است، اما پهنای باند آن تفاوتی نمی‌کند. مرحله‌ی سوم (Stage 3 یا همان FSDP) به ازای هر مرحله‌ی رفت و برگشت هزینه‌ی ارتباطی لایه‌به‌لایه را متحمل می‌شود تا در عوض حافظه‌ی مربوط به قطعه‌های پارامترها را هم آزاد کند. در این درس مرحله‌ی اول را به‌طور کامل پیاده‌سازی می‌کنیم.

محاسبات ریاضی حافظه با اعداد واقعی

برای مدلی با P پارامتر که با الگوریتم Adam و دقت ترکیبی آموزش می‌بیند:

بخش معمولی ZeRO-1 دلیل
پارامترهای fp16 2P بایت 2P بایت برای رفت مورد نیاز است
گرادیان‌های fp16 2P بایت 2P بایت برای برگشت مورد نیاز است
نسخه‌ی اصلی fp32 4P بایت 4P/N بایت فقط بهینه‌ساز از آن استفاده می‌کند
گشتاور اول fp32 4P بایت 4P/N بایت فقط بهینه‌ساز از آن استفاده می‌کند
گشتاور دوم fp32 4P بایت 4P/N بایت فقط بهینه‌ساز از آن استفاده می‌کند
مجموع 16P بایت 4P + 12P/N بایت

در حالت N=8: روش معمولی 16P بایت و ZeRO-1 برابر 5.5P بایت است که 65% کاهش نشان می‌دهد. در حالت N=64: روش معمولی 16P بایت و ZeRO-1 برابر 4.19P بایت است که 74% کاهش به همراه دارد.

چرا reduce_scatter از ترکیب allreduce-سپس-خردکردن بهتر است؟

عملیات Allreduce گرادیان جمع‌شده‌ی کامل را در اختیار همه‌ی رتبه‌ها می‌گذارد. اگر تنها به قطعه‌ی r نیاز داشته باشید، عملاً بخش (N-1)/N از گرادیان جمع‌شده روی رتبه‌ی r هدر می‌رود. عملیات Reduce_scatter دقیقاً همان قطعه‌ای را تحویل می‌دهد که رتبه مالک آن است؛ حجم بایت‌های ردوبدل‌شده به ازای هر رتبه معادل allreduce است (چون allreduce ترکیبی از reduce_scatter به‌اضافه‌ی allgather است)، اما نیمه‌ی دوم بعداً با allgather مربوط به قطعه‌های پارامترها جایگزین می‌شود. ترافیک خالص شبکه کاملاً یکسان با DDP است، در حالی که حافظه بر N تقسیم می‌شود.

خرد کردن وضعیت در ZeROتقسیم بهینه‌ساز به نسبت 1/N به ازای هر رتبه
ZeRO STATE SHARDINGVanilla DDP replicates the optimiser state on every rank, the largest allocation in the stack. ZeRO stage 1 gives each rank only 1/N of the Adam moments. reduce_scatter delivers each rank its gradient shard, it steps locally, then allgather broadcasts the updated parameter shards back so every rank rebuilds the full model. Memory drops linearly; the wire cost equals one allreduce.هر رتبه: پارامترهای کامل، ولی فقط 1/N از بهینه‌سازپارامترهاAdamشارد 0رتبه 0پارامترهاAdamشارد 1رتبه 1پارامترهاAdamشارد 2رتبه 2پارامترهاAdamشارد 3رتبه 3reduce_scatter grads inخروجی allgather پارامترها
در DDP معمولی وضعیت بهینه‌ساز در تمام رتبه‌ها تکثیر می‌شود که حجیم‌ترین بخش حافظه است. مرحله‌ی 1 الگوریتم ZeRO تنها 1/N گشتاورهای Adam را به هر رتبه اختصاص می‌دهد. دستور reduce_scatter شارد گرادیان هر رتبه را تحویل داده، گام بهینه‌سازی به‌صورت محلی اجرا می‌شود و سپس allgather شاردهای پارامتر به‌روزرسانی‌شده را بازپخش می‌کند تا مدل کامل در هر رتبه بازسازی شود. مصرف حافظه به‌صورت خطی کاهش می‌یابد و بار شبکه معادل یک allreduce باقی می‌ماند.

پیاده‌سازی

فایل code/main.py موارد زیر را پیاده‌سازی می‌کند:

  • توابع flatten_params(module) و unflatten_into(module, flat) که پارامترهای مدل را داخل یک تنسور پیوسته در حافظه (contiguous) بسته‌بندی کرده و سپس به حالت اولیه بازمی‌گردانند. ساختار مسطح و پیوسته همان چیزی است که خرد کردن بر اساس رتبه را به یک برش (slice) ساده تبدیل می‌کند.
  • کلاس ZeroOptimizer(model, world_size, rank, lr) که مالک قطعه‌ی مربوط به رتبه از نسخه‌ی اصلی و گشتاورهای Adam است.
  • متد step() که reduce_scatter را روی گرادیان مسطح اجرا می‌کند، الگوریتم Adam را روی قطعه‌ی همان رتبه اعمال کرده و پارامترهای به‌روزشده را با allgather بازپخش می‌کند.
  • یک نمونه‌ی نمایشی که یک پرسپترون چندلایه (MLP) سه‌لایه را برای 20 گام آموزش می‌دهد و بودجه‌ی حافظه‌ی مصرفی در هر گام را در کنار یک خط مبنا (baseline) از DDP معمولی چاپ می‌کند.

اجرای برنامه:

python3 code/main.py

خروجی: تابع زیان (loss) در هر گام و جدول حافظه‌ای که نشان می‌دهد ZeRO-1 برخلاف نسخه‌ی کامل DDP، تنها 1/N از وضعیت بهینه‌ساز را روی هر رتبه نگه می‌دارد.

الگوهای کاربردی در محیط عملیاتی

سه الگو برای آماده‌سازی ZeRO جهت استقرار در محیط عملیاتی (پروداکشن) ضروری است:

چک‌پوینت‌گیری خردشده (Sharded Checkpointing) اهمیت حیاتی دارد. وضعیت بهینه‌ساز در ZeRO-1 میان رتبه‌ها تقسیم شده است؛ بنابراین چک‌پوینت باید ثبت کند که چه رتبه‌ای مالک چه بخشی است. در درس 80 مانیفست (manifest) چک‌پوینت خردشده را می‌سازیم که امکان ادامه‌ی اجرای ZeRO را با همان تعداد رتبه (world size) فراهم می‌کند. بدون این مانیفست، وضعیت ذخیره‌شده هنگام راه‌اندازی مجدد غیرقابل خواندن خواهد بود.

اصل ماجرا دقت ترکیبی است. روش ZeRO یک تکنیک مبتنی بر دقت ترکیبی است؛ چیزی که خرد می‌شود همان نسخه‌ی اصلی با دقت fp32 است. اجرای ZeRO بدون دقت ترکیبی، هزینه‌ی حافظه‌ی نسخه‌ی اصلی fp32 را به سیستم تحمیل می‌کند بدون اینکه صرفه‌جویی حاصل از دقت fp16 در مرحله‌ی رفت را در بر داشته باشد. اجراهای محیط عملیاتی همیشه ZeRO را با autocast یا وزن‌های bf16 همراه می‌کنند.

مرحله‌ی اول بردی تقریباً بدون هزینه است. ترافیک ارتباطی از نظر پهنای باند دقیقاً مانند DDP است. صرفه‌جویی در حافظه رابطه‌ی خطی با N دارد. تنها هزینه‌ی آن، ردیابی و مدیریت قطعه‌های وضعیت بهینه‌ساز است. استک‌های عملیاتی (production stack) همیشه به‌طور پیش‌فرض از مرحله‌ی اول استفاده می‌کنند مگر این‌که حافظه‌ی گرادیان یا پارامترها نیز گلوگاه شود. مرحله‌ی دوم گرادیان‌ها را بدون هیچ ترافیک شبکه‌ی اضافه‌ای خرد می‌کند؛ تنها مرحله‌ی سوم است که خود پارامترها را خرد می‌کند و در ازای این صرفه‌جویی حافظه، هزینه‌ی ارتباطی لایه‌به‌لایه را می‌پذیرد.

استفاده از ابزارهای واقعی

الگوهای محیط عملیاتی:

  • DeepSpeed ZeRO: پیاده‌سازی مرجع. فایل deepspeed_config.json مراحل 1، 2 یا 3 و ابعاد پارتیشن‌ها را تعیین می‌کند.
  • PyTorch FSDP: معادل بومی پایتورچ. مقدار ShardingStrategy.SHARD_GRAD_OP همان ZeRO-2 است و FULL_SHARD معادل ZeRO-3 خواهد بود.
  • HuggingFace Accelerate: هر دو ابزار DeepSpeed و FSDP را ذیل یک پیکربندی یکپارچه ارائه می‌دهد.

جمع‌بندی و ادامه‌ی مسیر

درس 79 (موازی‌سازی پایپ‌لاینی یا Pipeline Parallel) بُعد عمود بر این خردسازی را بررسی می‌کند: به‌جای خرد کردن وضعیت بهینه‌سازِ یک مدل واحد، پایپ‌لاین لایه‌ها را میان رتبه‌ها تقسیم می‌کند. در درس 81 ترکیب DDP و ZeRO را در قالب یک دموی سرتاسری (end-to-end) پیاده‌سازی می‌کنیم.

تمرین‌ها

  1. کد را با خرد کردن گرادیان‌ها به ZeRO-2 ارتقا دهید: هر رتبه فقط باید گرادیان مربوط به قطعه‌ی خودش را ذخیره کند؛ این کار با صفر کردن بخش‌های غیرمربوط بعد از مرحله‌ی برگشت انجام می‌شود.
  2. یک پروفایلر حافظه اضافه کنید که مصرف واقعی بایت‌های fp32 را روی رتبه‌ی 0 چاپ کرده و با مقدار پیش‌بینی‌شده در فرمول مقایسه کند.
  3. زمان اجرای واقعی (wall-clock time) در هر گام را بین DDP معمولی و ZeRO-1 اندازه بگیرید و آن را به بخش‌های رفت، برگشت و ارتباطات شبکه تفکیک کنید.
  4. قابلیت برش گرادیان (Gradient clipping) را تحت ZeRO-1 پیاده‌سازی کنید: نُرم L2 باید روی همه‌ی قطعه‌ها از طریق allreduce گرفتن از توان دوم نرم محلی محاسبه شود.
  5. یک «ZeRO ساده‌لوحانه» (naive ZeRO) با allreduce به‌جای reduce_scatter پیاده کنید و تفاوت زمان تبادل شبکه را بسنجید. با تکیه بر اعداد و ارقام از انتخاب reduce_scatter دفاع کنید.

اصطلاحات کلیدی

اصطلاح در گفتار عامیانه معنای دقیق فنی
ZeRO-1 «خرد کردن بهینه‌ساز» هر رتبه 1/N از نسخه‌ی اصلی fp32 و گشتاورهای Adam را نگه می‌دارد
ZeRO-2 «خرد کردن گرادیان‌ها هم اضافه شد» هر رتبه پس از reduce_scatter، گرادیان‌های غیر از قطعه‌ی خود را نیز حذف می‌کند
ZeRO-3 «خرد کردن پارامترها» هر رتبه 1/N از پارامترهای fp16 را نگه می‌دارد؛ اجرای allgather لایه‌به‌لایه در رفت
Master copy «وزن‌های fp32» نسخه‌ی با دقت بالای پارامترها که بهینه‌ساز آن را به‌روزرسانی می‌کند
Reduce_scatter «تقسیم مجموع» ارسال گرادیان‌های جمع‌شده‌ی مربوط به هر قطعه صرفاً به همان رتبه‌ی مالک

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسچرا ZeRO ابتدا وضعیت بهینه‌ساز را به جای پارامترها شارد می‌کند؟
  2. پیش از درسالگوی ارتباطات شبکه‌ای در هر گام از ZeRO stage 1 چیست؟
  3. حین درسچرا پهنای باند مصرفی در هر گام مشابه DDP است؟
  4. حین درسبرای یک مدل 7B با بهینه‌ساز Adam در دقت ترکیبی روی 8 رتبه، کاهش حافظه نسبت به DDP معمولی چقدر است؟
  5. حین درسویژگی افزوده ZeRO-2 نسبت به ZeRO-1 چیست؟
  6. پس از درسچرا ZeRO نیازمند این است که چک‌پوینت وضعیت بهینه‌ساز ثبت کند کدام رتبه مالک کدام شارد است؟

سؤالات متداول

چرا ZeRO Stage 1 ترافیک شبکه را نسبت به DDP افزایش نمی‌دهد؟

چون در ZeRO-1 یک عملیات reduce_scatter و یک allgather انجام می‌شود که حجم داده جابه‌جاشده در آن‌ها از نظر پهنای باند دقیقاً معادل یک عملیات allreduce در DDP استاندارد است.

چه تفاوتی میان ZeRO-1 و ZeRO-2 وجود دارد؟

در ZeRO-1 تنها وضعیت بهینه‌ساز (نسخه اصلی fp32 و گشتاورهای Adam) خرد می‌شود، اما در ZeRO-2 علاوه‌بر بهینه‌ساز، گرادیان‌ها نیز بین رتبه‌ها تقسیم شده و حافظه آزاد می‌شود.

در آموزش یک مدل 7B با دقت ترکیبی، ZeRO-1 چقدر حافظه آزاد می‌کند؟

برای 8 رتبه (N=8)، مصرف حافظه از 16P بایت به 5.5P بایت می‌رسد که کاهشی حدود 65 درصدی در تخصیص حافظه کل بهینه‌ساز به ازای هر کارت ایجاد می‌کند.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.