به گزارش از وبسایت cncf 📌

رابط دستگاه در گذشته فقط می‌توانست دستگاه‌ها را شمارش کند: nvidia.com/gpu: 1 یعنی یک کارت کامل؛ یا داشته باشید یا نداشته باشید. HAMI، که کمیته نظارت فنی CNCF (TOC) آن را به‌عنوان یک پروژه انکوباتوری در ۱۵ ژوئیه ۲۰۲۶ پذیرفت، یک خط‌لوله کامل (webhook پذیرش جهشی، scheduler extender، حاشیه‌نویسی‌ها و اجرای درون کانتینر) ساخت تا آن‌چه واژگان قدیمی قادر به بیانش نبود را توصیف و اجرا کند.

تخصیص منابع پویا (DRA) در Kubernetes از نسخه 1.34 در دسترس عمومی قرار گرفت و از نسخه 1.35 به‌صورت پیش‌فرض فعال است. با فعال شدن ویژگی consumableCapacity، حالا یک پاد می‌تواند از زمان‌بند بخواهد بخش مشخصی از حافظه یا توان دستگاه را به‌صورت طبیعی و بدون حاشیه‌نویسی دریافت کند. پرسشی که در کانال‌های HAMI زیاد مطرح می‌شود این است: آیا DRA باعث منسوخ شدن HAMI می‌شود؟ پاسخ کوتاه: خیر. پاسخ کامل بستگی دارد به اینکه درباره کدام بخش‌های HAMI صحبت می‌کنیم؛ رمزگذاری درخواست‌های کسری که زمان‌بند بتواند ببیند، دقیقاً همان بخشی است که DRA پوشش می‌دهد، اما اعمال آن محدودیت‌ها داخل کانتینر با دانه‌بندی تماس‌های CUDA کاری است که DRA برای آن طراحی نشده است.

HAMI بر همین مبنا دو نیمه دارد: بخش رمزگذاری/اعلام منابع که می‌تواند روی بالای DRA بازسازی شود و بخش اجرای زمان‌رانت (enforcement) که باید حفظ شود. در ادامه هر دو نیمه را مرور می‌کنیم و سپس می‌بینیم پشته‌ی DRA امروز چه می‌خواهد.

برای فهم بهترِ تغییرات ناشی از DRA، مفید است نگاهی به ماشین‌هایی بیندازیم که جایگزین می‌شوند: کدام بخش‌های خط‌لوله HAMI صرفاً به خاطر باریکی API افزونه دستگاه لازم بودند. برای مثال، یک کاربر HAMI چنین منابعی را نوشته بود:

resources:
  limits:
    nvidia.com/gpu: 1 # شناسه دستگاه از 1 کارت فیزیکی
    nvidia.com/gpumem: 8000 # مگابایت از VRA آن کارت
    nvidia.com/gpucores: 10 # 10% محاسبه، در گام‌های 1%

این اعداد برای زمان‌بند پیش‌فرض معنایی ندارند: منابع گسترده در Kubernetes فقط اعداد صحیح را می‌فهمند و از مجموع گره کم می‌کنند، بدون این‌که بدانند gpumem و gpucores باید روی همان کارت فیزیکی باشند. بنابراین HAMI از یک webhook پذیرش جهشی استفاده می‌کرد تا درخواست را به scheduler extender تحویل دهد؛ آنجا گره‌ها فیلتر می‌شوند، کارت‌ها امتیازدهی می‌شوند، یک UUID دستگاه انتخاب می‌گردد و نتیجه در قالب یک حاشیه‌نویسی ثبت می‌شود. مثالی از آن حاشیه‌نویسی:

hami.io/vgpu-devices-allocated: GPU-,NVIDIA,8000,10

در سمت node، افزونه دستگاه در Allocate() آن حاشیه‌نویسی را می‌خواند و متغیرهای محیطی مثل:

CUDA_DEVICE_MEMORY_LIMIT_0=8000m
CUDA_DEVICE_SM_in
CUDA_DEVICE_SM_AL

را تزریق می‌کرد و libvgpu.so را پیش‌بارگذاری می‌نمود تا محدودیت‌ها اعمال شوند. این راه‌حل کار می‌کرد و در مقیاس ثابت شده است (مانند اجرای DaoCloud بر بیش از 10000 GPU در چندین دیتا سنتر). اما واضح است که بخش بزرگی از طراحی راه‌حل ناشی از محدودیت API بوده: webhook چون scheduler نمی‌توانست درخواست را تجزیه کند، حاشیه‌نویسی چون API فیلدی برای «کدام کارت و چه مقدار» نداشت، و توافق مرموز رشته‌ای بین scheduler و kubelet که فقط اجزای HAMI آن را می‌فهمیدند. بیشتر پروژه‌های اشتراک‌گذاری کسری GPU در آن دوره تجارت مشابهی داشتند و از قالب حاشیه‌نویسی خصوصی خود استفاده می‌کردند؛ DRA برای پایان دادن به همین وضعیت طراحی شد.

