فاز 19 · درس 77

آموزش پیاده‌سازی DDP از صفر در PyTorch برای موازی‌سازی داده

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
پیش‌نیاز
فاز 19، مسیر C، درس‌های 42 تا 49
چندین چرخ‌دنده‌ی همسان که توسط کابل‌های متصل به‌صورت هم‌زمان و هماهنگ به گردش درمی‌آیند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. سه عملیات کلیدی مورد نیاز DDP
  5. چرا میانگین و نه مجموع؟
  6. چرا باکت‌بندی گرادیان‌ها؟
  7. اهمیت تثبیت سید (Seed)
  8. پیاده‌سازی از صفر
  9. الگوهای کاربردی در محیط عملیاتی
  10. استفاده از ابزارهای واقعی
  11. خروجی این درس
  12. تمرین‌ها
  13. اصطلاحات کلیدی
  14. برای مطالعه‌ی بیشتر

آموزش مدل‌های هوش مصنوعی بزرگ روی یک پردازنده‌ی تک معمولاً ناممکن یا بسیار زمان‌بر است. موازی‌سازی داده (Data Parallelism) اساسی‌ترین تکنیک برای توزیع محاسبات بین پردازنده‌ها به شمار می‌رود، اما بسیاری از توسعه‌دهندگان کتابخانه‌ی DistributedDataParallel (به‌اختصار DDP) را مثل یک جعبه‌ی سیاه در نظر می‌گیرند. درک دقیق سازوکار داخلی DDP مانع از افت کارایی و بروز خطاهای پیچیده در سیستم‌های توزیع‌شده می‌شود.

در این درس نشان می‌دهیم که DDP جادو نیست؛ بلکه لایه‌ای حدوداً 200 خطی روی عملیات جمعی allreduce است که با مقداردهی اولیه‌ی یکسان و میانگین‌گیری گرادیان‌ها در هر گام، وزن‌های مدل را در تمام فرآیندها همگام نگه می‌دارد. پیاده‌سازی کامل این ساختار روی CPU با بک‌اند gloo انجام می‌شود؛ بنابراین برای آزمودن و درک مفاهیم پایه‌ای آن هیچ نیازی به پردازنده‌های گرافیکی گران‌قیمت یا کلاسترهای پیچیده نخواهید داشت.

ماژول DistributedDataParallel صرفاً یک هوک (hook) روی عملیات allreduce است. کافی است مدل را درون آن بپیچید (wrap کنید)، پارامترهای اولیه را از رتبه (rank) 0 برودکست (broadcast) کنید تا همه‌ی رتبه‌ها با وزن‌های کاملاً یکسان کار را شروع کنند، روی تک‌تک پارامترها یک هوک پس‌انتشار (backward hook) بگذارید که دستور allreduce گرادیان را صادر کند؛ ادامه‌ی مسیر همان گرادیان کاهشی (gradient descent) استاندارد است. کل این الگو در 200 خط پیاده می‌شود.

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

  • پیاده‌سازی یک لایه‌ی پوششی مشابه DistributedDataParallel که پارامترهای اولیه را برودکست کند و بعد از پس‌انتشار، روی گرادیان‌ها allreduce بزند.
  • اجرای N رتبه‌ی پردازنده (CPU ranks) با تابع torch.multiprocessing.spawn روی بک‌اند gloo به کمک راندوو مبتنی بر فایل (file-based rendezvous).
  • اثبات درستی همگام‌سازی گرادیان‌ها از طریق آموزش ترتیبی همان مدل روی همان داده‌ها و نمایش برابری گام‌به‌گام پارامترها.
  • دفاع از دلایل استفاده از باکت‌بندی (تلفیق گرادیان‌ها یا gradient fusion) و همپوشانی (ارتباط هم‌زمان با پس‌انتشار) به‌عنوان دو تغییری که یک DDP ساده را به یک DDP آماده‌ی محیط عملیاتی (پروداکشن) تبدیل می‌کنند.

صورت مسئله

