فاز 19 · درس 80

چک‌پوینت تکه‌تکه‌شده و ادامه‌ی اتمیک در آموزش توزیع‌شده

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

در آموزش مدل‌های زبانی حجیم روی چندین کارت گرافیک، وقوع خطا و متوقف شدن کار بخشی اجتناب‌ناپذیر از چرخه‌ی آموزش است. اگر از روش‌های سنتی برای ذخیره‌ی چک‌پوینت (Checkpoint) استفاده کنید، تمام پارامترها و وضعیت بهینه‌ساز باید روی یک گره متمرکز شوند و سپس روی دیسک قرار بگیرند. این کار علاوه‌بر هدر دادن ساعت‌ها از زمان باارزش محاسباتی، کل کلاستر را معطل درگاه شبکه‌ی یک کارت گرافیک نگه می‌دارد.

راهکار استاندارد برای پشت سر گذاشتن این گلوگاه، استفاده از چک‌پوینت‌های تکه‌تکه‌شده (Sharded Checkpoints) و الگوی نوشتن اتمیک (Atomic Write) است. با این شیوه، هر کارت یا رتبه (Rank) شارد داده‌های خود را به‌صورت کاملاً موازی در دیسک می‌نویسد و یک فایل مانیفست (Manifest) وظیفه‌ی ردیابی و مدیریت مالکیت این داده‌ها را بر عهده می‌گیرد.

در این درس یاد می‌گیرید چطور سیستم چک‌پوینت توزیع‌شده را از صفر بسازید، با نوشتن اتمیک در فایل‌های موقت جلوی خراب شدن داده‌ها در اثر کرش‌های ناگهانی را بگیرید و کدهایی مقاوم در برابر خطاهای رایج مثل تغییر تعداد گره‌ها یا ناقص ماندن فایل‌ها پیاده کنید.

در فرایند آموزش یک مدل 70 میلیارد پارامتری (70B)، خرابی گره‌ها هر چند ساعت یک‌بار کار را متوقف می‌کند. فرمت چک‌پوینت (checkpoint) تعیین می‌کند که 30 دقیقه از کار را از دست می‌دهید یا 30 ساعت را. چک‌پوینت تکه‌تکه‌شده (sharded checkpoint)، شارد مربوط به هر رتبه (rank) را به‌صورت موازی می‌نویسد و مالکیت هر بخش را در یک مانیفست (manifest) ثبت می‌کند. هنگام ادامه‌ی کار (resume)، شاردِ هر رتبه از فایل اختصاصی خودش بارگذاری می‌شود، وضعیت را روی همان تعداد فرایند (world size) بازسازی می‌کند و بهینه‌ساز طوری گام برمی‌دارد که انگار هیچ اتفاقی نیفتاده است. نوشتن اتمیک (atomic write) مانع از آن می‌شود که یک چک‌پوینت نیمه‌کاره اجرای بعدی را خراب کند.

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

  • ذخیره‌ی یک چک‌پوینت چندرتبه‌ای در قالب فایل‌های شارد به‌ازای هر رتبه، به‌همراه مانیفستی که مشخص می‌کند کدام رتبه مالک کدام بخش است.
  • استفاده از الگوی نوشتن اتمیک (نوشتن در یک مسیر موقت و سپس تغییر نام آن) تا قطعی یا کرش در میانه‌ی نوشتن، هرگز چک‌پوینت نیمه‌کاره تولید نکند.
  • ادامه‌ی کار از روی مانیفست و اعتبارسنجی انطباق بایت‌به‌بایت داده‌ها، هم برای پارامترهای fp16 و هم برای وضعیت بهینه‌ساز ZeRO در تک‌تک رتبه‌ها.
  • محافظت از اسکیما (schema)ی مانیفست در برابر سه حالت شکست (failure modes): تغییر world size، عدم تطابق تعداد شاردها، و نوشتن ناقص.

صورت مسئله

