پروفایلینگ TorchNeuron؛ پیدا کردن گلوگاه استنتاج روی Trainium

با torch.profiler و Neuron Explorer می‌توانید ببینید هر عملگر چقدر از زمان دستگاه Trainium را می‌بلعد و چرا ادغام عملیات این هزینه را تا یک‌ششم کم می‌کند.

پروفایلینگ TorchNeuron؛ پیدا کردن گلوگاه استنتاج روی Trainium

پروفایلینگ TorchNeuron روی AWS Trainium نشان می‌دهد چرا یک مدل کند است و زمان دقیقاً کجا خرج می‌شود. در این راهنما، با یک عملیات کوچک به نام RMSNorm جلو می‌رویم و سه پیاده‌سازی متفاوت آن را با ابزارهای پروفایلینگ پایتورچ و Neuron Explorer مقایسه می‌کنیم.

خیلی از متخصصان یادگیری ماشین حس خوبی دارند که چطور یک مدل را سریع اجرا کنند، اما کم‌تر می‌دانند چطور تحلیل کنند که چرا مدل کند است. این نوشته از صفر شروع می‌شود و هیچ تجربهٔ قبلی در پروفایلینگ نمی‌خواهد. تراشهٔ مورد استفاده AWS Trainium است؛ تراشه‌ای که مخصوص بارهای آموزش و استنتاج یادگیری عمیق ساخته شده و بک‌اند متن‌باز بومی پایتورچ آن TorchNeuron نام دارد.

TorchNeuron از جریان‌های استاندارد پایتورچ روی Trainium پشتیبانی می‌کند: اجرای eager، مسیر torch.compile و APIهای معمول پایتورچ، با کم‌ترین تغییر در کد. همچنین به رابط هستهٔ Neuron (NKI) وصل می‌شود تا اپراتورهای سفارشی و بهینه‌سازی عملکرد ممکن شود. پیش از این هم نوشته‌ای دربارهٔ اجرای مدل‌های هوگینگ‌فیس روی AWS Trainium با TorchNeuron منتشر کرده‌ایم.

بک‌اند Neuron چطور داخل پایتورچ می‌نشیند؟

پیش از پروفایل کردن، بهتر است بدانیم Neuron چگونه با پایتورچ یکپارچه می‌شود. بک‌اند Neuron از نقطهٔ توسعهٔ PrivateUse1 در پایتورچ استفاده می‌کند. این سازوکار اجازه می‌دهد یک شتاب‌دهنده بدون افزودن کلید ارسال اختصاصی (مثل cuda) با فریم‌ورک یکپارچه شود. بستهٔ torch_neuronx همراه با پایتورچ بالادستی، Neuron را به‌عنوان یک بک‌اند بومی برای کد پایتورچ ثبت می‌کند.

راه‌اندازی پروفایلر

برای پروفایل کردن یک بار کاری روی Neuron به دو شیء نیاز دارید:

  • NeuronConfig: تعیین می‌کند کدام جریان داده و کدام NeuronCoreها ثبت شوند و خروجی کجا نوشته شود. این شیء به‌عنوان experimental_config به torch.profiler.profile پاس داده می‌شود.
  • NeuronProfiler: یک NeuronConfig می‌گیرد و تابع بازگشتی export_trace را فراهم می‌کند. این تابع رد فریم‌ورک را در فایل trace.json و در کنار ردهای سیستمی Neuron می‌نویسد.

پارامتر modes جریان‌های داده‌ای مثل دستورهای دستگاه و ردهای رانتایم را انتخاب می‌کند. پارامتر capture_enabled_for_nc مشخص می‌کند کدام NeuronCoreها ردیابی شوند. اگر correlate_device_time=True باشد، زمان دستگاه از رد به جدول key_averages() منتقل می‌شود و می‌توانید زمان CPU و زمان دستگاه Neuron را کنار هم بسنجید.

