میزبانی و ارائه درون‌سازمانی مدل‌های زبانی بزرگ در نتفلیکس

میزبانی و ارائه درون‌سازمانی مدل‌های زبانی بزرگ در نتفلیکس

نویسندگان: تیم زمان اجرای مدل (Model Runtime) و تیم استنتاج (Inference) پلتفرم هوش مصنوعی نتفلیکس

مقدمه

بسیاری از سازمان‌ها از مدل‌های زبانی بزرگ (LLM) از طریق ای‌پی‌آی‌های میزبانی‌شده (Hosted APIs) استفاده می‌کنند. اما نتفلیکس فراتر رفته است؛ ما پشته کامل (Full Stack) را خودمان اجرا می‌کنیم، از استقرار مدل تا استنتاج، آن هم درون محیط عملیاتی موجود به جای یک سیلوی مجزای یادگیری ماشین (ML Silo). برخی از این تصمیمات در ابتدا بدیهی نبودند و چند مورد از آن‌ها ناترازی‌ها و هزینه‌های خود را تنها زیر بار عملیاتی واقعی نشان دادند.

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

نگاهی کلی به معماری

یادگیری ماشین در مقیاس کاربران نتفلیکس، توسط یک سیستم ارائه یکپارچه بر پایه JVM هدایت می‌شود که جریان کامل ابتدای تا انتهای برنامه‌های پایین‌دستی را مدیریت می‌کند: منطق مسیریابی و تست A/B، تولید نامزدها، فراخوانی ویژگی‌ها، استنتاج، پس‌پردازش، و ثبت گزارش (Logging) در هر مرحله. هر دو مسیر لحظه‌ای (Real-time) و دسته‌ای کش‌شده (Cached Batch) پشتیبانی می‌شوند. شکل ۱ دو روش دسترسی فراخوانندگان به استنتاج را نشان می‌دهد: مسیر gRPC از طریق این سیستم ارائه، و مسیر مستقیم HTTP که توسط برنامه‌های جدیدتر بر پایه LLM استفاده می‌شود.

محل اجرای استنتاج به مدل بستگی دارد. مدل‌های کوچک پردازنده (CPU) در داخل پردازش (In-process) اجرا می‌شوند تا از هزینه تماس‌های از راه دور جلوگیری شود. مدل‌های بزرگتر به پردازنده گرافیکی (GPU) نیاز دارند؛ سیستم ارائه، پیش‌پردازش و پس‌پردازش را به صورت محلی انجام می‌دهد اما استنتاج را به یک سرویس از راه دور به نام «سرویس امتیازدهی مدل» (Model Scoring Service یا MSS) واگذار می‌کند. MSS پس‌زمینه (Backend) مشترک استنتاج است که از XGBoost، TensorFlow، PyTorch و LLMها پشت یک رابط یکپارچه پشتیبانی می‌کند و سرور استنتاج NVIDIA Triton در لایه زیرین آن، بارگذاری مدل، دسته‌بندی (Batching) و زمان‌بندی GPU را مدیریت می‌کند.

بر روی Triton، یک لایه کنترل جاوا قرار دارد که استقرار، نسخه‌گذاری، بررسی سلامت، مقیاس‌پذیری خودکار (Autoscaling) و انتشار چند‌منطقه‌ای را مدیریت می‌کند. توسعه‌دهندگان مدل، آرتیفکت‌های خود را بسته‌بندی کرده و استقرار را پیکربندی می‌کنند؛ لایه کنترل نمونه‌های GPU را تخصیص داده، Triton را پیکربندی می‌کند و ارتقای بدون وقفه (Zero-downtime) را سازماندهی می‌نماید.

شکل ۱: نگاهی کلی به معماری ارائه (Serving)

تصمیمات طراحی و پیاده‌سازی

چهار تصمیم اصلی این پلتفرم را شکل می‌دهند: موتور، بسته‌بندی، سطح ای‌پی‌آی و استراتژی انتشار. این تصمیمات به ترتیب وابستگی ارائه می‌شوند، زیرا هر یک محدودیت‌هایی را برای بعدی ایجاد می‌کند.

استفاده از vLLM به عنوان موتور اصلی (Paved-Path Engine)