یک مدل 1 میلیارد پارامتری با 12 گیگابایت فعال‌سازی (activation) در حافظه‌ی یک پردازنده‌ی گرافیکی معمولی جا نمی‌شود. حتی اگر جا شود هم آموزش آن هفته‌ها طول می‌کشد. در موازی‌سازی داده (Data Parallelism)، دسته‌ی داده‌ها (batch) بین N رتبه تقسیم می‌شود، هر رتبه گذر رفت (forward) و برگشت (backward) را روی قطعه‌ی داده‌ی (shard) اختصاصی خود حساب می‌کند و در هر گام، گرادیان‌های همه‌ی رتبه‌ها با هم جمع می‌شوند تا هر N نسخه کاملاً یکسان بمانند. در نهایت بهینه‌ساز بر اساس همین گرادیانِ جمع‌شده گام برمی‌دارد.

اگر گرادیان‌ها همگام نشوند، این N نسخه تا همان گام 2 از هم فاصله می‌گیرند و واگرا می‌شوند. در این حالت دیگر با «یک مدل آموزش‌دیده روی داده‌های بیشتر» طرف نیستیم، بلکه N مدل مجزا داریم که برحسب تصادف وزن‌های اولیه‌شان یکی بوده است. از طرف دیگر، اگر همگام‌سازی گرادیان‌ها بد انجام شود (یک allreduce به ازای هر پارامتر، بدون همپوشانی و بدون باکت‌بندی)، شبکه گلوگاه سیستم می‌شود و پردازنده‌های گرافیکی بیکار می‌مانند تا داده از سیم‌های ارتباطی رد شود. هنر DDP این است که هزینه‌ی همگام‌سازی گرادیان‌ها را نسبت به محاسبات تقریباً به صفر برساند. نسخه‌ی استاندارد PyTorch DDP این کار را با باکت‌بندی گرادیان‌ها، همپوشانی allreduce با پس‌انتشار لایه‌ی بعدی و بهره‌گیری از NCCL روی بستر NVLink انجام می‌دهد. ما همه‌ی این سه مورد را روی CPU با استفاده از gloo پیاده می‌کنیم و دقیقاً همان مفاهیم را یاد می‌گیریم.

مفاهیم پایه

رتبه 3رتبه 2رتبه 1رتبه 0رتبه 3رتبه 2رتبه 1رتبه 0رفت و برگشت روی شارد خودoptimizer.step روی گرادیان‌های یکسانانتشار پارامترهاانتشار پارامترهاانتشار پارامترهاallreduce باکت گرادیانallreduce باکت گرادیانallreduce باکت گرادیانallreduce باکت گرادیان

سه عملیات کلیدی مورد نیاز DDP

مرحله عملیات جمعی (Collective) دلیل
مقداردهی اولیه برودکست از رتبه‌ی 0 همه‌ی رتبه‌ها کار را با پارامترهای کاملاً یکسان آغاز کنند
پس از پس‌انتشار allreduce روی تک‌تک گرادیان‌ها بهینه‌ساز باید گام خود را بر اساس میانگین گرادیان‌ها بردارد
گاهی اوقات برودکست بافرها آماره‌های جاری (running stats) در نرمال‌سازی دسته‌ای (Batchnorm) هماهنگ بمانند

چرا میانگین و نه مجموع؟

حاصل Allreduce-SUM تقسیم بر world_size، میانگین گرادیان را به ما می‌دهد. میانگین نسبت به world_size پایدار است؛ یعنی نرخ یادگیری‌ای (learning rate) که برای یک رتبه تنظیم کرده‌اید روی 4 رتبه هم کار می‌کند، چون اندازه‌ی گرادیان در هر گام تغییری نکرده است. اگر Allreduce-SUM را بدون تقسیم استفاده کنید، با هر بار تغییر اندازه‌ی کلاستر مجبورید نرخ یادگیری را دوباره از اول تنظیم کنید. ماژول DDP عملیات SUM را درون خود قرار داده و حاصل را تقسیم می‌کند؛ در این درس ما هم دقیقاً همین کار را انجام می‌دهیم.

چرا باکت‌بندی گرادیان‌ها؟