دو نکتهٔ فنی که بدون آن‌ها نتیجه ناقص می‌شود: در فهرست فعالیت‌ها باید ProfilerActivity.PrivateUse1 را اضافه کنید تا فعالیت دستگاه Neuron جمع‌آوری شود، و درون حلقهٔ پروفایل torch.neuron.synchronize() را صدا بزنید. بدون این همگام‌سازی، ممکن است پروفایلر پیش از پایان کار دستگاه خروجی بگیرد و ستون زمان Neuron خالی بماند. زمان‌بندی نمونه در این راهنما با wait=2، warmup=1 و active=1 در چهار فراخوانی prof.step() تنظیم شده تا یک تکرار گرم ثبت شود و سرباز اولیهٔ راه‌اندازی در نتیجه نیاید.

جدول key_averages را چطور بخوانیم؟

بعد از پایان پروفایلینگ، جدول key_averages() را بررسی کنید. اگر بر اساس self_privateuse1_time_total مرتب کنید، عملگرهایی که بیشترین هزینهٔ دستگاه را دارند بالای جدول می‌آیند. خروجی کامل این جدول رویدادهای سمت CPU مثل تخصیص حافظه و انتقال از میزبان به دستگاه را هم شامل می‌شود؛ بنابراین برای تمرکز روی محاسبه، فهرست را به عملگرهای RMSNorm محدود می‌کنیم.

نمونهٔ اول: RMSNorm با عملیات خام پایتورچ

در این پیاده‌سازی، هر خط تابع به یک یا چند عملگر ATen نگاشت می‌شود و می‌تواند تنسور میانی بسازد: مربع گرفتن، میانگین روی آخرین بُعد، وارون جذر میانگین، و در آخر ضرب در وزن.

نامتعداد فراخوانیزمان CPUزمان دستگاه Neuronسهم از زمان دستگاه
aten::mul360.9 میکروثانیه1,164.5 میکروثانیه49.5%
aten::add126.6 میکروثانیه434.6 میکروثانیه18.5%
aten::rsqrt115.6 میکروثانیه415.0 میکروثانیه17.6%
aten::mean121.1 میکروثانیه340.8 میکروثانیه14.5%

مجموع زمان دستگاه در این حالت 2.355 میلی‌ثانیه است. هر ردیف مستقیماً به بخشی از تابع نگاشت می‌شود: aten::mul برای x * x، ضرب در rms و ضرب نهایی در وزن اجرا می‌شود. aten::mean آخرین بُعد را کاهش می‌دهد. aten::add مقدار اپسیلون را اضافه می‌کند و چون این مقدار اسکالر از سمت CPU می‌آید، یک کپی از میزبان به دستگاه را هم راه می‌اندازد. aten::rsqrt هم وارون جذر را حساب می‌کند.

جدول میانگین زمان عملگرهای ضرب در پروفایل Neuron
سه فراخوانی aten::mul بیشترین سهم زمان دستگاه را می‌گیرند.

Neuron Explorer: دیدن مسیر یک عملگر

Neuron Explorer یک نمایشگر مرورگری برای رد سیستمی، خط زمانی دستگاه و تفکیک هر موتور است. به آن می‌توان از طریق تونل SSH یا افزونهٔ Visual Studio Code دسترسی داشت. در این نوشته از Explorer استفاده می‌کنیم تا آنچه را جدول key_averages() به‌صورت عددی گزارش می‌دهد تأیید کنیم و ببینیم بین دو ارسال چه اتفاقی می‌افتد.

بعد از پروفایلینگ، پوشهٔ خروجی رد سیستمی و — اگر داده دستگاه را ثبت کرده باشید — فایل‌های جفت‌شدهٔ .neff و .ntff را در خود دارد. قالب فایل اجرایی Neuron (NEFF) اجراپذیر کامپایل‌شده را نگه می‌دارد و NTFF متناظر، رد دستورهای سخت‌افزاری آن است؛ این دو یک هش عددی مشترک دارند که نشان می‌دهد جفت هستند. برای باز کردن پروفایل سیستم یا ترکیبی، کل پوشهٔ خروجی را آپلود می‌کنید. برای پروفایل فقط دستگاه، باید فایل‌های هم‌نام .neff و .ntff را جداگانه بارگذاری کنید.

