SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-13

چرا سرعت LLM خودمیزبان با افزایش کاربران کاهش می‌یابد؟

اگر LLM شما با بیش از 1 کاربر کند می‌شود، مشکل از سخت‌افزار نیست. با تنظیم پارامتر num_parallel و مدیریت KV cache در موتورهای سرویس‌دهی، محدودیت صف و ظرفیت پردازش همزمان را رفع کنید.

چرا سرعت LLM خودمیزبان (self-hosted) با افزایش کاربران کاهش می‌یابد؟

یک LLM خودمیزبان با رسیدن به 5 کاربر همزمان دچار کندی می‌شود، زیرا سرور همچنان در حال تولید پاسخ‌ها به صورت یکی‌یکی است و چهار کاربر دیگر در صف انتظار قرار دارند. مستندات Ollama در مورد تنظیمات پیش‌فرض صریح است: OLLAMA_NUM_PARALLEL «حداکثر تعداد درخواست‌های موازی است که هر مدل به‌طور همزمان پردازش می‌کند و مقدار پیش‌فرض آن 1 است.» هیچ خرابی‌ای وجود ندارد. چهار نفر از پنج کاربر شما منتظر نوبت خود هستند.

راه‌حل این مشکل به‌ندرت ارتقای سخت‌افزاری سرور است. راه‌حل، استفاده از یک موتور سرویس‌دهی (serving engine) است که چندین درخواست را در یک forward pass از مدل عبور دهد، به همراه حافظه آزاد کافی برای نگهداری وضعیت گفتگوی همه کاربران در حین انجام این عملیات. هر دو بخش اهمیت دارند و بخش دوم همان چیزی است که در واقع سقف ظرفیت شما را تعیین می‌کند.

دو مرحله‌ای که هر درخواست طی می‌کند

مرحله Prefill کل پرامپت را یک‌جا می‌خواند و کش توجه (attention cache) را برای آن می‌سازد. از آنجا که تمام توکن‌های پرامپت با هم از مدل عبور می‌کنند، Prefill یک ضرب ماتریسی بزرگ است و توسط توان عملیاتی محاسباتی (arithmetic throughput) محدود می‌شود. مرحله Decode سپس پاسخ را توکن به توکن می‌نویسد. برای هر توکن، لازم است تمام وزن‌های مدل دوباره از حافظه خوانده شوند، در حالی که محاسبات انجام‌شده روی آن تک‌توکن بسیار ناچیز است. مرحله Decode توسط پهنای باند حافظه محدود می‌شود.

این عدم تقارن، دلیل اصلی کارایی دسته‌بندی (batching) است. عملیات Decode برای یک کاربر، مثلاً 5 GB وزن را به ازای هر توکن می‌خواند و اکثر واحدهای محاسباتی را بیکار می‌گذارد. با افزودن درخواست دوم، موتور همان 5 GB را یک‌بار می‌خواند و سپس دو توکن از آن محاسبه می‌کند. کاربر دوم تقریباً هیچ زمان اضافه‌ای تحمیل نمی‌کند. سرویس‌دهی به درخواست‌ها به‌صورت کاملاً متوالی، این مزیت را از بین می‌برد.

دو عدد، تجربه کاربر را توصیف می‌کنند. TTFT (زمان تا اولین توکن) برابر است با زمان انتظار در صف به‌علاوه زمان Prefill. ITL (تأخیر بین توکن‌ها) فاصله زمانی بین توکن‌های استریم‌شده است و توسط مرحله Decode تعیین می‌شود. یک سرور کند معمولاً در یکی از این دو مورد ضعف دارد و راه‌حل‌های آن‌ها یکسان نیست.

دسته‌بندی ایستا (Static batching) باعث می‌شود همه منتظر کندترین پاسخ بمانند

دسته‌بندی ایستا نسخه ساده‌انگارانه است و زمانی رخ می‌دهد که شما درخواست‌ها را به‌صورت دستی در کد برنامه گروه‌بندی کنید. موتور پردازش، N درخواست را جمع‌آوری کرده و آن‌ها را با هم اجرا می‌کند و تا زمانی که طولانی‌ترین تولید متن در گروه به پایان نرسد، تمام جایگاه‌ها (slots) را اشغال نگه می‌دارد.

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