این پلتفرم در ابتدا بر پایه TensorRT-LLM ساخته شد که در آن زمان یک موتور استنتاج پرقدرت بود و قبلاً با Triton (بخش محاسباتی مورد استفاده در MSS) یکپارچه شده بود.

تا تابستان ۲۰۲۵، دو تغییر رخ داد: موتورهای متن‌باز تا حد زیادی شکاف عملکردی خود را با پشته‌های اختصاصی پر کردند، و ترکیب بارهای کاری ما گسترش یافت تا شامل تولید تعبیه‌سازی (Embedding)، استنتاج پیش‌پرکننده (Prefill-only) برای رتبه‌بندی و بازیابی، رمزگشایی خودبازگشتی (Autoregressive decoding)، و مدل‌های سفارشی با منطق محدودیت غیرساده در هر مرحله شود. ما عملکرد را بر اساس این ترکیب بار کاری مجدداً ارزیابی کردیم و vLLM را به عنوان موتور اصلی خود بر اساس تناسب عملیاتی انتخاب کردیم:

  • بارگذاری معماری‌های سفارشی مدل بدون خط لوله کامپایل چندمرحله‌ای – که منجر به تکرار سریع‌تر روی مدل‌های غیراستاندارد می‌شود.
  • قلاب‌های قابلیت توسعه (Extensibility hooks) برای منطق رمزگشایی سفارشی – که برای کارهای رمزگشایی محدودشده ضروری است.
  • قابلیت اشکال‌زدایی (Debuggability) – بازرسی آسان‌تر خرابی‌ها و حالات میانی نسبت به یک موتور کامپایل‌شده در نسخه‌های اولیه TensorRT-LLM.
  • آشنایی – بسیاری از متخصصان یادگیری ماشین قبلاً از vLLM در تحقیقات استفاده می‌کردند که این امر هزینه انتقال از مرحله تحقیق به تولید را کاهش داد.

یکپارچه‌سازی vLLM در Triton

پس از انتخاب vLLM، تصمیم بعدی نحوه بسته‌بندی مدل‌ها برای آن بود. Triton از دو روش پشتیبانی می‌کند و این انتخاب پیامدهای مهمی برای قابلیت نگهداری دارد – به ویژه اینکه چگونه آرتیفکت‌های مدل با ارتقای فرانت‌اند جفت می‌شوند.

بک‌اند پایتون (Python backend): توسعه‌دهنده، مشخصات تانسور ورودی/خروجی صریح را در زمان بسته‌بندی تعریف می‌کند. این مشخصات در آرتیفکت منجمد می‌شوند و باید با آنچه سازنده درخواست فرانت‌اند فروشنده شخص ثالث انتظار دارد مطابقت داشته باشند؛ بنابراین هر ارتقای فرانت‌اند که مشخصات I/O را تغییر دهد، نیازمند هماهنگی در کد بسته‌بندی است، در غیر این صورت درخواست‌ها در زمان اجرا با شکست مواجه می‌شوند.

بک‌اند vLLM (vLLM backend): آرتیفکت تنها یک پیکربندی JSON است که به وزن‌های مدل و توکن‌ساز اشاره می‌کند. بک‌اند vLLM در Triton این پیکربندی را می‌خواند و مشخصات تانسور I/O را به صورت پویا در زمان استقرار تولید می‌کند – توسعه‌دهنده هرگز آن‌ها را تعریف نمی‌کند. مدل‌ها و فرانت‌اند به طور مستقل تکامل می‌یابند.

بک‌اند vLLM از نظر معماری پیش‌فرض درستی است. اما دو مسئله در محیط عملیاتی به ما ضربه زد:

  1. عدم تطابق نسخه Triton/vLLM: بک‌اند vLLM در Triton در برابر یک سطح ای‌پی‌آی خاص از vLLM کامپایل شده است. وقتی این دو دچار تغییر و عدم هماهنگی می‌شوند – برای مثال Triton 25.09 ماژول vllm.engine.metrics را فراخوانی می‌کند که در vLLM 0.11.2 حذف شده است – بک‌اند کلاً در بارگذاری شکست می‌خورد. پلتفرم باید نسخه‌های سازگار را هنگام ساخت ایمیج سرویس ثابت نگه دارد و از تغییر نسخه vLLM توسط توسعه‌دهندگان مدل در زمان بسته‌بندی جلوگیری کند.
  2. منطق مدل سفارشی: بک‌اند vLLM انتظار یک مدل استاندارد و سازگار با HuggingFace را دارد و کل چرخه زندگی استنتاج را مدیریت می‌کند. مدل‌هایی که نیاز به پیش‌پردازش، پس‌پردازش سفارشی یا اجرای غیراستاندارد دارند – مانند خطوط لوله هم‌افزایی (Ensemble) یا توکن‌سازی سفارشی – باید از بک‌اند پایتون استفاده کنند که کنترل کاملی روی تابع execute() می‌دهد. این راه فرار احتمالاً برای دسته‌ای از مدل‌ها همچنان ضروری خواهد بود.

