چرا سرعت LLM خودمیزبان با افزایش کاربران کاهش مییابد؟
اگر LLM شما با 5 کاربر همزمان کند میشود، مشکل از سختافزار نیست. با تنظیم پارامتر num_parallel در Ollama و مدیریت KV cache و Batching، ظرفیت سرور خود را افزایش دهید.
چرا سرعت یک 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 گیگابایت وزن را به ازای هر توکن میخواند و اکثر واحدهای محاسباتی را بیکار میگذارد. با افزودن درخواست دوم، موتور همان 5 گیگابایت را یکبار میخواند و سپس دو توکن از آن محاسبه میکند. کاربر دوم تقریباً هیچ زمان اضافهای تحمیل نمیکند. سرویسدهی به درخواستها به صورت کاملاً متوالی، این مزیت را از بین میبرد.
دو عدد، تجربه کاربر را توصیف میکنند. TTFT (زمان تا اولین توکن) برابر است با زمان انتظار در صف به علاوه زمان Prefill. ITL (تأخیر بین توکنها) فاصله زمانی بین توکنهای استریمشده است و توسط مرحله Decode تعیین میشود. یک سرور کند معمولاً در یکی از این دو مورد ضعف دارد و راهحلهای آنها یکسان نیست. پیش از تغییر هر تنظیماتی، ارزش دارد مشخص کنید با کدامیک درگیر هستید و اندازهگیری جداگانه زمان Prefill و Decode روشی است که با آن به این نتیجه میرسید.
دستهبندی ایستا (Static batching) باعث میشود همه منتظر کندترین پاسخ بمانند
دستهبندی ایستا نسخه سادهانگارانه است و زمانی رخ میدهد که شما خودتان درخواستها را در کد برنامه گروهبندی کنید. موتور، N درخواست را جمعآوری کرده، آنها را با هم اجرا میکند و تا زمانی که طولانیترین تولید (generation) در گروه به پایان نرسد، تمام جایگاهها (slots) را اشغال نگه میدارد.
کاربری که درخواست خلاصهای 1,200 توکنی دارد، چهار پاسخ تکخطی را در دسته قفل میکند، زیرا دسته تا زمانی که کندترین عضو آن تمام نشود، هیچ جایگاهی را آزاد نمیکند.
این کار دو هزینه به همراه دارد. توالیهای تکمیلشده همچنان جایگاههایی را اشغال میکنند که هیچ پردازش مفیدی انجام نمیدهند؛ بنابراین با تغییر طول خروجیها، نرخ عملیاتی (throughput) کاهش مییابد و طول خروجیهای چت بسیار متغیر است. درخواستی که یک گام پس از تشکیل دسته برسد، باید منتظر بماند تا کل دسته تخلیه شود و سپس تازه مرحله prefill آن آغاز گردد؛ این یعنی TTFT (زمان رسیدن به اولین توکن) آن درخواست، توسط مقالهٔ طولانی شخص دیگری تعیین میشود.
دستهبندی پیوسته (Continuous batching) درخواستها را در هر توکن میپذیرد و خاتمه میدهد
دستهبندی پیوسته، زمانبندی را در سطح یک گام رمزگشایی (decoding step) انجام میدهد. پس از هر گام، زمانبند دنبالههایی را که بهتازگی توکن پایان (stop token) خود را منتشر کردهاند حذف میکند و سپس درخواستهای در انتظار را در اسلاتهای خالی میپذیرد. پاسخی که در گام 40 به پایان میرسد، اسلات خود را در همان گام 40 آزاد میکند، نه در پایان یک دسته (batch).
این موضوع عجیب و غریب نیست. llama-server در -cb, --cont-batching مستند کرده است که این قابلیت «آیا دستهبندی پیوسته (که با نام دستهبندی پویا نیز شناخته میشود) فعال شود یا خیر (پیشفرض: فعال)» است و vLLM بر پایه همین ایده ساخته شده است. Ollama نیز درخواستهای موازی را سرویسدهی میکند. مقدار پیشفرض فقط تعداد را روی 1 محدود میکند؛ به همین دلیل است که بسیاری از افراد به این نتیجه میرسند که سختافزارشان توانایی اجرای همزمان (concurrency) را ندارد، در حالی که پیکربندی آنها بوده که این اجازه را نداده است.
نتایج منتشرشده از دستهبندی پیوسته معمولاً روی کارتهای دیتاسنتر اندازهگیری میشوند که هم توان پردازشی مازاد دارند و هم دهها گیگابایت حافظه برای کش (cache) در اختیار دارند. شکل آن نتایج در سیستم شما نیز صدق میکند، اما اندازه آنها خیر؛ و بخش حافظه در ادامه، دلیل این تفاوت را توضیح میدهد.
رقابت Prefill با Decode برای منابع پردازشی یکسان
هنگامی که یک درخواست جدید در حین استریم شدن چهار پاسخ دیگر وارد میشود، ابتدا باید Prompt آن Prefill شود و عملیات Prefill از نظر پردازشی سنگین است. اگر زمانبند (scheduler) برای این Prefill یک گام اختصاصی در نظر بگیرد، آن چهار کاربری که در حال دریافت پاسخ هستند، در طول این مدت هیچ توکنی دریافت نخواهند کرد. در مورد یک Prompt طولانی، این وقفه در تمام پنجرههای باز قابل مشاهده است. این همان لرزشی (stutter) است که کاربران هنگام توصیف سکتههای سرور (hiccups) در لحظه ارسال درخواست توسط دیگران، به آن اشاره میکنند.
قابلیت 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 استفاده میکند که به جای محاسبه مجدد برای هر درخواست، کش را برای یک پیشوند مشترک در Prompt بازاستفاده میکند.
حافظهای که زودتر از همه تمام میشود، KV cache است
هر توکن در هر گفتگوی فعال، یک بردار کلید (key vector) و یک بردار مقدار (value vector) در هر لایه از مدل باقی میگذارد. این همان KV cache (حافظه پنهان کلید/مقدار) است و همان چیزی است که به فرایند رمزگشایی (decode) اجازه میدهد از محاسبه مجدد کل پرامپت برای هر توکن جدید اجتناب کند. اندازه آن برای هر توکن توسط ساختار مدل تعیین میشود: 2 (یک کلید، یک مقدار) ضربدر تعداد لایهها، ضربدر تعداد سرهای کلید/مقدار، ضربدر ابعاد سر، ضربدر تعداد بایتهای هر مقدار. این اعداد را از config.json مدل استخراج کنید.
یک بار این محاسبه را انجام دهید تا سقف حافظه دیگر یک معما نباشد. یک مدل معمولی 8B با 36 لایه، 8 سر کلید/مقدار و ابعاد سر 128، که حافظه پنهان را با دقت 16-bit نگه میدارد، به ازای هر توکن 2 36 8 128 2 بایت هزینه دارد. این مقدار برابر با 147,456 بایت یا حدود 144 KiB است. بنابراین، یک گفتگوی 8,192 توکنی به حدود 1.2 گیگابایت حافظه پنهان نیاز دارد. پنج گفتگو از این نوع، علاوه بر وزنهای مدل، به حدود 6 گیگابایت حافظه نیاز دارند و این پاسخ واقعی به این پرسش است که چه تعداد کاربر در سیستم جای میگیرند.
همروندی (concurrency) باعث افزایش context میشود و ابزارها نیز صراحتاً به این موضوع اشاره میکنند. در FAQ مربوط به Ollama آمده است: «پردازش موازی درخواستها برای یک مدل مشخص، منجر به افزایش اندازه context به میزان تعداد درخواستهای موازی میشود. برای مثال، یک context با اندازه 2K و 4 درخواست موازی، منجر به یک context با اندازه 8K و تخصیص حافظه اضافی خواهد شد.» رم مورد نیاز با OLLAMA_NUM_PARALLEL ضربدر OLLAMA_CONTEXT_LENGTH مقیاسپذیر است. در llama-server، contextای که با -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 است؛ بنابراین درخواست اخراج شده، حافظه پنهان خود را دور میریزد و هنگام پذیرش مجدد، دوباره فرایند prefill را انجام میدهد. این کار دو بار انجام میشود. مستندات هشدار میدهند که «پیشگیری و محاسبه مجدد میتواند بر تأخیر نهایی (end-to-end latency) تأثیر منفی بگذارد» و این خط لاگ، بهترین توضیح برای این است که چرا یک کاربر بدشانس بسیار طولانیتر از بقیه منتظر مانده است، در حالی که میانگین عملکرد شما مناسب به نظر میرسد. برای ثبت تعداد تجمعی، disable_log_stats=False را تنظیم کنید یا شمارنده پیشگیری را از معیارهای Prometheus که vLLM ارائه میدهد، بخوانید.
تغییرات در تعداد 2، 5 و 20 کاربر همزمان
دو کاربر. این وضعیت روی یک GPU که حافظه کش مازاد دارد تقریباً نامحسوس است، زیرا جریان رمزگشایی (decode) دوم با صرف زمان بسیار کمی در کنار جریان اول اجرا میشود. در یک VPS مبتنی بر CPU با 4 تا 8 گیگابایت رم، این کار بدون هزینه نیست: هر دو جریان از تعداد محدودی vCPU و پهنای باند رم یکسانی استفاده میکنند، بنابراین هر کاربر تقریباً نصف توکن در ثانیه دریافت میکند و تقاضا برای حافظه کش در برابر بودجهای بسیار محدودتر، دو برابر میشود.
پنج کاربر. اینجاست که تنظیمات پیشفرض دیگر کافی نیستند و مشکل به صورت یک صف (queue) بروز میکند. با OLLAMA_NUM_PARALLEL برابر با 1، چهار نفر منتظر کسی میمانند که پاسخ طولانیتری درخواست کرده است، و هر کدام از آنها به محض رسیدن نوبتشان، سرعت عادی را تجربه میکنند. اگر تعداد پردازش موازی را افزایش دهید، ماهیت مشکل تغییر میکند: پنج اسلات با 8K کانتکست برای هر کدام، به معنای یافتن فضایی برای 40K توکن در حافظه کش است. اگر این مقدار در VRAM جا نشود، موتور لایهها را به رم سیستم منتقل میکند (offload)، و اگر در رم هم جا نشود، سیستم شروع به swap میکند و سرعت تولید توکن در ثانیه بهشدت افت میکند.
بیست کاربر. بیست انسان در یک رابط کاربری چت معمولاً به معنای بیست درخواست همزمان نیستند، و این مهمترین نکتهای است که باید پیش از خرید سختافزار درک کرد. یک فرد پاسخ را میخواند و بین هر نوبت، 20 تا 60 ثانیه فکر میکند، بنابراین بیشتر زمان نشست (session) آنها در حالت بیکار (idle) سپری میشود. بیست عامل (agent) یا بیست کار خلاصهسازی اسناد، بیست جریان واقعی بدون هیچگونه زمان بیکاری هستند. این یک ماشین متفاوت میطلبد. یک توسعهدهندهای که یک عامل کدنویسی را به سمت سرور Ollama خود هدایت کرده است، به حالت دوم نزدیکتر است تا حالت اول؛ زیرا عامل تا زمانی که کار در حال اجراست، درخواستها را پشت سر هم ارسال میکند و هیچکدام از وقفههای مطالعه که یک انسان دارد را باقی نمیگذارد.
آیا کاربران شما همزمان فعال هستند یا فقط لاگین کردهاند؟
پیش از تعیین هرگونه ابعاد و ظرفیت، تعداد درخواستهای در حال پردازش (in flight) را محاسبه کنید. محاسبات ساده است: تعداد درخواستهای در حال پردازش برابر است با تعداد کاربران، ضربدر ثانیههای صرفشده برای تولید متن در هر نوبت، تقسیم بر فاصله زمانی بین نوبتها.
- ابتدا سرعت تکجریانی (single-stream) خود را اندازه بگیرید؛ هم برای prefill و هم برای decode. از اعداد کارتهای دیگران استفاده نکنید: تعداد توکن بر ثانیه را روی سیستم خودتان اندازه بگیرید و از همان عدد استفاده کنید.
- چرخه کاری (duty cycle) را تخمین بزنید. بیست کاربر چت، 12 ثانیه زمان تولید در هر نوبت، و یک نوبت در هر 90 ثانیه، نتیجه میدهد: 20 * 12 / 90، که حدود 2.7 درخواست در حال پردازش است.
- تعداد slotها را کمی بیشتر از این مقدار تنظیم کنید و سپس آن را با حافظه تطبیق دهید: حاصلضرب تعداد slotها در context هر درخواست، باید در حافظه کش توکنهای موجود شما جای بگیرد.
- صف را کوتاه نگه دارید تا سرریز (overflow) بهسرعت و بهطور واضح منجر به شکست شود.
توکنهای کش در دسترس، همان حافظه آزاد پس از بارگذاری وزنها (weights) است که بر هزینه هر توکن (از بخش قبل) تقسیم میشود. یک کارت 24 GB که یک مدل 8B را با دقت 16-bit اجرا میکند، حدود 16 GB را صرف وزنها کرده و تقریباً 6 GB حافظه کش قابلاستفاده در میزان بهرهوری پیشفرض دارد که معادل حدود پنج مکالمه 8K است. برای جای دادن تعداد بیشتر، context هر درخواست را کوتاه کنید یا کش را با فرمت 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 pssystemctl show باید سه متغیری که بهتازگی تنظیم کردهاید را نمایش دهد. اگر اینطور نیست، فایل drop-in ذخیره نشده است و سایر اقدامات شما بیاثر خواهند بود. ollama ps سپس مدل بارگذاریشده را با اندازهای بزرگتر از وزنهای خالص نمایش میدهد، زیرا چهار اسلات با 8,192 توکن، 32,768 توکن حافظه کش را در کنار خود رزرو میکنند. اگر ستون PROCESSOR بخشی از مدل را روی CPU نشان میدهد در حالی که انتظار داشتید تمام آن روی GPU باشد، یعنی حافظه کش بیشتری نسبت به ظرفیت کارت گرافیک درخواست کردهاید. یکی از این دو عدد را کاهش دهید. کاهش context معمولاً گزینه امنتری است، اما پنجرهای که بیش از حد کوچک باشد، بدون اعلام خطا، پرامپتهای طولانی را بهطور خودکار کوتاه میکند؛ بنابراین بهتر است اندازه num_ctx را آگاهانه تعیین کنید تا اینکه صرفاً آن را تا زمان جایگیری مدل کاهش دهید.
مقدار پیشفرض صف (queue) نیاز به بررسی دقیقتری دارد. Ollama تا OLLAMA_MAX_QUEUE درخواست را در صف قرار میدهد و «مقدار پیشفرض 512 است». پس از آن، سرور با «خطای 503 که نشاندهنده بارگذاری بیش از حد سرور است» پاسخ میدهد. یک صف با عمق 512 روی سیستمی که همزمان چهار درخواست را پردازش میکند، وعدهای است که نمیتوانید به آن عمل کنید، زیرا کلاینتی که در جایگاه 300 قرار دارد، بسیار پیش از رسیدن نوبتش دچار timeout میشود. یک صف کوتاه، خطایی را برمیگرداند که برنامه شما میتواند آن را دوباره تلاش (retry) یا گزارش کند؛ این وضعیت بسیار بهتر از یک آیکون بارگذاری است که هرگز به نتیجه نمیرسد.
آن را بهصورت واقعی تست کنید. دو درخواست را در یک لحظه از دو ترمینال ارسال کنید و هر دو را زیر نظر بگیرید. اگر درخواست دوم تا زمانی که اولی تمام نشود هیچ خروجی تولید نکند، تنظیمات موازیسازی اعمال نشده است.
زمانی که یک موتور سرویسدهی واقعی به سوددهی میرسد
زمانی که یک GPU با فضای خالی دارید و بیش از حدود 4 درخواست بهطور همزمان در حال پردازش هستند، vLLM ارزش پیچیدگی راهاندازی خود را نشان میدهد. زمانبند (scheduler) این ابزار بر اساس توکن عمل میکند، حافظه کش آن صفحهبندی (paged) شده است تا قطعات آزاد مجدداً استفاده شوند و VRAM اضافی را بهجای بلااستفاده ماندن، به ظرفیت همزمانی تبدیل میکند. تا اوت 2026، نصب و راهاندازی مستندشده شامل دو دستور زیر است:
uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instructcurl 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 یک درخواست تازه وارد، بخشی از زمان گامی را اشغال میکند که در حالت عادی در اختیار کاربران در حال دریافت stream بود. تحت فشار حافظه (cache pressure)، زمانبند (scheduler) عملیات preemption را انجام میدهد که باعث میشود درخواستِ نیمهکاره به ابتدای مرحله prefill بازگردد.
رابط کاربری چت، صدکهای بالا (tails) را نشان میدهد، نه میانگینها را. جریانی که در میانهٔ جمله برای دو ثانیه متوقف میشود، حتی اگر زمان کل تکمیل آن مناسب باشد، از دید کاربر «خراب» تلقی میشود. شاخصهای p95 TTFT و p95 ITL را تحت بار کاری مورد انتظار خود اندازهگیری کنید و میانگین توکن در ثانیه را صرفاً به عنوان عددی برای ظرفیت در نظر بگیرید، نه توصیفی از تجربه کاربری.
تنظیمات عملیاتی از همینجا نشأت میگیرند. میزان همزمانی (concurrency) را کمی کمتر از حد مجاز حافظه محدود کنید تا موتور هرگز مجبور به preemption نشود. یک صف کوتاه و قابلپیشبینی، بهتر از یک batch بزرگ است که باعث ایجاد نوسان (thrashing) میشود؛ زیرا کاربری که چهار ثانیه منتظر میماند و سپس محتوا را بهصورت روان دریافت میکند، رضایت بیشتری نسبت به کاربری دارد که بلافاصله شروع میکند اما دو بار دچار وقفه میشود.
هنگام کندی عملکرد چه مواردی را بررسی کنیم
هر کاربر عادی است، اما زمان انتظار طولانی است. این یک مشکل صف است، نه مشکل سرعت. ابتدا تنظیمات موازیسازی (parallel) را بررسی کنید. مدل بهدرستی در حال سرویسدهی است، اما هر بار فقط یک درخواست را پردازش میکند.
خطای HTTP 503 از سمت Ollama. صف پر شده است. یا سرور واقعاً به حداکثر ظرفیت خود رسیده است، یا OLLAMA_MAX_QUEUE عمداً روی مقدار کمی تنظیم شده تا بار اضافی دفع شود؛ که این دقیقاً همان رفتاری است که انتظار میرود.
تعداد توکن در ثانیه هنگام بارگذاری روی سرور CPU بهشدت افت میکند. در حین وقوع این مشکل، دستور vmstat 1 را اجرا کنید. مقادیر غیرصفر در ستونهای si و so نشاندهنده این است که سیستم در حال استفاده از swap است؛ بنابراین وزنهای مدل در هر توکن از روی دیسک خوانده میشوند. هیچ تغییر پیکربندیای این مشکل را حل نمیکند. اندازه مدل یا تعداد اسلاتها را کاهش دهید.
از هر ده کاربر، یک نفر بسیار بیشتر از بقیه منتظر میماند. در لاگ vLLM به دنبال preempted بگردید. پیشخرید (Preemption) و محاسبه مجدد آن معمولاً دلیل این اتفاق است و به این معناست که حافظه کش برای طول متنی که مجاز دانستهاید، بیش از حد اشغال شده است.
زمان TTFT حتی در حالت بیکار بودن سرور نیز بد است. این مشکل مربوط به prefill است، نه همزمانی (concurrency). پرامپتهای طولانی پیش از ظاهر شدن اولین توکن، زمان واقعی مصرف میکنند؛ بنابراین پیش از بررسی سختافزار، اندازه پرامپت و کش کردن پیشوند (prefix caching) را بررسی کنید. اگر این انتظار طولانی فقط برای اولین نفری که پس از یک دوره سکوت درخواست میدهد رخ میدهد و بقیه کاربران مشکلی ندارند، این اصلاً مربوط به prefill نیست؛ بلکه Ollama در حال تخلیه مدل از حافظه و خواندن مجدد وزنها از روی دیسک است که ارزش دارد با نگه داشتن مدل در حافظه بین درخواستها آن را رفع کنید.
FAQ
چرا وقتی نفر دوم از LLM شخصی من استفاده میکند، سرعت آن کاهش مییابد؟
در بیشتر موارد، سرعت اصلاً کاهش نمییابد، بلکه درخواستها در صف قرار میگیرند. Ollama بهصورت پیشفرض با OLLAMA_NUM_PARALLEL روی 1 عرضه میشود، بنابراین درخواست دوم منتظر میماند تا درخواست اول آخرین توکن خود را تولید کند. برای تشخیص این دو حالت، زمان استریم یک کاربر را در حالی که کاربر دیگری منتظر است اندازهگیری کنید: اگر نرخ توکن بر ثانیه آنها پس از شروع عادی باشد، شما با صف مواجه هستید و افزایش تعداد پردازش موازی مشکل را حل میکند. اگر هر دو استریم با نصف سرعت اجرا میشوند، شما واقعاً در حال اشتراکگذاری پهنای باند حافظه هستید و این یک محدودیت سختافزاری است.
یک GPU کوچک به چند کاربر همزمان میتواند سرویسدهی کند؟
به جای تعداد کاربران، حافظه را محاسبه کنید. ابتدا وزنها (Weights) و سپس KV cache را در نظر بگیرید که هزینه آن برابر است با: 2 ضربدر تعداد لایهها ضربدر تعداد سرهای کلید/مقدار (key/value heads) ضربدر ابعاد سر (head dimension) ضربدر بایت، به ازای هر توکن و هر مکالمه فعال. یک مدل 8B معمولی با 36 لایه، 8 سر کلید/مقدار و ابعاد سر 128، در حالت 16-bit حدود 144 KiB به ازای هر توکن هزینه دارد، بنابراین یک مکالمه با 8,192 توکن تقریباً به 1.2 GB حافظه نیاز دارد. یک کارت 24 GB که آن مدل را در حالت 16-bit نگه میدارد، حدود 6 GB فضای خالی برای کش دارد که برای حدود پنج مکالمه با context کامل کافی است، یا اگر context را کوتاه کنید، این تعداد بیشتر میشود.
آیا continuous batching پاسخدهی به هر کاربر را کندتر میکند؟
معمولاً میانگین تأخیر (median latency) بهبود مییابد، زیرا درخواستها دیگر منتظر پایان یافتن کل یک batch نمیمانند. اما تأخیر در صدکهای بالا (tail latency) بدتر میشود. هر دنباله (sequence) اضافی، کاری را به هر مرحله رمزگشایی (decoding step) اضافه میکند، پیشپردازش (prefill) یک درخواست جدید بخشی از یک مرحله را از کاربران در حال استریم میگیرد و یک درخواست پیشدستانه (preempted) باید دو بار prefill شود. به جای میانگین، تأخیر بین توکنها در p95 را اندازهگیری کنید، زیرا در پنجره چت، وقفهها به شکلی آشکار میشوند که میانگینها آنها را پنهان میکنند.
آیا باید OLLAMA_NUM_PARALLEL را افزایش دهم یا به vLLM مهاجرت کنم؟
ابتدا تعداد پردازش موازی را افزایش دهید. این کار رایگان است، با یک فایل drop-in انجام میشود و مشکل رایج انتظار چهار نفر پشت یک پاسخ طولانی را حل میکند. محدودیت اصلی حافظه است: درخواستهای موازی، context مورد نیاز شما را چند برابر میکنند، پس مراقب انتقال لایهها به CPU باشید. زمانی به vLLM مهاجرت کنید که GPU با VRAM مازاد دارید و بیش از چهار درخواست واقعاً در حال اجرا هستند؛ چرا که در این نقطه، paged cache و زمانبندی به ازای هر توکن، بازدهی بیشتری نسبت به هزینهشان دارند.
آیا هستههای CPU بیشتر، سرعت سرور LLM را افزایش میدهد؟
نه برای بخشی که کاربران بیشترین توجه را به آن دارند. رمزگشایی (Decode) برای هر توکن، کل مدل را از حافظه میخواند، بنابراین محدود به پهنای باند RAM است و پس از اشباع پهنای باند، هستههای اضافی کمکی نمیکنند. پیشپردازش (Prefill) با تعداد هستهها مقیاسپذیر است، بنابراین هستههای بیشتر، زمان رسیدن به اولین توکن در promptهای طولانی را کاهش میدهند. در یک VPS با 4 تا 8 GB رم، محدودیت اصلی معمولاً ظرفیت حافظه است و راهکار مؤثر، استفاده از مدل کوچکتر یا context کوتاهتر است، نه vCPU بیشتر.