این کار دو هزینه به همراه دارد. توالی‌های تکمیل‌شده همچنان جایگاه‌هایی را اشغال می‌کنند که هیچ پردازش مفیدی انجام نمی‌دهند، بنابراین با تغییر طول خروجی‌ها، نرخ عملیاتی (throughput) به‌شدت کاهش می‌یابد؛ و طول خروجی‌های چت بسیار متغیر است. درخواستی که یک گام پس از تشکیل دسته برسد، باید منتظر بماند تا کل دسته تخلیه شود و سپس مرحله prefill آن آغاز گردد؛ این یعنی زمان اولین توکن (TTFT) آن درخواست، توسط مقاله طولانی شخص دیگری تعیین می‌شود.

دسته‌بندی پیوسته (Continuous batching) درخواست‌ها را در هر توکن می‌پذیرد و خاتمه می‌دهد

دسته‌بندی پیوسته، زمان‌بندی را در سطح یک گام رمزگشایی (decoding step) انجام می‌دهد. پس از هر گام، زمان‌بند توالی‌هایی را که به‌تازگی توکن توقف (stop token) خود را صادر کرده‌اند حذف می‌کند و سپس درخواست‌های در انتظار را در اسلات‌های خالی جای می‌دهد. پاسخی که در گام 40 به پایان می‌رسد، اسلات خود را در همان گام 40 آزاد می‌کند، نه در پایان یک دسته (batch).

این موضوع غیرعادی نیست. llama-server در مستندات خود -cb, --cont-batching را به‌عنوان «اینکه آیا دسته‌بندی پیوسته (که با نام دسته‌بندی پویا نیز شناخته می‌شود) فعال باشد یا خیر (پیش‌فرض: فعال)» تعریف می‌کند و vLLM بر پایه همین ایده ساخته شده است. Ollama نیز درخواست‌های موازی را سرویس‌دهی می‌کند. مقدار پیش‌فرض فقط تعداد را روی یک محدود می‌کند؛ به همین دلیل است که بسیاری از کاربران به این نتیجه می‌رسند که سخت‌افزارشان توانایی اجرای هم‌زمان (concurrency) را ندارد، در حالی که این پیکربندی خودشان بوده که آن را غیرفعال کرده است.

نتایج منتشرشده از دسته‌بندی پیوسته معمولاً روی کارت‌های دیتاسنتر اندازه‌گیری می‌شوند که هم توان پردازشی مازاد دارند و هم ده‌ها گیگابایت حافظه برای کش (cache). شکل آن نتایج در سیستم شما نیز صادق است، اما اندازه آن‌ها خیر؛ و بخش حافظه در ادامه، دلیل این موضوع را توضیح می‌دهد.

رقابت Prefill با Decode برای منابع پردازشی یکسان

هنگامی که یک درخواست جدید در حین استریم شدن چهار پاسخ وارد می‌شود، prompt آن باید ابتدا prefill شود و prefill از نظر محاسباتی سنگین است. اگر زمان‌بند (scheduler) برای این prefill یک گام (step) اختصاصی در نظر بگیرد، آن چهار کاربر در حال استریم، در طول این مدت هیچ توکنی دریافت نمی‌کنند. در یک prompt طولانی، این موضوع باعث ایجاد یک وقفه مشهود در تمام پنجره‌های باز می‌شود. این همان لرزشی (stutter) است که کاربران هنگام توصیف وقفه سرور در لحظه ارسال درخواست توسط شخص دیگر، به آن اشاره می‌کنند.