یک ترنسفورمر هزاران تنسور پارامتر دارد. اجرای یک allreduce به ازای هر تنسور، یعنی پرداخت هزاران‌باره‌ی تاخیر پایه‌ی gloo. ساختار DDP گرادیان‌ها را در دسته‌های حدوداً 25 مگابایتی (باکت) گروه‌بندی می‌کند و برای هر باکت یک allreduce صادر می‌کند. در این روش حجم کل داده‌ی ارسالی روی شبکه تغییری نمی‌کند، اما تاخیر ارتباطی سرشکن می‌شود. برای مدل کوچک این درس، همه‌ی پارامترها را در یک باکت قرار می‌دهیم؛ آنچه اهمیت دارد و منتقل می‌شود درک این ساختار است.

اهمیت تثبیت سید (Seed)

هر رتبه برای بُر زدن داده‌ها (shuffling) باید torch.manual_seed(seed + rank) را صدا بزند، اما برای مقداردهی اولیه‌ی پارامترها باید از torch.manual_seed(seed) استفاده کند. اگر برای بُر زدن از یک سید مشترک استفاده کنید، همه‌ی رتبه‌ها دسته‌ی داده‌ها را با یک ترتیب مشابه می‌بینند (که نقض غرض موازی‌سازی داده است). برای پارامترها، برودکست اولیه‌ی رتبه‌ی 0 در زمان ساخت ماژول همان چیزی است که نسخه‌ها را کاملاً همسان می‌کند و مقداردهی اولیه‌ی هر رتبه را رونویسی (overwrite) خواهد کرد. یکسان بودن سید مقداردهی اولیه صرفاً یک پشتیبان است: اگر برودکست اجرا نشود یا مشکلی پیدا کند، مقداردهی‌های اختصاصی هر رتبه باعث می‌شوند وزن‌های اولیه تفاوت داشته باشند و همگام‌سازی گرادیان به‌تنهایی هرگز نمی‌تواند آن‌ها را دوباره به هم برساند. این الگوی سید را درست پیاده کنید، وگرنه آزمون برابری پارامترها در همان گام 1 شکست می‌خورد.

همگام‌سازی گرادیان در DDPhook · باکت · همپوشانی
DDP GRADIENT SYNCA backward hook on every parameter drops its gradient into a bucket. The moment bucket 1 fills, its allreduce launches on the wire while layers 2 and 1 are still backpropagating, so communication hides behind compute. One broadcast at construction made all replicas identical; the summed gradients keep them identical every step.گام رو به عقبمسیر جریان گرادیان‌هالایه 1لایه 2لایه 3لایه 4باکت 1باکت 0ارتباط allreduce، حین اجرای گام پس‌انتشار اجرا می‌شود
یک backward hook روی هر پارامتر، گرادیان آن را درون یک باکت می‌ریزد. به محض پر شدن باکت 1، عملیات allreduce آن روی شبکه آغاز می‌شود در حالی که لایه‌های 2 و 1 هنوز در حال پس‌انتشار هستند؛ بنابراین ارتباطات در پشت محاسبات پنهان می‌شود. یک بار broadcast در زمان ساخت، همه کپی‌ها را یکسان کرد؛ گرادیان‌های جمع‌شده نیز آن‌ها را در هر گام یکسان نگه می‌دارند.

پیاده‌سازی از صفر

فایل code/main.py این بخش‌ها را پیاده کرده است:

  • MiniMLP: یک شبکه‌ی پرسپترون چندلایه (MLP) سه‌لایه، آن‌قدر کوچک که در چند ثانیه همگرا شود و آن‌قدر کامل که ساختار کار را شفاف نشان دهد.
  • DistributedDataParallel(model, world_size): در زمان ساخت، پارامترها را برودکست می‌کند و لایه‌ی پوششی‌ای برمی‌گرداند که متد sync_grads آن گرادیان‌های جمع‌شده از طریق allreduce را بر world_size تقسیم می‌کند.
  • worker(rank, world_size, ...): حلقه‌ی کامل آموزش شامل مقداردهی اولیه‌ی torch.distributed روی gloo، گذر رفت، برگشت، همگام‌سازی و اعمال گام بهینه‌ساز.
  • _reference_single_process_loop(...): همین مدل را به‌صورت ترتیبی روی همین داده‌ها و در یک فرایند آموزش می‌دهد؛ این متد در تست‌ها استفاده می‌شود تا برابری بایتی پارامترها بعد از هر گام تایید شود.

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