یک چک‌پوینت معمولی، همه‌ی پارامترها و وضعیت بهینه‌ساز را در رتبه‌ی 0 جمع‌آوری (gather) می‌کند و سپس همه‌چیز را در یک تک‌فایل می‌نویسد. برای یک مدل 70B، این یعنی عبور 1.1 ترابایت داده‌ی وضعیت از درگاه شبکه‌ی یک رتبه‌ی واحد. این نوشتن، بقیه‌ی رتبه‌ها را معطل می‌کند؛ چون باید منتظر پایان مرحله‌ی جمع‌آوری بمانند و بیکار می‌نشینند. در این حالت، پهنای باند ورودی/خروجی (IO) برابر با کندترین اتصال شبکه‌ی یک GPU است، نه مجموع پهنای باند کل کلاستر. روی یک کلاستر واقعی، مرحله‌ی «جمع‌آوری و سپس نوشتن» می‌تواند بیش از کل یک ساعت آموزش قبلی طول بکشد؛ این یعنی خروجی کل کار به کمتر از یک چک‌پوینت در هر روز کاری کاهش می‌یابد.

چک‌پوینت‌های تکه‌تکه‌شده این الگو را دگرگون می‌کنند: هر رتبه شارد خود را به‌صورت موازی در فایل اختصاصی خودش می‌نویسد. مانیفست ثبت می‌کند که کدام رتبه مالک کدام شارد بوده است تا فرایند resume بتواند هر شارد را دقیقاً سر جای اولش برگرداند. مجموع پهنای باند نوشتن، همگام با مقیاس کلاستر افزایش می‌یابد. یک چک‌پوینت 1 ترابایتی که نوشتن آن از طریق یک رتبه 4 ساعت زمان می‌برد، با 64 رتبه تنها 4 دقیقه طول می‌کشد. علاوه‌بر این، مانیفست قراردادی روشن برای شناسایی موارد ناسازگار در زمان بازیابی ارائه می‌دهد: تغییر world size و موارد نوشتن ناقص قابل تشخیص هستند، و مسیر بارگذاری داده می‌تواند به‌جای استفاده‌ی خاموش از داده‌های نامعتبر، با توقف با صدای رسا (fail loud) خطای خود را اعلام کند.

مفاهیم کلیدی

وضعیت رتبه 0نوشتن rank0.bin.tmpوضعیت رتبه 1نوشتن rank1.bin.tmpوضعیت رتبه 2نوشتن rank2.bin.tmpوضعیت رتبه 3نوشتن rank3.bin.tmpنوشتن manifest.json.tmpتغییر نام تمام tmp.ها به نام‌های نهاییچک‌پوینت کامل شد

اسکیمای مانیفست

{
  "world_size": 4,
  "step": 1234,
  "wall_clock_seconds": 4521,
  "shards": [
    {"rank": 0, "path": "rank0.bin", "sha256": "...", "param_shard_offset": 0, "param_shard_numel": 65536},
    {"rank": 1, "path": "rank1.bin", "sha256": "...", "param_shard_offset": 65536, "param_shard_numel": 65536}
  ],
  "schema_version": 1
}

سه فیلد در این اسکیما نقش حیاتی دارند: world_size باعث می‌شود بازیابی روی تعداد متفاوتی از فرایندها به‌جای ایجاد خرابی خاموش، بلافاصله با توقف با صدای رسا روبه‌رو شود. فیلد sha256 به‌ازای هر شارد، موارد نوشتن ناقص یا خراب‌شده را تشخیص می‌دهد. فیلدهای param_shard_offset و param_shard_numel برای هر شارد به لودر اجازه می‌دهند تنسور (tensor) مسطح پارامترها را در موقعیت درست خود بازسازی کند.

نوشتن اتمیک

الگوی استاندارد به این صورت است: هر شارد را در فایلی با پسوند <name>.tmp می‌نویسید، مانیفست را در manifest.json.tmp ذخیره می‌کنید، روی تک‌تک آن‌ها fsync می‌زنید، نام فایل‌های شارد را تغییر می‌دهید و در آخرین مرحله نام فایل مانیفست را عوض می‌کنید. یک عملیات تغییر نام در استاندارد POSIX روی یک سیستم‌فایل یکسان، اتمیک است: یعنی یا فایل جدید به‌طور کامل وجود دارد یا فایل قبلی. مجموعه‌ای از تغییر نام‌ها یک تراکنش واحد به‌شمار نمی‌روند؛ بنابراین تغییر نام مانیفست همان نقطه‌ی قطعی‌شدن (commit) است و لودر هش همه‌ی شاردها را با مانیفست مقایسه و بررسی می‌کند. هر چک‌پوینت را داخل دایرکتوری گام اختصاصی خودش ذخیره کنید؛ در این صورت، اگر پیش از تغییر نام مانیفست کرشی رخ دهد، چک‌پوینت قبلی همچنان معتبر و فعال باقی می‌ماند. استفاده‌ی مجدد از یک دایرکتوری مشترک این تضمین را از بین می‌برد؛ چرا که تغییر نام شاردها پیش از رسیدن مانیفست جدید، شاردهای قدیمی را بازنویسی می‌کند. بدون نوشتن اتمیک، یک کرش ناگهانی می‌تواند شاردی ناقص به‌جا بگذارد در حالی که مانیفست موجود به آن اشاره می‌کند؛ در نتیجه، در زمان بازیابی، وضعیت بهینه‌ساز تخریب می‌شود.