قابلیت Chunked prefill یک prompt طولانی را به قطعات کوچک‌تر تقسیم کرده و هر قطعه را در همان گامی که decodeهای در حال اجرا قرار دارند، ترکیب می‌کند. راهنمای تنظیمات vLLM این بده‌بستان (tradeoff) را به‌طور مستقیم بیان می‌کند: مقادیر کوچک‌تر برای chunk budget «باعث بهبود ITL می‌شوند، زیرا تعداد prefillهایی که سرعت decode را کاهش می‌دهند کمتر است»، در حالی که مقادیر بالاتر «باعث بهبود زمان اولین توکن (TTFT) می‌شوند، زیرا می‌توانید توکن‌های prefill بیشتری را در یک batch پردازش کنید». شما در حال انتخاب این هستید که تجربه چه کسی را اولویت دهید: شخصی که منتظر شروع پاسخ است، یا کسانی که در حال تماشای استریم متن هستند.

طول prompt تعیین می‌کند که این موضوع چقدر آسیب‌زا است. یک prompt با 6000 توکن و پاسخ 200 توکنی، شامل 6000 توکن کار prefill در برابر 200 گام decode است. چت‌های مبتنی بر بازیابی (RAG) و promptهای سیستمی طولانی، هر دو شما را به این وضعیت سوق می‌دهند، بنابراین prefill دیگر یک خطای ناچیز نیست و به عاملی تبدیل می‌شود که کاربران منتظر آن می‌مانند. قابلیت Prefix caching زمانی که بخش طولانی تکرار می‌شود کمک می‌کند: vLLM از --enable-prefix-caching استفاده می‌کند که به جای محاسبه مجدد برای هر درخواست، cache را برای یک پیشوند (prefix) مشترک در prompt بازاستفاده می‌کند.

اولین حافظه‌ای که تمام می‌شود، KV cache است

هر توکن در هر گفتگوی فعال، یک بردار کلید (key vector) و یک بردار مقدار (value vector) در هر لایه از مدل باقی می‌گذارد. این همان KV cache (حافظه پنهان کلید/مقدار) است و باعث می‌شود مرحله decode نیازی به محاسبه مجدد کل prompt برای هر توکن جدید نداشته باشد. اندازه آن برای هر توکن توسط ساختار مدل تعیین می‌شود: 2 (یک کلید، یک مقدار) ضرب در تعداد لایه‌ها، ضرب در تعداد سرهای کلید/مقدار، ضرب در ابعاد سر، ضرب در تعداد بایت‌های هر مقدار. این اعداد را از config.json مدل استخراج کنید.

یک بار این محاسبه را انجام دهید تا سقف حافظه دیگر یک معما نباشد. یک مدل معمولی 8B با 36 لایه، 8 سر کلید/مقدار و ابعاد سر 128، که cache را با دقت 16-bit نگه می‌دارد، به ازای هر توکن 2 36 8 128 2 بایت هزینه دارد. این مقدار برابر با 147,456 بایت یا حدود 144 KiB است. بنابراین، یک گفتگوی 8,192 توکنی تقریباً به 1.2 GB حافظه cache نیاز دارد. پنج گفتگو از این نوع، علاوه بر وزن‌های مدل، به حدود 6 GB حافظه نیاز دارند و این پاسخ واقعی برای این است که چه تعداد کاربر در سیستم جای می‌گیرند.

هم‌زمانی (concurrency)، زمینه (context) را چندبرابر می‌کند و ابزارها این موضوع را صراحتاً اعلام می‌کنند. در FAQ مربوط به Ollama آمده است: «پردازش موازی درخواست‌ها برای یک مدل مشخص، منجر به افزایش اندازه context به تعداد درخواست‌های موازی می‌شود. برای مثال، یک context با اندازه 2K و 4 درخواست موازی، منجر به یک context با اندازه 8K و تخصیص حافظه اضافی خواهد شد.» رم مورد نیاز با OLLAMA_NUM_PARALLEL ضرب در OLLAMA_CONTEXT_LENGTH مقیاس‌پذیر است. در llama-server، زمینه‌ای که با -c درخواست می‌کنید بین -np اسلات تقسیم می‌شود، بنابراین افزایش تعداد اسلات به تنهایی باعث کاهش ظرفیت هر درخواست می‌شود. به جای فرض کردن، اندازه context هر اسلات را از لاگ راه‌اندازی (startup log) بخوانید.