python3 code/main.py

خروجی: جدولی از روند آموزش در هر گام که تابع زیان (loss) و چک‌سام پارامترهای اجرای تک‌فرایندی را با اجرای DDP روی 4 رتبه مقایسه می‌کند. هر دو مسیر منحنی‌های زیان کاملاً یکسانی (تا دقت اعشار float epsilon) تولید می‌کنند که نشان می‌دهد همگام‌سازی گرادیان کاملاً درست است.

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

سه الگو برای این‌که DDP به سطحی از پایداری برسد که بتوان آن را به محیط عملیاتی فرستاد:

شناسایی پارامترهای بلااستفاده (Find unused parameters): در برخی شبکه‌ها، گذر رفت بسته به شرایط از روی برخی پارامترها عبور می‌کند (مانند خروج زودهنگام یا مسیریاب در مدل‌های مخلوطی از خبرگان / MoE). پارامترهای ردشده فاقد گرادیان هستند، اما هوک آماده‌سازی باکت در DDP همچنان منتظر گرادیان آن‌ها می‌ماند و کل allreduce دچار بن‌بست (deadlock) می‌شود. فعال کردن find_unused_parameters=True به DDP می‌گوید پیش از اجرای reduce، بررسی کند که کدام پارامترها واقعاً گرادیان گرفته‌اند. هزینه‌ی این کار پیمایش گراف در هر گام است؛ بنابراین تا زمانی که گذر رفت شاخه‌بندی شرطی (branch) ندارد، آن را خاموش بگذارید.

بهینه‌سازی گراف ایستا (Static graph optimisation): اگر ساختار گذر رفت در تمام گام‌ها ثابت بماند، با فعال کردن static_graph=True به DDP اجازه می‌دهید زمان‌بندی باکت‌ها را از پیش محاسبه کند. این بهینه‌سازی در مقیاس‌های بزرگ اهمیت زیادی دارد؛ ذخیره‌ی چند میلی‌ثانیه‌ای که با پیش‌محاسبه در هر گام به دست می‌آید، در طول 10000 گام تاثیر چشمگیری خواهد داشت.

دقت در انباشت گرادیان (Gradient Accumulation): انباشت گرادیان روی K ریزدسته (microbatch) بدون همگام‌سازی لحظه‌ای در هر ریزدسته، نرخ پردازش را تا 10 برابر بیشتر می‌کند. ماژول DDP یک مدیریت‌کننده‌ی زمینه (context manager) به نام no_sync() در اختیارتان می‌گذارد که همگام‌سازی allreduce بعد از پس‌انتشار را متوقف می‌کند. اگر استفاده از این زمینه را فراموش کنید، K بار allreduce بیهوده اجرا خواهید کرد و نرخ پردازش به کف می‌رسد.

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

الگوهای محیط عملیاتی (پروداکشن):

  • PyTorch DDP: پیاده‌سازی مرجع و استاندارد. ماژول torch.nn.parallel.DistributedDataParallel(model) امکانات باکت‌بندی، همپوشانی و زمینه‌ی no_sync را به‌صورت آماده پیاده‌سازی کرده است.
  • HuggingFace Accelerate: یک لانچر (launcher) اضافه می‌کند که متغیرهای محیطی torchrun و بسته‌بندی مدل را مدیریت می‌کند؛ اما در لایه‌های زیرین از همان DDP استفاده می‌کند.
  • موازی‌سازی داده در Megatron-LM: برای مدل‌های بزرگ، DDP را با موازی‌سازی تنسور (tensor parallel) ترکیب می‌کند؛ البته بخش موازی‌سازی داده‌ی آن از همان الگوی allreduce-after-backward پیروی می‌کند.

خروجی این درس

در درس 78 (خرد کردن به سبک ZeRO)، عملیات allreduce به ازای هر پارامتر را با reduce_scatter جایگزین می‌کنیم تا هر رتبه تنها قطعه‌ی اختصاصی خود از وضعیت بهینه‌ساز را ذخیره کند. در درس 81 نیز DDP را با ZeRO ترکیب می‌کنیم تا دمو را به‌صورت سرتاسری بسازیم.