سه حالت شکستی که اسکیما باید در برابرشان ایمن باشد

شکست نشانه راهکار دفاعی
تغییر World-size ادامه‌ی کار روی N=8 با مانیفست مربوط به N=4 عدم تطابق world_size در مانیفست و توقف با صدای رسا
عدم تطابق تعداد شاردها ادامه‌ی کار فایل‌های rank*.bin کمتری نسبت به شاردهای مانیفست می‌بیند پیمایش شاردها و اعتبارسنجی وجود تک‌تک آن‌ها
نوشتن ناقص بریده شدن فایل شارد در میانه‌ی عمل flush اعتبارسنجی sha256 هنگام بارگذاری

هر یک از این راهکارها مانع بارگذاری داده‌های نامعتبر در همان ابتدا می‌شوند؛ در غیر این صورت با خطای خاموشی روبه‌رو می‌شوید که شاید 100 گام بعد، وقتی تابع زیان (loss) مقدارش NaN شد، خودش را نشان دهد.

چرا فایل‌های مجزا به‌ازای هر رتبه، نه یک تک‌فایل بزرگ؟

نوشتن هم‌زمان در یک فایل با استفاده از O_APPEND در POSIX برای نوشتن‌های بایت‌محور کار می‌کند؛ اما در عمل، آفست‌های داخل یک شارد اسپن‌هایی (span) در مقیاس چند مگابایت را پوشش می‌دهند و قفل‌گذاری روی فایل سربار غالبی ایجاد می‌کند. فایل‌های مجزا برای هر رتبه هیچ رقابتی با هم ندارند و اگر سیستم‌فایل زیرساختی موازی باشد (مثل Lustre یا GPFS)، از تکنیک توزیع بلوک‌ها (striping) بیشترین بهره را می‌برند. به همین دلیل تمام استک‌های عملیاتی (DeepSpeed، FSDP و NeMo) از فایل‌های مجزا به‌ازای هر رتبه استفاده می‌کنند.

چک‌پوینت تکه‌تکه‌شده (Sharded)نوشتن موازی · تغییر نام اتمیک
SHARDED CHECKPOINTEvery rank streams its own shard to a temp file at the same time, so write bandwidth scales with the cluster instead of one gather bottleneck. Only after all shards land does the rename flip the temp files and the manifest live. Resume reads the manifest first, checks world size and hashes, and fails loudly on any mismatch.رتبه 0رتبه 1رتبه 2رتبه 3rank0.bin.tmprank1.bin.tmprank2.bin.tmprank3.bin.tmprank0.binrank1.binrank2.binrank3.binتغییر نام: تبدیل هم‌زمان همه تکه‌هامانیفستworld=4 · sha256
هر رتبه تکه‌ی خود را به‌طور هم‌زمان در یک فایل موقت می‌نویسد، بنابراین پهنای باند نوشتن همراه با کلاستر مقیاس‌پذیر می‌شود و گلوگاه تجمیع (gather) از بین می‌رود. تنها پس از ذخیره شدن تمام تکه‌ها، تغییر نام فایل‌های موقت و مانیفست را فعال می‌کند. فرایند بازیابی ابتدا مانیفست را می‌خواند، world size و هش‌ها را بررسی می‌کند و در صورت هرگونه مغایرت بلافاصله خطا می‌دهد.

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

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

  • دیتاکلاس ShardManifest با اسکیمای معرفی‌شده در بالا به‌همراه توابع to_json/from_json.
  • تابع save_sharded(state_dict_per_rank, dir, step) که وضعیت باینری هر رتبه را با استفاده از الگوی اتمیکِ «نوشتن موقت و سپس تغییر نام» در فایل اختصاصی خودش می‌نویسد و سپس مانیفست را ذخیره می‌کند.
  • تابع load_sharded(dir, expected_world_size) که مانیفست را می‌خواند، sha256 هر شارد را بررسی می‌کند و دیکشنری وضعیت پارامترها (state dict) را به‌ازای هر رتبه برمی‌گرداند.
  • یک آزمون رفت‌وبرگشت (round-trip): ساخت وضعیت به‌ازای هر رتبه، ذخیره، بارگذاری، و بررسی انطباق دقیق بایت‌به‌بایت.