در مقابل، vLLM از پیش‌تخصیص (preallocation) استفاده می‌کند. --gpu-memory-utilization (پیش‌فرض 0.92) «کسری از حافظه GPU است که برای اجرای مدل استفاده می‌شود». هر چقدر از حافظه پس از بارگذاری وزن‌ها باقی بماند، به استخر paged KV تبدیل می‌شود و زمانی که این استخر کم می‌آورد، scheduler به جای شکست دادن درخواست، آن را اخراج (evict) می‌کند:

WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.

در موتور V1 از vLLM، حالت پیش‌فرض پیش‌گیری (preemption)، RECOMPUTE است؛ بنابراین درخواست اخراج شده، cache خود را دور می‌ریزد و هنگام پذیرش مجدد، دوباره مرحله prefill را انجام می‌دهد. این کار دو بار انجام می‌شود. مستندات هشدار می‌دهند که «پیش‌گیری و محاسبه مجدد می‌تواند تأثیر منفی بر تأخیر کلی (end-to-end latency) داشته باشد» و این خط لاگ، بهترین توضیح برای این است که چرا یک کاربر بدشانس بسیار بیشتر از بقیه منتظر مانده، در حالی که میانگین زمان پاسخ‌دهی شما مناسب به نظر می‌رسد. برای ثبت تعداد تجمعی، disable_log_stats=False را تنظیم کنید یا شمارنده preemption را از معیارهای Prometheus که vLLM ارائه می‌دهد، بخوانید.

تغییرات در تعداد 2، 5 و 20 کاربر همزمان

دو کاربر. این وضعیت روی یک GPU که حافظه کش مازاد دارد تقریباً نامحسوس است، زیرا جریان رمزگشایی دوم با صرف زمان بسیار کمی در کنار جریان اول پردازش می‌شود. در یک VPS فقط متکی به CPU با 4 تا 8 گیگابایت رم، این کار رایگان نیست: هر دو جریان از تعداد محدودی vCPU و پهنای باند رم یکسانی استفاده می‌کنند، بنابراین هر کاربر تقریباً نصف توکن در ثانیه دریافت می‌کند و تقاضا برای حافظه کش نسبت به بودجه بسیار محدودتر، دو برابر می‌شود.

پنج کاربر. اینجاست که تنظیمات پیش‌فرض دیگر کافی نیستند و مشکل از صف شروع می‌شود. با OLLAMA_NUM_PARALLEL برابر با 1، چهار نفر منتظر کسی می‌مانند که درخواست پاسخ طولانی‌تری داشته است و هر کدام از آن‌ها به محض رسیدن نوبتشان، سرعت عادی را تجربه می‌کنند. اگر تعداد پردازش موازی را افزایش دهید، ماهیت مشکل تغییر می‌کند: پنج اسلات با 8K کانتکست برای هر کدام، به معنای یافتن فضایی برای 40K توکن در حافظه کش است. اگر این مقدار در VRAM جا نشود، موتور لایه‌ها را به رم سیستم منتقل می‌کند و اگر در رم هم جا نشود، سیستم شروع به Swap می‌کند و سرعت تولید توکن در ثانیه به‌شدت افت می‌کند.

بیست کاربر. بیست انسان در یک رابط کاربری چت معمولاً به معنای بیست درخواست همزمان نیستند و این مفیدترین نکته‌ای است که باید پیش از خرید سخت‌افزار درک کنید. یک فرد پاسخ را می‌خواند و بین هر نوبت، 20 تا 60 ثانیه فکر می‌کند، بنابراین بخش بزرگی از نشست (session) آن‌ها در حالت بیکار (idle) است. بیست عامل (agent) یا بیست کار خلاصه‌سازی اسناد، بیست جریان واقعی بدون هیچ زمان بیکاری هستند. این سناریو به ماشین متفاوتی نیاز دارد.

آیا کاربران شما همزمان فعال هستند یا فقط لاگین کرده‌اند؟