آنچه DRA تغییر می‌دهد، جایگزینی شمارش‌های صحیح با یک مدل ادعایی (claim) شبیه به PVC است. چهار شیء در گروه Resource.k8s.io/v1 جریان را حمل می‌کنند و هر کدام مسئولیت متفاوتی دارند: ResourceSlice (منتشرشده توسط درایور دستگاه که دستگاه‌های واقعی هر گره را با ویژگی‌های ساختار یافته توضیف می‌کند)، DeviceClass (نوشته شده توسط ادمین که دسته‌هایی از دستگاه‌ها را با فیلتر CEL تعریف می‌کند)، ResourceClaim و ResourceClaimTemplate (نوشته شده توسط صاحب بار که ادعاها را ایجاد می‌کنند). زمان‌بند پیش از اتصال پاد، یک دستگاه مشخص را به ادعا اختصاص می‌دهد و نتیجه به‌عنوان یک شیء API حالت‌دار در وضعیت ادعا زندگی می‌کند. این تصمیم دیگر در یک رشته حاشیه‌نویسی پنهان نیست؛ حالا یک شیء درجه یک است که kubectl می‌تواند آن را ببیند، RBAC می‌تواند از آن محافظت کند و کنترلرهای دیگر می‌توانند روی آن عمل کنند.

زمان‌بندی انتشار مهم است: هسته‌ی DRA در Kubernetes نسخه 1.34 به GA رسید و از نسخه 1.35 پایدار شد. افزونه‌ها با سرعت خودشان پیش می‌روند: فهرست‌های اولویت‌بندی شدهٔ دستگاه در نسخه 1.36 پایدار شدند، در حالی که دستگاه‌های قابل پارتیشن‌بندی و consumableCapacity در نسخه 1.36 در حالت بتا بودند و تا زمان نگارش این متن (ژوئیه ۲۰۲۶) هنوز کامل نشده بودند. بخش consumableCapacity همان تکه‌ای است که برای نقشه‌برداری مدل HAMI ضروری است.

Core DRA به‌تنهایی امکان اشتراک‌گذاری به سبک HAMI را فراهم نمی‌کند: مدل پایهٔ اشتراک‌گذاری چند پاد به یک ResourceClaim ارجاع می‌دهد که یعنی پادها تخصیص یکسانی را به اشتراک می‌گذارند و تکه‌بندی کسری را تضمین نمی‌کند. consumableCapacity اما دو قابلیت کلیدی اضافه می‌کند: درایور می‌تواند دستگاه را با allowMultipleAllocations علامت بزند تا ادعاهای هم‌زمان مستقل (حتی از namespaceهای مختلف) روی آن امکان‌پذیر شوند، و ادعا می‌تواند درخواست «ظرفیت» مشخصی کند تا به‌جای درخواست دستگاه کامل، مقدار مشخصی از منبع نام‌گذاری‌شده را بخواهد. سپس زمان‌بند همان‌ کاری را انجام می‌دهد که برای حافظه گره انجام می‌داد: حسابداری ظرفیت تا مجموع اعطاها از ظرفیت اعلام‌شده دستگاه فراتر نرود.

تصویرِ تطبیق با منابع گستردهٔ HAMI تقریباً مکانیکی است: nvidia.com/gpumem: 8000 به یک درخواست ظرفیت حافظه تبدیل می‌شود و nvidia.com/gpucores: 10 به یک درخواست ظرفیت محاسباتی. مرحلهٔ فیلتر که قبلاً توسط توسعه‌دهندهٔ HAMI انجام می‌شد (“آیا این کارت هنوز 8000 مگابایت بدون وعده دارد؟”) به ریاضیات زمان‌بند بالادستی تبدیل می‌شود و ردهایی که کاربران HAMI آن‌ها را به‌عنوان CardInsufficientMemory می‌شناختند، اکنون به یک ادعای غیرقابل‌زمان‌بندی (unschedulable claim) استاندارد تبدیل می‌شوند. به همین دلیل نگهبانان HAMI، DRA را نه به‌عنوان رقیب بلکه به‌عنوان همگرایی می‌بینند: Kubernetes مدلِ راه‌حلی را که HAMI مدت‌ها پیاده‌سازی کرده بود، پذیرفته است و نقشه‌ی راه پروژه‌ها نیز در سال 2026 به سمت تطبیق با استاندارد DRA حرکت کرد.