اجرای کد:

python3 code/main.py

خروجی: 4 فایل شارد به‌همراه فایل مانیفست ذخیره می‌شوند و سپس با اعتبارسنجی انطباق بایت‌به‌بایت دوباره بارگذاری می‌گردند.

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

سه الگو سطح پایداری چک‌پوینت را برای استفاده در محیط عملیاتی (پروداکشن) تضمین می‌کنند.

نوشتن غیرهم‌زمان (Async write). استک‌های عملیاتی نوشتن چک‌پوینت را روی ترد (thread) یا فرایند (process) مجزایی ارسال می‌کنند تا آموزش بدون وقفه ادامه یابد. مانع هماهنگی (barrier) در چک‌پوینت بعدی قرار دارد: تا زمانی که ذخیره‌ی قبلی کامل نشده باشد، نباید ذخیره‌ی بعدی شروع شود. فلگ async_io در DeepSpeed دقیقاً همین کار را انجام می‌دهد. در این درس نوشتن را هم‌زمان نگه داشتیم تا مراحل به‌وضوح دیده شوند.

اول دیسک محلی سریع، سپس آپلود غیرهم‌زمان. ابتدا روی NVMe محلی (بسیار سریع) بنویسید و سپس به‌شکل غیرهم‌زمان داده‌ها را روی S3 یا GCS آپلود کنید. این الگوی دومرحله‌ای، سرعت چک‌پوینت‌گیری را روی کلاستر برای ادامه‌ی سریع کار بالا نگه می‌دارد و هم‌زمان نسخه‌ای پایدار را نیز خارج از کلاستر آرشیو می‌کند. مانیفست اصلی مسیر محلی را نگه می‌دارد؛ مانیفست آپلود مسیر مخزن راه‌دور (remote) را ثبت می‌کند.

چرخش چک‌پوینت‌ها (Rotation). اجراهای عملیاتی معمولاً آخرین K چک‌پوینت (معمولاً 3 تا 5 مورد) را نگه می‌دارند و قدیمی‌ترین‌ها را پاک می‌کنند. بدون چرخش، دیسک در میانه‌ی فرایند پر می‌شود و چک‌پوینت بعدی شکست می‌خورد. با چرخش، ذخیره‌سازی بعدی ابتدا قدیمی‌ترین فایل را حذف می‌کند و فضای دیسک را آزاد می‌سازد.

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

الگوهای موجود در محیط‌های عملیاتی:

  • چک‌پوینت‌گیری در DeepSpeed. دستور deepspeed.save_checkpoint(tag=step) فایل‌های هر رتبه و یک فایل latest را ذخیره می‌کند که به تگ فعال اشاره دارد.
  • چک‌پوینت‌گیری با FSDP در PyTorch. ماژول torch.distributed.checkpoint وضعیت شاردشده را به‌کمک یک Planner ذخیره می‌کند؛ این پلنر چیدمان داده‌ها را برای هر رتبه تعیین می‌کند.
  • کتابخانه‌ی NeMo. ابزارهای DeepSpeed و FSDP را در رابط کاربری یکپارچه‌ی save_to_checkpoint قرار می‌دهد و متادیتای موردنیاز را اضافه می‌کند.

خروجی این درس

در درس 81، یک چک‌پوینت شاردشده از اجرای سرتاسری DDP+ZeRO ذخیره می‌کنیم و آن را روی همان world size دوباره بارگذاری می‌کنیم تا مطمئن شویم قرارداد بازیابی (resume) بدون نقص کار می‌کند.