پیش از تعیین ابعاد هر چیزی، تعداد درخواست‌های در حال اجرا (in flight) را محاسبه کنید. محاسبات ساده است: تعداد درخواست‌های در حال اجرا برابر است با تعداد کاربران، ضرب‌در ثانیه‌های صرف‌شده برای تولید پاسخ در هر نوبت، تقسیم بر ثانیه‌های بین هر نوبت.

  1. ابتدا سرعت تک‌جریانی (single-stream) خود را اندازه بگیرید؛ هم prefill و هم decode. از عددی که دیگران برای کارت خود به دست آورده‌اند استفاده نکنید: تعداد توکن بر ثانیه را روی سیستم خودتان اندازه بگیرید و از همان مقدار استفاده کنید.
  2. چرخه کاری (duty cycle) را تخمین بزنید. بیست کاربر چت، 12 ثانیه زمان تولید در هر نوبت، و یک نوبت در هر 90 ثانیه، برابر است با 20 * 12 / 90، که حدود 2.7 درخواست در حال اجرا است.
  3. تعداد slotها را کمی بیشتر از این مقدار تنظیم کنید و سپس آن را با حافظه تطبیق دهید: حاصل‌ضرب تعداد slotها در context هر درخواست باید در توکن‌های cache موجود شما جای بگیرد.
  4. صف را کوتاه نگه دارید تا سرریز (overflow) سریع و به‌وضوح مشخص شود.

توکن‌های cache موجود، همان حافظه آزاد پس از بارگذاری وزن‌هاست که بر هزینه هر توکن (طبق بخش قبل) تقسیم شده است. یک کارت 24 GB که مدل 8B را با دقت 16-bit اجرا می‌کند، حدود 16 GB را صرف وزن‌ها کرده و در حالت استفاده پیش‌فرض، حدود 6 GB حافظه cache قابل استفاده دارد که معادل حدود پنج مکالمه 8K است. برای جای دادن موارد بیشتر، context هر درخواست را کوتاه کنید یا cache را با دقت 8-bit ذخیره کنید (llama-server از --cache-type-k q8_0 استفاده می‌کند). هر دو روش با فدا کردن بخشی از کیفیت، همزمانی (concurrency) را افزایش می‌دهند. پیش از صرف هزینه برای سخت‌افزار، خواندن نسخه صادقانه این معامله ارزشمند است: کجا نقطه سربه‌سر GPU VPS در برابر توکن‌های API قرار دارد.

جایی که تنظیمات پیش‌فرض Ollama دیگر کافی نیستند

تعداد پردازش‌های موازی را از طریق unit سرویس افزایش دهید، زیرا دستور export در shell به daemon تحت مدیریت systemd نمی‌رسد.

sudo systemctl edit ollama.service
[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"
sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama ps

دستور systemctl show باید سه متغیری که به‌تازگی تنظیم کرده‌اید را نمایش دهد. اگر این‌طور نیست، فایل drop-in ذخیره نشده است و سایر اقدامات شما بی‌اثر خواهند بود. سپس ollama ps مدل بارگذاری‌شده را با اندازه‌ای بزرگ‌تر از وزن‌های خالص نمایش می‌دهد، زیرا چهار اسلات با 8,192 توکن، 32,768 توکن حافظه کش را در کنار آن‌ها رزرو می‌کنند. اگر ستون PROCESSOR بخشی از مدل را روی CPU نشان می‌دهد در حالی که انتظار داشتید تمام آن روی GPU باشد، یعنی حافظه کش بیشتری نسبت به ظرفیت باقی‌مانده کارت گرافیک درخواست کرده‌اید. یکی از این دو عدد را کاهش دهید.

مقدار پیش‌فرض صف نیاز به بررسی مجدد دارد. Ollama تا OLLAMA_MAX_QUEUE درخواست را در صف قرار می‌دهد و «مقدار پیش‌فرض 512 است». فراتر از آن، سرور «با خطای 503 که نشان‌دهنده بارگذاری بیش از حد سرور است» پاسخ می‌دهد. یک صف با عمق 512 روی سیستمی که هم‌زمان چهار درخواست را پردازش می‌کند، وعده‌ای است که نمی‌توانید به آن عمل کنید، زیرا کلاینتی که در موقعیت 300 قرار دارد، بسیار پیش از رسیدن نوبتش دچار timeout می‌شود. یک صف کوتاه، خطایی را برمی‌گرداند که اپلیکیشن شما می‌تواند آن را مجدداً تلاش کند یا گزارش دهد؛ این بهتر از نمایش یک آیکون بارگذاری است که هرگز به نتیجه نمی‌رسد.

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

زمانی که یک موتور سرویس‌دهی واقعی به سوددهی می‌رسد

زمانی که یک GPU با فضای خالی دارید و بیش از حدود 4 درخواست به‌طور هم‌زمان در حال پردازش هستند، vLLM ارزش پیچیدگی راه‌اندازی خود را نشان می‌دهد. زمان‌بند (scheduler) این ابزار بر اساس توکن عمل می‌کند، حافظه کش آن صفحه‌بندی شده است تا قطعات آزاد مجدداً استفاده شوند و VRAM بلااستفاده را به‌جای رها کردن در حالت بیکار، به ظرفیت هم‌زمانی تبدیل می‌کند. تا اوت 2026، نصب و راه‌اندازی مستندشده شامل دو دستور است:

uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instruct
curl http://localhost:8000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "Qwen/Qwen2.5-1.5B-Instruct",
        "messages": [{"role": "user", "content": "Say hello."}]
    }'