اما زمان‌بندی تنها نیمهٔ راه است. DRA با consumableCapacity یک پی‌گیری وعده است؛ تضمین می‌کند که زمان‌بند بیش از ظرفیت دستگاه قول ندهد، اما در مواجهه با کانتینری که در زمان اجرا قول را می‌شکند کاری انجام نمی‌دهد. برای GPU این شکست می‌تواند فاجعه‌بار باشد: CUDA به ResourceClaim اهمیتی نمی‌دهد و یک حلقهٔ حریص cudaMalloc به‌راحتی VRAM را می‌بلعد. این‌جاست که اجرای HAMi وارد می‌شود: وظیفهٔ دوم HAMI انجام enforcement است که در HAMi-core زندگی می‌کند، یک کتابخانهٔ C (libvgpu.so) که قبل از اجرای کانتینر preload می‌شود، تماس‌های CUDA و NVML را رهگیری می‌کند و محدودیت‌های تخصیص‌یافته را اعمال می‌کند.

این رفتار را می‌توان روی کارت‌های مشترک به‌سادگی بررسی کرد: به دو پاد هر کدام 8000 مگابایت تخصیص دهید، سپس یکی را عمداً از حد خود بیشتر مصرف کنید — متخلف دقیقاً تا مرز 8000 مگابایت محدود می‌ماند و همسایه بدون مشکل کار می‌کند، حتی اگر کارت فیزیکی همچنان VRAM آزاد داشته باشد. این محدودسازی در سطح ظرف انجام می‌شود که دقیقاً چیزی است که برای خوشه‌های چندتیمی مشترک لازم است. اما باید صادق باشم: این اجرای نرم‌افزاری از طریق تداخل کتابخانه قابل دورزدن است — بار کاری‌ای که preload را دور بزند (مثل لینک استاتیک با درایور، تنظیم CUDA_DISABLE_CONTROL، یا container-in-container) از کنترل خارج می‌شود. برای محیط‌های چندمستاجر خصمانه، پارتیشن‌بندی سخت‌افزاری (مثل NVIDIA Multi-Instance GPU — MIG که HAMI می‌تواند آن را نیز به‌صورت داینامیک زمان‌بندی کند) مطلوب است. برای تیم‌های همکار، رهگیری نرم‌افزاری مزیت دارد: گام‌های 1 مگابایت برای حافظه و 1 درصد برای محاسبه در برابر پروفایل‌های ثابت MIG. هیچ چیز در DRA این لایهٔ enforcement را جایگزین نمی‌کند.

وظیفهٔ درایور در مدل DRA به CDI ختم می‌شود: درایور به runtime می‌گوید کدام گره‌ها باید deviceها را نصب کنند و چه محیطی باید تنظیم شود. آنچه پس از فراخوانی CUDA رخ می‌دهد از نظر طراحی خارج از دامنهٔ DRA است. از این رو معماری واقع‌گرایانه ترکیبی است: DRA زبان درخواست و زمان‌بندی را فراهم می‌کند، HAMi-core عضلهٔ زمان‌رانت را تأمین می‌کند و یک درایور بین این دو ترجمه انجام می‌دهد.

پشتهٔ DRA-HAMI که امروز وجود دارد، درایورش را دارد و کار در سه مخزن تقسیم شده است، زیرا هر کدام مسائل عملیاتی متفاوتی را حل می‌کنند. k8s-dra-driver پایه است: یک درایور DRA که حافظه و محاسبات هر GPU را به‌عنوان consumableCapacity در ResourceSlices منتشر می‌کند، افزونهٔ kubelet را اجرا می‌کند که تخصیص‌ها را در گره حل می‌کند و کانتینرها را از طریق CDI با اجرای HAMi-core متصل می‌نماید. این پروژه آن را به‌عنوان اولین درایور متن‌باز DRA برای GPUهای NVIDIA با consumableCapacity فعال توصیف کرده است.

HAMI-DRA به پرسشی که هر تیم پلتفرم می‌پرسد پاسخ می‌دهد: صدها مانیفست قبلاً علیه nvidia.com/gpu و nvidia.com/gpumem نوشته شده‌اند — حالا چه می‌شود؟ این پروژه یک webhook پذیرش جهشی فراهم می‌کند که منابع گسترده کلاسیک را از پادهای ورودی حذف می‌کند و به‌جای آن ResourceClaimها را در جریان ایجاد می‌کند، در حالی که UUID آشنا و یادداشت‌های نوع دستگاه را برای هدف‌گذاری حفظ می‌کند. نام‌های منابعی که ترجمه می‌شوند قابل تنظیم‌اند (ResourceName، resourceMem، resourceCores در مقادیر نمودار) تا منابعی که تغییر نام یافته یا منابع خاص فروشنده همچنان کار کنند.

اگر این الگو آشنا به نظر می‌رسد، به این دلیل است که HAMI راه‌حلی را که بسیاری از تیم‌ها قبلاً اجرا می‌کردند، رسمی و قابل مشاهده در API کرده است — ترکیبی از DRA برای درخواست و زمان‌بندی و HAMi-core برای اجرای زمان‌رانت که امروز در عمل کار می‌کند. 🔧