فرانت‌اند HTTP سازگار با اکوسیستم

با مشخص شدن موتور و بسته‌بندی، پرسش بعدی نحوه دسترسی فراخوانندگان به سیستم بود. یکی از اهداف کلیدی طراحی سیستم ما این بود که مدل‌های زبانی بزرگ نباید استثناهای خاصی باشند. هر مدلی – چه انسامبل XGBoost و چه LLMهای بزرگ – از طریق همان فراخوانی gRPC امتیازدهی می‌شود، بنابراین ما از همان کتابخانه‌های کلاینت، بررسی سلامت و خطوط لوله استقرار مجدداً استفاده می‌کنیم. با توجه به اینکه رابط ای‌پی‌آی سازگار با OpenAI به رابط واقعی (De facto) برای اکوسیستم LLM تبدیل شده است – موتورهای استنتاج، فریم‌ورک‌های ارکستراسیون، ابزارهای ارزیابی و کتابخانه‌های کلاینت همگی با آن کار می‌کنند – ما ای‌پی‌آی سازگار با OpenAI را به عنوان یک فرانت‌اند اضافی در کنار gRPC ارائه کردیم.

نتیجه این کار در مسیر آزمایش تا تولید مشخص می‌شود: ارتقا از یک مدل میانی به یک مدل شخصی‌سازی‌شده و خود-میزبانی‌شده – به دلایل کیفیت، تاخیر، هزینه یا حریم خصوصی داده‌ها – تقریباً بدون درز انجام می‌شود. ای‌پی‌آی یکسان است و تغییرات کد حداقل است.

در پشت ای‌پی‌آی، پیاده‌سازی مجدداً از فرانت‌اند سازگار با OpenAI شرکت NVIDIA Triton استفاده می‌کند. این سرویس یک سرور داخلی Triton را راه‌اندازی کرده، آن را در یک TritonLLMEngine می‌پیچد که اسکیمای درخواست را به درخواست‌های استنتاج Triton تبدیل می‌کند و پاسخ‌ها را از طریق FastAPI ارائه می‌دهد. فرانت‌اندهای KServe HTTP/gRPC نیز در کنار آن فعال هستند، بنابراین همان نمونه Triton همچنان از طریق gRPC برای لایه کنترل جاوا قابل دسترسی است. استفاده مستقیم از فرانت‌اند Triton یک شکاف را نشان داد: response_format – که توسط اسکیما پذیرفته می‌شد – قبل از رسیدن به vLLM به صورت خاموش نادیده گرفته می‌شد، به طوری که فراخواننده‌ای که درخواست خروجی JSON داشت بدون محدودیت‌های رمزگشایی هدایت‌شده ادامه می‌داد و ممکن بود JSON نادرستی را دریافت کند بدون اینکه هیچ خطایی توسط پلتفرم نشان داده شود. ما فرانت‌اند را وصله (Patch) کردیم تا response_format را در زمان درخواست به پارامترهای رمزگشایی هدایت‌شده vLLM ترجمه کند.

استراتژی‌های استقرار

با استقرار سطح ای‌پی‌آی و موتور، مسئله باقی‌مانده این است که چگونه نسخه‌های جدید بدون افت درخواست‌ها منتشر شوند. استقرار GPU نسبت به سرویس‌های CPU زمان بیشتری برای راه‌اندازی می‌برد و اسکیمای I/O ممکن است بین نسخه‌های مدل تغییر کند – که یک مشکل هماهنگی اضافی ایجاد می‌کند. پلتفرم دو استراتژی ارائه می‌دهد:

استقرار قرمز-مشکی (Red-Black): نسخه جدیدی را در کنار نسخه فعلی مستقر می‌کند. پس از اینکه نمونه جدید از بررسی‌های سلامت عبور کرد، ترافیک به صورت مرحله‌ای منتقل می‌شود – نسخه جدید افزایش می‌یابد در حالی که نسخه قدیمی با همان سرعت کاهش می‌یابد. اگر هر مرحله با شکست مواجه شود، سیستم یک بازگشت به عقب (Rollback) اتمی را تحریک می‌کند. Red-Black زمانی انتخاب درستی است که رابط مدل پایدار باشد. تجربه عملیاتی نشان داد که وقتی نسخه جدید به تغییر اسکیمای I/O نیاز دارد (مثلاً ابعاد تانسور جدید)، یک شکاف هماهنگی ایجاد می‌شود: مصرف‌کننده بالادستی نمی‌تواند پیکربندی خود را تا زمانی که مدل جدید کاملاً فعال نشده است به روز کند، بنابراین ناچاراً درخواست‌های «قدیمی» را به استقرار «جدید» در طول پنجره مهاجرت ارسال می‌کند و آن درخواست‌ها با شکست مواجه می‌شوند.

استقرار نسخه‌بندی‌شده (Versioned): این شکاف را با نگهداری یک استقرار مستقل برای هر جفت (modelId, modelVersion) حل می‌کند. نسخه‌های متعدد به طور همزمان خدمت‌رسانی می‌کنند و استقرار مدل را از به‌روزرسانی‌های مصرف‌کننده جدا می‌کنند: مصرف‌کننده منتظر می‌ماند تا نسخه جدید کاملاً آماده شود و سپس پیکربندی خود را تغییر دهد، در حالی که نسخه قدیمی همچنان به ترافیک قدیمی پاسخ می‌دهد. پلتفرم استقرارهای قدیمی‌تر را پس از عدم فعالیت پاکسازی می‌کند اما همواره آخرین نسخه را حفظ می‌کند. هزینه این روش، افزایش موقت هزینه GPU در طول تداخل دوره انتقالی است.

ما توصیه می‌کنیم پیکربندی‌های متغیر (مانند اشکال تانسور) را مستقیماً در مدل استنتاج جاسازی کنید تا مستقل از نسخه شود و بتوان از مسیر ارزان‌تر Red-Black استفاده کرد. روش Versioned برای موارد نادری محفوظ است که تغییرات شکست‌دهنده در رابط اجتناب‌ناپذیر است.

نکات عملیاتی

فراتر از آن چهار تصمیم، دو جزئیات عملیاتی قابل ذکر است – هر دو شامل شکاف‌های تولیدی بودند که مرحله طراحی پیش‌بینی نکرده بود.

توالی بوت (Boot sequence)

راه‌اندازی یک نمونه vLLM-on-Triton شامل چند مرحله هماهنگ قبل از باز شدن پورت gRPC است. دو مورد از آن‌ها غیرمعمول هستند:

  • کش کردن مدل: دانلود LLMهای بزرگ مستقیماً از S3 یا Hugging Face هنگام استارت‌آپ به اندازه‌ای کند است که تاخیر شروع سرد (Cold-start) را از حد تحمل زمان‌بندها فراتر می‌برد. ما مدل‌ها را در زمان اعلام مدل روی Amazon FSx عینی‌سازی (Materialize) می‌کنیم، بنابراین شروع‌های گرم به جای ذخیره‌سازی شیء، به یک سیستم فایل با کارایی بالا دسترسی پیدا می‌کنند.
  • Triton داخلی در برابر مستقل: زمانی که مصرف‌کنندگان به ای‌پی‌آی سازگار با OpenAI نیاز دارند، Triton به عنوان یک سرور داخلی در درون پردازش فرانت‌اند OpenAI اجرا می‌شود؛ در غیر این صورت به صورت مستقل اجرا می‌شود. این مورد برای هر استقرار در زمان بسته‌بندی پیکربندی می‌شود.

بقیه توالی بوت مکانیکی است: استخراج بسته مدل، نصب پلاگین‌های سفارشی vLLM از طریق entry_points پایتون، پاکسازی دایرکتوری چندپردازشی Prometheus، و مسدود کردن پورت gRPC تا زمانی که موتور آماده شود.

نقطه پایانی متریک‌های یکپارچه