تمرین‌ها

  1. قابلیت ساخت باکت‌های گرادیان با اندازه‌ی قابل‌تنظیم را اضافه کرده و میزان بهبود سرعت را در مقایسه با روش «یک allreduce به ازای هر پارامتر» روی مدلی عمیق‌تر بسنجید.
  2. سازوکار no_sync() را به‌صورت یک context manager پیاده کنید و مطمئن شوید حاصل انباشت گرادیان روی K ریزدسته، با خط مبنای تک‌فرایندی مطابقت دارد.
  3. حالتی برای find_unused_parameters تعریف کنید که در آن گذر رفت گاهی از یکی از لایه‌های MLP عبور کند؛ بررسی کنید که بدون این فلگ، سیستم حتماً دچار بن‌بست شود.
  4. gloo را بردارید و همگام‌سازی را فقط با torch.distributed.barrier() انجام دهید تا تفاوت کارایی همگام‌سازی مبتنی بر allreduce با روش مبتنی بر barrier را احساس کنید.
  5. سهم تاخیر همگام‌سازی گرادیان را از کل زمان یک گام برای اندازه‌های دسته‌ی 1، 16 و 256 اندازه بگیرید و چگونگی مقیاس‌پذیری آن را توضیح دهید.

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

اصطلاح در گفت‌وگوهای عامیانه معنای دقیق فنی
DDP «موازی‌سازی داده» لایه‌ای روی مدل که پارامترها را برودکست و گرادیان‌ها را در هر گام allreduce می‌کند
باکت (Bucket) «ادغام گرادیان‌ها» گروه‌بندی N عملیات کوچک allreduce در یک عملیات بزرگ
همپوشانی (Overlap) «پنهان کردن بار ارتباطی» صدور دستور allreduce هم‌زمان با محاسبات پس‌انتشار لایه‌های بعدی
no_sync «انباشت» نادیده گرفتن allreduce پس از پس‌انتشار برای انباشت گرادیان
find_unused «گذر رفت شرطی» شناسایی پارامترهای بدون گرادیان پیش از فراخوانی عملیات reduce

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

خودآزمایی

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

  1. پیش از درسچرا DDP پارامترها را در زمان ساخت broadcast می‌کند به جای اینکه به seed یکسان در رتبه‌ها متکی باشد؟
  2. پیش از درسالگوریتم DDP پس از backward و پیش از optimizer.step چه کاری انجام می‌دهد؟
  3. حین درسچرا به جای استفاده از allreduce-MEAN، بعد از allreduce-SUM بر world_size تقسیم می‌کنیم؟
  4. حین درسباکت‌بندی گرادیان در محیط عملیاتی DDP چه دستاوردی دارد؟
  5. حین درسچرا تابع no_sync() در PyTorch DDP وجود دارد؟
  6. پس از درسچرا پیاده‌سازی مرجع این درس میکروبچ هر رتبه را به صورت متوالی در یک فرایند پیمایش می‌کند؟

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

چرا در DDP به جای مجموع گرادیان‌ها از میانگین استفاده می‌شود؟

چون با محاسبه‌ی میانگین، مقدار گرادیان نسبت به تعداد رتبه‌ها (world_size) تغییر نمی‌کند و نیازی نیست با تغییر ابعاد کلاستر، نرخ یادگیری دوباره تنظیم شود.

هدف از باکت‌بندی (Bucketing) گرادیان‌ها در DDP چیست؟

ترنسفورمرها هزاران تنسور دارند؛ تجمیع گرادیان‌ها در باکت‌های حدوداً 25 مگابایتی مانع از تکرار تاخیر ارتباطی شبکه (latency) به ازای تک‌تک پارامترها می‌شود.

کاربرد فلگ find_unused_parameters چیست و چه هزینه‌ای دارد؟

در شبکه‌های با شاخه‌بندی شرطی مانع از بن‌بست (deadlock) در انتظار گرادیانِ پارامترهای ردشده می‌شود، اما در ازای هر گام هزینه‌ی پیمایش گراف محاسباتی را تحمیل می‌کند.

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

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