زیرساخت

اجرای مدل زبانی روی زیرساخت داخلی (on-premise)

چرا سازمان‌ها مدل زبانی را روی زیرساخت داخلی اجرا می‌کنند، این کار در عمل چه می‌خواهد — از حافظه‌ی GPU تا batching — و در ازایش چه چیزی از دست می‌رود.

· ۱۰ دقیقه مطالعه

مسئله از کجا شروع می‌شود

یک تیم مهندسی که تا امروز روی API ابری کار می‌کرده، معمولاً به یکی از این سه دیوار می‌خورد: دیتایی که اجازه‌ی خروج از شبکه‌ی سازمان را ندارد، صورت‌حسابی که با رشد ترافیک خطی بالا می‌رود و کسی نمی‌تواند سقفش را تضمین کند، یا وابستگی به سرویسی که وقتی از دسترس خارج شود، محصول شما هم از دسترس خارج می‌شود. اجرای مدل زبانی روی زیرساخت داخلی — همان on-premise — پاسخی است که به این سه فشار داده می‌شود. اما این پاسخ رایگان نیست: شما یک هزینه‌ی متغیر را با یک مسئولیت عملیاتی ثابت عوض می‌کنید. این مقاله درباره‌ی همین معامله است — اینکه on-premise در عمل چه چیزی از شما می‌خواهد و در ازایش چه چیزی از هوش مصنوعی ابری را از دست می‌دهید.

سه انگیزه‌ی واقعی

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

پیش‌بینی‌پذیری هزینه. هزینه‌ی API به ازای توکن است، پس با مصرف بالا می‌رود. هزینه‌ی on-premise عمدتاً ثابت است: سخت‌افزار، برق، فضا و آدم. اگر بار شما پایدار و بالا باشد، هزینه‌ی هر درخواست با افزایش استفاده کاهش پیدا می‌کند. اگر بار شما کم و پراکنده باشد، همان سخت‌افزار بیشتر وقت‌ها بیکار است و شما بابت بیکاری‌اش پول می‌دهید. شکل منحنی هزینه را در مقاله‌ی هزینه‌ی توکن بیشتر باز کرده‌ایم.

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

لایه‌ی سرویس‌دهی: چیزی که واقعاً باید بسازید

اشتباه رایج این است که on-premise را «دانلود وزن‌ها و اجرای یک اسکریپت» تصور کنیم. آن اسکریپت یک demo است، نه یک سرویس. چیزی که یک تیم باید داشته باشد:

  • موتور سرویس‌دهی (inference server). جایی که وزن‌ها یک بار در حافظه‌ی GPU بارگذاری می‌شوند و بین همه‌ی درخواست‌ها مشترک می‌مانند. این لایه است که continuous batching، مدیریت KV cache و استریم توکن‌ها را انجام می‌دهد.
  • لایه‌ی سازگاری API. اگر خروجی سرویس داخلی شما همان شکل درخواست/پاسخ استاندارد را داشته باشد، مهاجرت کد سمت مصرف‌کننده تقریباً صفر می‌شود و می‌توانید بعداً بین مدل داخلی و ابری جابه‌جا شوید.
  • صف و quota. روی زیرساخت داخلی، ظرفیت شما ثابت است. بدون صف، اولین باری که ده تیم هم‌زمان درخواست بفرستند، همه با هم کند می‌شوند. باید مشخص باشد کدام workload اولویت دارد و سهم هر مصرف‌کننده چقدر است.
  • مشاهده‌پذیری. تأخیر تا اولین توکن، توکن بر ثانیه، طول صف، مصرف حافظه‌ی GPU و نرخ خطا. بدون این‌ها هیچ ظرفیت‌سنجی‌ای ممکن نیست.
  • چرخه‌ی به‌روزرسانی. مدل جدید، وزن جدید، پیکربندی جدید — با امکان بازگشت. این را باید مثل استقرار هر سرویس دیگری ببینید.

حساب حافظه‌ی GPU

این بخش جایی است که بیشترین برآورد اشتباه اتفاق می‌افتد. حافظه‌ی GPU سه مصرف‌کننده‌ی اصلی دارد و باید هر سه را جمع بزنید.

۱. وزن‌ها. ساده‌ترین بخش: تعداد پارامتر ضربدر بایت به ازای هر پارامتر.

حافظه‌ی وزن‌ها ≈ تعداد پارامتر × بایت بر پارامتر

FP16 / BF16 → 2 بایت
INT8        → 1 بایت
4-bit       → 0.5 بایت

یعنی یک مدل ۷ میلیارد پارامتری در FP16 حدود ۱۴ گیگابایت فقط برای وزن‌ها می‌خواهد، همان مدل در INT8 حدود ۷ گیگابایت و در ۴ بیت حدود ۳٫۵ گیگابایت. این یک ضرب است، نه یک benchmark — و دقیقاً به همین دلیل قابل اتکاست.

۲. KV cache. این بخشی است که معمولاً فراموش می‌شود و بعد در بار واقعی سیستم را از پا درمی‌آورد. مدل برای هر توکنی که تا الان دیده، بردارهای key و value هر لایه را نگه می‌دارد تا مجبور نشود دوباره حساب کند:

KV cache یک دنباله ≈ 2 (K و V) × تعداد لایه × تعداد head کلید/مقدار
                      × ابعاد هر head × بایت بر مقدار × طول دنباله