پاسخی که حاوی یک آرایه choices باشد، به این معنی است که سرور بالا آمده و مدل بارگذاری شده است. تحت فشار کاری، دو پارامتری که اهمیت دارند عبارتند از --max-num-seqs، یعنی «حداکثر تعداد توالی‌هایی که در یک تکرار پردازش می‌شوند» و --max-num-batched-tokens، یعنی «حداکثر تعداد توکن‌هایی که در یک تکرار قابل پردازش هستند». پارامتر اول، هم‌زمانی را محدود می‌کند. پارامتر دوم همان بودجه پیش‌پر کردن (prefill) تکه‌تکه است که پیش‌تر توضیح داده شد.

در شرایطی که کمتر از چهار درخواست در حال پردازش باشد، یا روی هر سیستمی که فاقد GPU پشتیبانی‌شده است، vLLM تنها پیچیدگی ایجاد می‌کند و بازدهی کمی دارد. این ابزار به یک کارت گرافیک با معماری CUDA نیاز دارد و در هنگام شروع، بخش عمده حافظه را اشغال می‌کند که برای یک VPS با 4 تا 8 گیگابایت رم، انتخاب اشتباهی است. در چنین مواردی، راهکار استفاده از یک مدل کوچک‌تر با کانتکست کوتاه‌تر و یک صف مدیریت‌شده توسط خودتان است. تفاوت Ollama و vLLM به عنوان موتورهای سرویس‌دهی این انتخاب را به‌طور کامل بررسی می‌کند و اجرای Qwen 3 8B روی یک VPS نشان می‌دهد که یک مدل با اندازه متوسط، پیش از اضافه کردن حتی یک کاربر اضافی، چه منابعی را طلب می‌کند.

موازنه‌ای که در باورهای رایج نادیده گرفته می‌شود

استفاده از Continuous batching باعث افزایش توان عملیاتی (throughput) کلی می‌شود و معمولاً تأخیر میانه (median latency) را نیز بهبود می‌بخشد، زیرا درخواست‌های موجود در صف زودتر پردازش می‌شوند. اما تأخیر دم (tail latency) در جهت مخالف حرکت می‌کند و این نیمه از ماجرا به‌ندرت مطرح می‌شود.

هر دنباله (sequence) اضافی در یک گام، مقدار کمی کار اضافه ایجاد می‌کند، بنابراین با پر شدن batch، مقدار ITL برای همه افزایش می‌یابد. مرحله prefill یک درخواست جدید، بخشی از زمان گامی را اشغال می‌کند که در غیر این صورت در اختیار کاربرانِ در حال استریم بود. تحت فشار حافظه (cache pressure)، زمان‌بند (scheduler) عملیات preemption را انجام می‌دهد که باعث می‌شود درخواستِ نیمه‌کاره به ابتدای مرحله prefill بازگردد.