تمرین‌ها

  1. نوشتن غیرهم‌زمان را اضافه کنید: ذخیره‌سازی را در یک ترد مجزا آغاز کنید و بگذارید آموزش ادامه یابد. چک‌پوینت بعدی را تا زمان اتمام چک‌پوینت قبلی متوقف نگه دارید.
  2. قابلیت چرخش last_5_steps را اضافه کنید: 5 چک‌پوینت آخر را نگه دارید و پیش از نوشتن چک‌پوینت جدید، قدیمی‌ترین را پاک کنید.
  3. یک مسیر بررسی سریع با CRC اضافه کنید تا بازخوانی‌های داخل حلقه سریع‌تر انجام شوند (جایی که در فرایند چرخش، بدون نیاز به محاسبه‌ی کامل sha256، یک چک‌پوینت به‌عنوان چک‌پوینت فعالِ جدید تعیین می‌شود).
  4. امکان بارگذاری در ابعاد متفاوت (cross-world-size) را پیاده کنید: تعادل مجدد شاردها از N=4 به N=8 را با خواندن مانیفست، ادغام و سپس شاردبندی مجدد انجام دهید.
  5. قابلیت آپلود به یک شبیه‌ساز S3 (یک دایرکتوری دوم) را اضافه کنید و مانیفست آپلود را بنویسید. از منطق سیاست ذخیره‌سازی دومرحله‌ای دفاع کنید.

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

اصطلاح در گفت‌وگوها در عمل یعنی چه؟
Sharded checkpoint (چک‌پوینت تکه‌تکه‌شده) «ذخیره‌سازی به‌ازای هر رتبه» هر رتبه فایل شارد خود را به‌صورت موازی می‌نویسد
Manifest (مانیفست) «ایندکس» یک فایل JSON که مسیر شاردها، آفست‌ها و sha256 را ذخیره می‌کند
Atomic write (نوشتن اتمیک) «نوشتن در فایل tmp و تغییر نام» نوشتن در فایلی با پسوند tmp. و سپس تغییر نام از طریق POSIX؛ تا در صورت بروز کرش، فایل قبلی سالم بماند
Partial write (نوشتن ناقص) «شارد ناقص یا بریده‌شده» کرش در حین نوشتن فایلی معیوب می‌سازد؛ مقدار sha256 این مشکل را پیدا می‌کند
Rotation (چرخش چک‌پوینت‌ها) «نگه‌داشتن K مورد آخر» پاک کردن قدیمی‌ترین چک‌پوینت قبل از ذخیره‌ی فایل جدید برای مدیریت فضای دیسک

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

خودآزمایی

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

  1. پیش از درسچرا الگوی «تجمیع و سپس نوشتن» (gather-then-write) در مقیاس بالا رویکرد اشتباهی برای ذخیره چک‌پوینت است؟
  2. پیش از درسمانیفست چه چیزی را ثبت می‌کند که فایل‌های تکه‌ها (shard) نمی‌توانند؟
  3. حین درسچرا نوشتن ابتدا در name>.tmp> و سپس تغییر نام، بهتر از نوشتن مستقیم است؟
  4. حین درسمحاسبه‌ی sha256 به ازای هر تکه در برابر چه چیزی محافظت می‌کند؟
  5. حین درسچرا مسیر بارگذاری در صورت عدم تطابق world_size بلافاصله با خطا متوقف می‌شود؟
  6. پس از درسچرا در محیط عملیاتی (پروداکشن) چرخش چک‌پوینت‌ها (نگه‌داشتن K مورد آخر) انجام می‌شود؟

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

چرا ذخیره‌ی چک‌پوینت در فایل‌های مجزا به‌ازای هر رتبه بهتر از یک فایل مشترک است؟

نوشتن هم‌زمان تمام رتبه‌ها در یک فایل به دلیل رقابت روی قفل‌های سیستم‌فایل و سربار سنگین آفست‌ها گلوگاه ایجاد می‌کند؛ اما فایل‌های مجزا بدون هیچ رقابتی به‌طور موازی نوشته می‌شوند و از حداکثر پهنای باند دیسک‌های توزیع‌شده بهره می‌برند.

الگوی نوشتن اتمیک (Atomic Write) چگونه از خراب شدن چک‌پوینت جلوگیری می‌کند؟

در این روش، فایل‌ها ابتدا با پسوند موقت (مانند tmp.) ذخیره و همگام‌سازی (fsync) می‌شوند و در آخرین مرحله نام آن‌ها تغییر می‌کند. چون تغییر نام در POSIX یک عملیات اتمیک است، در صورت قطعی یا کرش ناگهانی هرگز فایل ناقص جایگزین چک‌پوینت سالم نمی‌شود.

نقش فیلد sha256 و world_size در فایل مانیفست چیست؟

فیلد world_size جلوی بازیابی اشتباه روی تعداد کارت‌های نامتناسب را می‌گیرد و sha256 سلامت تک‌تک شاردها را بررسی می‌کند تا در صورت ناقص یا دستکاری شدن داده‌ها، فرایند آموزش پیش از ایجاد خطای خاموش متوقف شود.

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

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