پاکسازی Prometheus در بالا به یک شکاف مشاهده‌پذیری گسترده‌تر اشاره دارد. vLLM متریک‌ها را در PROMETHEUS_MULTIPROC_DIR به صورت فایل‌های db. می‌نویسد؛ Triton متریک‌های سطح سرور را از طریق نقطه پایانی Prometheus خود گزارش می‌دهد. هیچ‌کدام از دیگری آگاه نیستند و پل داخلی Triton تنها ۹ مورد از بیش از ۴۰ متریک vLLM را نشان می‌دهد – و موارد حیاتی مانند توان توکن (Token throughput)، بهره‌برداری از کش KV و نرخ ماندگاری کش پیشوند (Prefix cache hit rates) را از دست می‌دهد.

ما یک پروکسی سبک HTTP اضافه کردیم که هر دو را در یک نقطه پایانی واحد metrics/ ادغام می‌کند: این پروکسی متریک‌های Triton را از طریق HTTP دریافت کرده، متریک‌های vLLM را با استفاده از MultiProcessCollector در Prometheus از روی دیسک می‌خواند و خروجی ترکیب‌شده را برمی‌گرداند. داشبوردها و هشدارهای موجود بدون تغییر کار می‌کنند.

بررسی عمیق: رمزگشایی محدودشده در مقیاس بالا

برخی از بارهای کاری تولیدی نتفلیکس به شدت به کنترل دقیق بر تولید توکن متکی هستند. به‌جای اعمال منطق تجاری پس از استنتاج – که مستلزم پرداخت هزینه برای تولیدات نامعتبر و سپس تلاش مجدد یا ترمیم است – ما محدودیت‌ها را به داخل حلقه رمزگشایی (Decode loop) منتقل می‌کنیم تا مدل خروجی‌هایی تولید کند که ساختاراً با قوانین منطبق باشند. ما این کار را از طریق رابط سفارشی logits processor در vLLM پیاده‌سازی می‌کنیم و هر محدودیت را به عنوان یک ماشین حالت مدل‌سازی می‌کنیم که با تاریخچه توکن‌های تولیدشده تکامل می‌یابد و ماسک‌های صلاحیت توکن را در هر مرحله صادر می‌کند. هر درخواست پروکسور پیکربندی‌شده خود را دریافت می‌کند، زیرا درخواست‌های مختلف قوانین متفاوتی را اعمال می‌کنند.

مقیاس‌پذیری این بخش دو نسخه از موتور را طی کرد: ما ابتدا روی vLLM V0 مستقر شدیم (چون V1 دارای کمبودهایی در ویژگی‌ها بود)، سپس در سه‌ماهه چهارم ۲۰۲۵ پس از به بلوغ رسیدن V1 به آن مهاجرت کردیم. دو بخش بعدی حالت قبل و بعد را نشان می‌دهند.

چرا پیاده‌سازی اول مقیاس‌پذیر نبود؟

پیاده‌سازی اولیه ما با پایتون خالص از نظر عملکرد درست بود اما به یک گلوی بطری در مقیاس‌پذیری برخورد کرد. در vLLM V0، پردازنده‌های سفارشی لاجیت‌ها به ازای هر درخواست اجرا می‌شوند: GPU لاجیت‌ها را برای کل دسته تولید می‌کند، CPU آن‌ها را کپی کرده و منتظر انتقال می‌ماند، و سپس منطق محدودیت به صورت ترتیبی برای هر درخواست اجرا می‌شود – ترتیبی به این دلیل که قفل GIL پایتون مانع از موازی‌سازی کار درخواست‌ها می‌شود. بنابراین زمان CPU در پردازش لاجیت به صورت خطی با اندازه دسته رشد می‌کند و تاخیرهای انتهایی (Tail latencies) ایجاد می‌کند. تاخیر ابتدا تا انتها محدود به CPU می‌شود حتی اگر گذار رو به جلوی مدل (Forward pass) به طور موثر روی GPU دسته‌بندی شده باشد. این یک گلوگاه نامرئی در بنچمارک‌های تک‌درخواست است که تنها تحت همزمانی واقعی ظاهر می‌شود. شکل ۲ الگوی سریالی را نشان می‌دهد.

شکل ۲: اجرای سریالی پردازنده لاجیت‌ها روی CPU با vLLM V0

ویژگی vLLM V1 امکان طراحی در سطح دسته (Batch-level) را فراهم کرد…