رابط کاربری چت، مقادیر دم (tails) را نشان می‌دهد، نه میانگین‌ها را. استریمی که در میان جمله دو ثانیه متوقف می‌شود، حتی اگر زمان کل تکمیل آن مناسب باشد، از نظر کاربر «خراب» تلقی می‌شود. مقادیر p95 برای TTFT و p95 برای ITL را تحت بار کاری مورد انتظار خود اندازه‌گیری کنید و تعداد توکن در ثانیه (mean tokens per second) را صرفاً به عنوان عددی برای ظرفیت در نظر بگیرید، نه توصیفی از تجربه کاربری.

تنظیمات عملیاتی از همین‌جا نشأت می‌گیرند. میزان هم‌زمانی (concurrency) را کمی کمتر از حد مجاز حافظه محدود کنید تا موتور هرگز نیازی به preemption نداشته باشد. یک صف کوتاه و قابل‌پیش‌بینی، بسیار بهتر از یک batch بزرگ است که باعث ایجاد نوسان (thrashing) می‌شود؛ زیرا کاربری که چهار ثانیه منتظر می‌ماند و سپس محتوا را به‌صورت روان دریافت می‌کند، رضایت بیشتری نسبت به کاربری دارد که بلافاصله شروع می‌کند اما دو بار دچار وقفه می‌شود.

هنگام کندی عملکرد چه مواردی را بررسی کنیم

هر کاربر عادی است، اما زمان انتظار طولانی است. این یک مشکل صف است، نه مشکل سرعت. ابتدا تنظیمات موازی‌سازی (parallel) را بررسی کنید. مدل به‌درستی در حال سرویس‌دهی است، اما هر بار فقط یک درخواست را پردازش می‌کند.

خطای HTTP 503 از سمت Ollama. صف پر شده است. یا سرور واقعاً به حداکثر ظرفیت خود رسیده است، یا OLLAMA_MAX_QUEUE عمداً روی مقدار کمی تنظیم شده تا بار اضافی دفع شود؛ که این دقیقاً همان رفتاری است که انتظار می‌رود.

تعداد توکن در ثانیه هنگام بارگذاری روی سرور CPU به‌شدت افت می‌کند. در حین وقوع این مشکل، دستور vmstat 1 را اجرا کنید. مقادیر غیرصفر در ستون‌های si و so به این معناست که سیستم در حال استفاده از swap است؛ بنابراین وزن‌های مدل برای هر توکن از روی دیسک خوانده می‌شوند. هیچ تغییر پیکربندی‌ای این مشکل را حل نمی‌کند. اندازه مدل یا تعداد slotها را کاهش دهید.

از هر ده کاربر، یک نفر بسیار بیشتر از بقیه منتظر می‌ماند. در لاگ vLLM به دنبال preempted بگردید. دلیل معمول این اتفاق، Preemption و محاسبه مجدد (recompute) است؛ این یعنی حافظه کش برای طول context مجاز، بیش از حد اشغال شده است.

مقدار TTFT حتی زمانی که سرور بیکار است، نامطلوب است. این مشکل مربوط به prefill است، نه هم‌زمانی (concurrency). پرامپت‌های طولانی پیش از ظاهر شدن اولین توکن، زمان واقعی مصرف می‌کنند؛ بنابراین پیش از بررسی سخت‌افزار، اندازه پرامپت و قابلیت prefix caching را بررسی کنید.

FAQ

چرا وقتی نفر دوم از LLM شخصی من استفاده می‌کند، سرعت آن کاهش می‌یابد؟

در بیشتر موارد، سرعت کاهش نمی‌یابد، بلکه درخواست‌ها در صف قرار می‌گیرند. Ollama به‌صورت پیش‌فرض با OLLAMA_NUM_PARALLEL برابر با 1 عرضه می‌شود، بنابراین درخواست دوم منتظر می‌ماند تا درخواست اول آخرین توکن خود را تولید کند. برای تشخیص این دو حالت، زمان استریم یک کاربر را در حالی که دیگری منتظر است اندازه بگیرید: اگر نرخ توکن بر ثانیه آن‌ها پس از شروع عادی است، شما با صف مواجه هستید و افزایش تعداد پردازش موازی این مشکل را حل می‌کند. اگر هر دو استریم با نصف سرعت اجرا می‌شوند، شما در حال اشتراک‌گذاری پهنای باند حافظه هستید و این یک محدودیت سخت‌افزاری است.