دو نکته‌ی مهم اینجاست. اول اینکه این عدد خطی با طول دنباله رشد می‌کند؛ پس یک درخواست با context طولانی چند برابر یک درخواست کوتاه حافظه می‌گیرد. دوم اینکه خطی با تعداد درخواست هم‌زمان رشد می‌کند. ظرفیت واقعی سرویس شما را معمولاً همین عدد تعیین می‌کند، نه اندازه‌ی وزن‌ها. اگر می‌خواهید بدانید چرا context طولانی این‌قدر گران تمام می‌شود، پنجره‌ی زمینه همین موضوع را از زاویه‌ی دیگری توضیح می‌دهد.

۳. سربار اجرا. فعال‌سازی‌های میانی، بافرهای موقت، فضای kernelها و تکه‌تکه شدن حافظه. این را نمی‌شود دقیق حساب کرد ولی نمی‌شود صفر هم گرفت.

قاعده‌ی عملی: حافظه‌ی وزن‌ها را حساب کنید، بودجه‌ی KV cache را بر اساس بدترین ترکیب «طول context × تعداد درخواست هم‌زمان» که می‌خواهید پشتیبانی کنید اضافه کنید، و بعد حاشیه‌ی امن بگذارید. اگر GPU را تا لبه پر کنید، اولین صف شلوغ به خطای out-of-memory تبدیل می‌شود.

کوانتیزه‌سازی: چه چیزی می‌خرید و چه چیزی می‌دهید

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

مدل کوانتیزه‌شده معمولاً در جواب‌های کوتاه و ساده تفاوت محسوسی ندارد، اما در کارهایی که زنجیره‌ی استدلال طولانی دارند، در تولید خروجی ساختاریافته مثل JSON، و در زبان‌هایی که در داده‌ی آموزش سهم کمتری داشته‌اند — فارسی از همین دسته است — زودتر و بیشتر افت می‌کند. پس تنها راه درست این است که کیفیت را روی workload خودتان بسنجید، نه روی معیارهای عمومی. یک مجموعه‌ی ارزیابی کوچک اما واقعی از همان کاری که قرار است انجام شود، تصمیم‌گیری را از حدس خارج می‌کند؛ ارزیابی ایجنت روش ساختن چنین مجموعه‌ای را توضیح می‌دهد.

batching، تأخیر و throughput

روی یک GPU، اجرای تک‌درخواستی اتلاف است. سخت‌افزار وقتی بازده دارد که چند دنباله هم‌زمان پیش بروند. اینجا یک تنش ذاتی وجود دارد:

هدفاثر batch بزرگ‌تر
throughput کل (توکن بر ثانیه‌ی سرویس)بهتر می‌شود
تأخیر یک درخواست منفردبدتر می‌شود
مصرف حافظه‌ی KV cacheبیشتر می‌شود

یعنی نمی‌توانید هم‌زمان بیشترین throughput و کمترین تأخیر را داشته باشید. باید تصمیم بگیرید کدام workload کدام را می‌خواهد: یک chat تعاملی به تأخیر تا اولین توکن حساس است، یک job دسته‌ای شبانه اصلاً نیست. جدا کردن این دو روی صف‌های مختلف — یا حتی روی مدل‌های مختلف — تقریباً همیشه از تنظیم یک batch size واحد برای همه بهتر جواب می‌دهد.

آنچه از دست می‌دهید

این بخش را معمولاً در ارائه‌های داخلی حذف می‌کنند. حذفش نکنید:

  • سقف کیفیت. بزرگ‌ترین مدل‌هایی که می‌توانید داخل بیاورید معمولاً هم‌سطح بهترین مدل‌های میزبانی‌شده نیستند. برای خیلی از کارها این فرقی نمی‌کند؛ برای کارهای استدلالی سنگین می‌کند.
  • سرعت به‌روزرسانی. مدل بهتر که منتشر شود، روی API همان روز در دسترس است. روی زیرساخت شما یک پروژه است: ارزیابی، ظرفیت‌سنجی، استقرار.
  • کشش ظرفیت. ابر در اوج بار کش می‌آید. GPU شما نمی‌آید. اوج بار روی زیرساخت داخلی یعنی صف طولانی‌تر.
  • قابلیت‌های جانبی. چیزهایی مثل prompt caching یا ابزارهای آماده‌ی سمت سرور را باید خودتان بسازید.
  • هزینه‌ی آدم. این بزرگ‌ترین قلم پنهان است. یک سرویس مدل داخلی باید کسی داشته باشد که نگهش دارد، در ساعت غیر اداری هم.

پس چه زمانی منطقی است

on-premise وقتی جواب می‌دهد که حداقل یکی از این‌ها درست باشد: الزام قانونی یا قراردادی اجازه‌ی خروج داده را ندهد؛ بار شما به قدر کافی بالا و پایدار باشد که سخت‌افزار بیکار نماند؛ یا به تضمین رفتاری و کنترل نسخه‌ای نیاز داشته باشید که سرویس بیرونی نمی‌دهد.

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

تصمیم on-premise یک تصمیم زیرساختی است، نه یک تصمیم مدل. قبل از انتخاب وزن‌ها، مشخص کنید چه کسی این سرویس را نگه می‌دارد، ظرفیتش را چطور می‌سنجید و وقتی GPU پر شد چه اتفاقی می‌افتد. اگر جواب این سه سؤال روشن نیست، هنوز آماده‌ی این کار نیستید.