در Explorer زمان از چپ به راست و چرخهٔ حیات رویدادها از بالا به پایین جریان دارد. هر عملگر ATen همین الگو را طی می‌کند: ساخته‌شدن در لاین فریم‌ورک، زمان‌بندی در لاین رانتایم با نام nrta_execute_schedule، و اجرا در لاین سخت‌افزار با نام nc_exec_running.

لاینپسوند برچسبچه چیزی را نشان می‌دهد
ارسال فریم‌ورکframework/TID:<id>رویدادهای ارسال عملگر (رانتایم PrivateUse1) به تفکیک شناسهٔ نخ
دستگاه فریم‌ورکframework/Stream:<n>رویدادهای تعامل با دستگاه (درایور PrivateUse1) به تفکیک شمارهٔ جریان
رانتایم Neuronneuron_rt/NC:<n>فراخوانی‌های API رانتایم Neuron، به تفکیک NeuronCore منطقی
سخت‌افزار Neuronneuron_hw/NC<n>:<id>رویدادهای اجرای سخت‌افزاری، به تفکیک NeuronCore فیزیکی

در حالت eager هر عملگر مستقل اجرا می‌شود. با نمایشگر زنجیرهٔ وابستگی می‌توانید یک aten::mul.Tensor را از لایهٔ فریم‌ورک تا زمان‌بندی رانتایم و اجرا روی دستگاه دنبال کنید.

مسیر اجرای عملگر ضرب از فریم‌ورک تا سخت‌افزار
Neuron Explorer مسیر یک aten::mul را از ارسال تا اجرا نشان می‌دهد.

نمونهٔ دوم: torch.rms_norm

حالا عملیات جداگانه را با تابع داخلی torch.rms_norm جایگزین می‌کنیم. Neuron این عملیات را به یک هستهٔ ادغام‌شده پایین می‌آورد. نتیجه در جدول چنین می‌شود:

نامتعداد فراخوانیزمان CPUزمان دستگاه Neuronسهم از زمان دستگاه
aten::_fused_rms_norm147.6 میکروثانیه403.3 میکروثانیه100%
جدول پروفایل هستهٔ ادغام‌شدهٔ RMSNorm
RMSNorm ادغام‌شده به‌صورت یک عملگر دستگاه ظاهر می‌شود.

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

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

نمونهٔ سوم: عملیات خام با torch.compile

در نمونهٔ آخر، همان تابع خام RMSNorm را با torch.compile و بک‌اند neuron کامپایل می‌کنیم. TorchDynamo بایت‌کد پایتون را به‌صورت گراف FX می‌گیرد و AOTAutograd آن را به عملگرهای ATen پایین می‌آورد. این‌جا بک‌اند Neuron کل گراف را یک‌جا دریافت می‌کند، نه هر عملگر را جداگانه؛ در نتیجه عملیات را ادغام می‌کند، دسترسی به حافظه را بهینه می‌کند و گراف را در یک فایل NEFF می‌سازد که با یک ارسال سخت‌افزاری اجرا می‌شود.

نامتعداد فراخوانیزمان CPUزمان دستگاه Neuronسهم از زمان دستگاه
Torch-Compiled Region: 0/0198.4 میکروثانیه375.5 میکروثانیه100%
جدول پروفایل ناحیهٔ کامپایل‌شده با torch.compile
تابع کامپایل‌شده به‌صورت یک ناحیهٔ Torch-Compiled در جدول دیده می‌شود.

جدول یک اجرای دستگاه نشان می‌دهد، شبیه نمونهٔ دوم، اما مسیر کد متفاوت است. این‌جا به‌جای فراخوانی یک عملیات ادغام‌شدهٔ آماده، کامپایلر خودِ ادغام را از تابع پایتون کشف می‌کند. در این اجرا زمان Neuron با torch.rms_norm قابل مقایسه است: 375 میکروثانیه در برابر 403 میکروثانیه.