یک GPU کوچک به چند کاربر همزمان می‌تواند سرویس‌دهی کند؟

به جای تعداد کاربران، حافظه را محاسبه کنید. ابتدا وزن‌های مدل، سپس حافظه پنهان KV که هزینه آن برابر است با 2 ضرب‌در تعداد لایه‌ها ضرب‌در تعداد سرهای کلید/مقدار ضرب‌در ابعاد سر ضرب‌در بایت، به ازای هر توکن و هر مکالمه فعال. یک مدل 8B معمولی با 36 لایه، 8 سر کلید/مقدار و ابعاد سر 128، حدود 144 KiB به ازای هر توکن در حالت 16-بیتی هزینه دارد، بنابراین یک مکالمه 8,192 توکنی تقریباً به 1.2 GB حافظه نیاز دارد. یک کارت 24 GB که مدل را در حالت 16-بیتی نگه می‌دارد، حدود 6 GB فضای خالی برای حافظه پنهان دارد که معادل پنج مکالمه با کانتکست کامل است، یا اگر کانتکست را کوتاه کنید، تعداد بیشتری خواهد بود.

آیا دسته‌بندی پیوسته (continuous batching) پاسخ هر کاربر را کندتر می‌کند؟

میانه تأخیر معمولاً بهبود می‌یابد، زیرا درخواست‌ها دیگر منتظر پایان یافتن کل یک دسته نمی‌مانند. اما تأخیر در صدک‌های بالا (tail latency) بدتر می‌شود. هر دنباله اضافی، کاری را به هر مرحله رمزگشایی اضافه می‌کند، پیش‌پر کردن (prefill) یک درخواست جدید بخشی از یک مرحله را از کاربران در حال استریم می‌گیرد و یک درخواست پیش‌دستانه (preempted) باید دو بار پیش‌پر شود. تأخیر بین توکن‌ها را در p95 اندازه بگیرید، نه میانگین؛ زیرا پنجره چت وقفه‌ها را به شکلی آشکار می‌کند که میانگین‌ها آن را پنهان می‌کنند.

آیا باید OLLAMA_NUM_PARALLEL را افزایش دهم یا به vLLM مهاجرت کنم؟

ابتدا تعداد پردازش موازی را افزایش دهید. این کار رایگان است و تنها به یک فایل پیکربندی نیاز دارد و مشکل رایج انتظار چهار نفر پشت یک پاسخ طولانی را حل می‌کند. محدودیت اصلی حافظه است: درخواست‌های موازی کانتکستی که باید نگه دارید را چند برابر می‌کنند، پس مراقب انتقال لایه‌ها به CPU باشید. زمانی به vLLM مهاجرت کنید که GPU با VRAM کافی داشته باشید و بیش از چهار درخواست به‌طور همزمان در حال اجرا باشند؛ زیرا در این نقطه است که حافظه پنهان صفحه‌بندی‌شده (paged cache) و زمان‌بندی به ازای هر توکن، کارایی بیشتری نسبت به هزینه‌اش ارائه می‌دهد.

آیا هسته‌های CPU بیشتر، سرعت سرور LLM را افزایش می‌دهد؟

نه برای بخشی که کاربران بیشترین توجه را به آن دارند. رمزگشایی برای هر توکن، کل مدل را از حافظه می‌خواند، بنابراین محدود به پهنای باند RAM است و پس از اشباع پهنای باند، هسته‌های اضافی کمکی نمی‌کنند. پیش‌پر کردن (prefill) با تعداد هسته‌ها مقیاس‌پذیر است، بنابراین هسته‌های بیشتر زمان رسیدن به اولین توکن را در پرامپت‌های طولانی کاهش می‌دهند. در یک VPS با 4 تا 8 GB رم، محدودیت اصلی معمولاً ظرفیت حافظه است و راهکار مؤثر، استفاده از مدل کوچک‌تر یا کانتکست کوتاه‌تر است، نه vCPU بیشتر.