گراف کامپایل‌شده در مسیر زمان‌بندی رانتایم
گراف کامپایل‌شده یک مسیر واحد را طی می‌کند.

مقایسهٔ کنار هم

نسخهزمان دستگاه Neuronزمان CPUتعداد فراخوانی عملگر
عملیات خام2,355 میکروثانیهحدود 125 میکروثانیه6
torch.rms_norm403 میکروثانیهحدود 48 میکروثانیه1
خام به‌همراه torch.compile376 میکروثانیهحدود 98 میکروثانیه1

هر دو روش، یعنی torch.rms_norm و torch.compile، محاسبه را در یک ارسال هسته ادغام می‌کنند. در این اجراها هر کدام حدود یک‌ششم زمان دستگاهِ پیاده‌سازی خام را می‌گیرند. ادغام، فاصله‌های میان اجراهای سخت‌افزاری را حذف می‌کند و جلوی ساخته‌شدن مقادیر میانی مثل x_sq و mean_sq بین عملگرهای جداگانه را می‌گیرد.

جمع‌بندی و قدم بعدی

تابع key_averages() نشان می‌دهد کدام عملگرها زمان دستگاه را می‌بلعند و خروجی کاملش آماده‌سازی و انتقال‌های سمت CPU را هم آشکار می‌کند. Neuron Explorer همان تصویر را به‌صورت بصری در اختیار می‌گذارد تا یک عملگر را از ارسال فریم‌ورک تا زمان‌بندی رانتایم و اجرا روی سخت‌افزار دنبال کنید و فاصله‌هایی را ببینید که ادغام حذفشان می‌کند.

همین روند روی مدل‌های بزرگ‌تر هم جواب می‌دهد: نقطهٔ داغ را در جدول پیدا کنید، آن را در رد تأیید کنید و بعد ببینید آیا یک عملیات ادغام‌شده یا torch.compile می‌تواند هزینه‌اش را کم کند. اگر روی شتاب‌دهنده‌های دیگر کار می‌کنید، نوشتهٔ TorchTPU و اجرای بومی پایتورچ روی تراشه‌های TPU گوگل هم مسیر مشابهی را نشان می‌دهد.

در بخش دوم این مجموعه، پروفایل سطح دستگاه بررسی می‌شود: باز کردن NEFF، نگاه به تفکیک TensorEngine، VectorEngine و دسترسی مستقیم به حافظه (DMA)، و قرار دادن هسته روی خط روف‌لاین برای تشخیص اینکه محاسبه‌محور است یا حافظه‌محور.

پرسش‌های متداول

چرا ستون زمان دستگاه در جدول پروفایل خالی می‌ماند؟

اگر داخل حلقهٔ پروفایل torch.neuron.synchronize() را صدا نزنید، ممکن است پروفایلر پیش از پایان کار دستگاه خروجی بگیرد و ستون زمان Neuron خالی بماند.

تفاوت پروفایل سیستم و پروفایل فقط دستگاه چیست؟

برای پروفایل سیستم یا ترکیبی، کل پوشهٔ خروجی را در Neuron Explorer بارگذاری می‌کنید؛ برای پروفایل فقط دستگاه باید فایل‌های هم‌نام .neff و .ntff را جداگانه آپلود کنید.

آیا torch.compile به‌اندازهٔ torch.rms_norm سریع است؟

در این اجرا زمان دستگاه برای torch.compile حدود 376 میکروثانیه و برای torch.rms_norm حدود 403 میکروثانیه بود؛ هر دو حدود یک‌ششم پیاده‌سازی خام زمان می‌برند.

منبع: Hugging Face

نوشتهٔ تیم گاندو

ما گاندو را می‌سازیم؛ دستیار هوش مصنوعی‌ای که از اسناد کسب‌وکار شما جواب می‌دهد.

دربارهٔ ما