لماذا يتباطأ خادم LLM عند 5 مستخدمين؟
يعمل مستخدم واحد جيداً، لكن خمسة يتباطأون. تعرّف إلى أثر batching وKV cache وprefill وعمق الطابور، ولماذا تعالج Ollama طلباً واحداً افتراضياً.
لماذا يتباطأ نموذج اللغة (LLM) المستضاف ذاتياً عند زيادة عدد المستخدمين؟
عندما يتوقف نموذج لغة مستضاف ذاتياً عن الاستجابة عند وصول 5 مستخدمين متزامنين، فالسبب المعتاد هو ضبط الخادم على توليد رد واحد في كل مرة، بينما ينتظر الأربعة الآخرون في طابور. توضح وثائق Ollama صراحةً بشأن إعدادها الافتراضي: OLLAMA_NUM_PARALLEL هو "الحد الأقصى لعدد الطلبات المتوازية التي سيعالجها كل نموذج في نفس الوقت، والقيمة الافتراضية هي 1". لا يوجد عطل في النظام، وبرمجيات الخدمة قادرة على العمل على عدة طلبات في آن واحد. أربعة من مستخدميك الخمسة ينتظرون دورهم لأن الإعدادات فرضت ذلك.
الإصلاح نادراً ما يكون في الحصول على خادم أقوى. الحل يكمن في محرك خدمة يدفع طلبات متعددة عبر النموذج في نفس تمريرة المعالجة (forward pass)، بالإضافة إلى توفير ذاكرة كافية للاحتفاظ بمحادثات الجميع أثناء المعالجة. كلا الجزأين مهمان، والجزء الثاني هو ما يحدد فعلياً سقف قدرة خادمك.
المرحلتان اللتان يمر بهما كل طلب
تقرأ مرحلة الـPrefill كامل المطالبة (prompt) دفعة واحدة وتبني ذاكرة التخزين المؤقت للانتباه (attention cache) الخاصة بها. يمر كل رمز (token) في المطالبة عبر النموذج معاً، لذا فإن الـPrefill عبارة عن عملية ضرب مصفوفات كبيرة، وتكون محدودة بسرعة الحسابات الحسابية. أما مرحلة الـDecode فتكتب الإجابة رمزاً واحداً في كل مرة. يحتاج كل رمز إلى قراءة أوزان النموذج كاملة من الذاكرة مجدداً، بينما تكون العمليات الحسابية التي تُجرى على ذلك الرمز الواحد ضئيلة جداً. لذا، فإن الـDecode محدودة بعرض نطاق الذاكرة (memory bandwidth).
هذا التباين هو السبب الجوهري لفعالية التجميع (batching). فعملية الـDecode لمستخدم واحد تقرأ، على سبيل المثال، 5 GB من الأوزان لكل رمز وتترك معظم وحدات الحساب خاملة. عند إضافة طلب ثانٍ، يقرأ المحرك نفس الـ5 GB مرة واحدة، ثم يحسب رمزين منها. لا يستهلك المستخدم الثاني أي وقت إضافي تقريباً. إن معالجة الطلبات واحداً تلو الآخر بشكل صارم تهدر هذه الميزة.
هناك رقمان يصفان تجربة المستخدم. الـTTFT (الوقت المستغرق للحصول على الرمز الأول) هو وقت الانتظار في الطابور مضافاً إليه وقت الـPrefill. أما الـITL (زمن الانتقال بين الرموز) فهو الفجوة بين الرموز المتدفقة، ويتم تحديده بواسطة الـDecode. عادة ما يكون الخادم البطيء بطيئاً في إحدى هاتين المرحلتين، والحلول لكل منهما مختلفة. من الضروري تحديد أي منهما تواجه قبل تغيير أي إعدادات، وقياس الـPrefill والـDecode بشكل منفصل هي الطريقة التي تكتشف بها ذلك.
التجميع الثابت يجعل الجميع ينتظرون أبطأ استجابة
التجميع الثابت هو النسخة البدائية، وهو ما تحصل عليه إذا قمت بتجميع الطلبات بنفسك في كود التطبيق. يجمع المحرك N من الطلبات، ويشغلها معاً، ويحتفظ بكل خانة حتى تنتهي أطول عملية توليد في المجموعة.
مستخدم يطلب ملخصاً بطول 1,200 رمز يبقي أربع إجابات من سطر واحد محتجزة في الدفعة، لأن الدفعة لا تحرر أي خانة حتى ينتهي أبطأ عضو فيها.
ينتج عن ذلك تكلفتان. تستمر التسلسلات المنتهية في شغل خانات لا تحسب أي شيء مفيد، لذا تنخفض الإنتاجية الفعلية مع تباين أطوال المخرجات، وأطوال مخرجات الدردشة تتباين كثيراً. الطلب الذي يصل بعد خطوة واحدة من تشكيل الدفعة ينتظر حتى تفرغ الدفعة بالكامل قبل أن يبدأ حتى في مرحلة التعبئة المسبقة (prefill)، مما يعني أن زمن الوصول إلى الرمز الأول (TTFT) الخاص به يتحدد بناءً على مقال شخص آخر.
تسمح المعالجة الدفعية المستمرة (Continuous batching) بقبول الطلبات وإنهائها مع كل رمز (token)
تجدول المعالجة الدفعية المستمرة العمليات على مستوى خطوة فك التشفير الواحدة. بعد كل خطوة، يُسقط المجدول التسلسلات التي أصدرت للتو رمز التوقف الخاص بها، ثم يقبل الطلبات المنتظرة في الفتحات الشاغرة. الرد الذي ينتهي عند الخطوة 40 يحرر فتحته عند الخطوة 40، وليس في نهاية الدفعة.
هذا ليس أمراً غريباً. توثق llama-server الميزة -cb, --cont-batching على أنها "تحدد ما إذا كان سيتم تمكين المعالجة الدفعية المستمرة (المعروفة أيضاً بالمعالجة الدفعية الديناميكية) (الافتراضي: ممكّن)"، وتدرج "فك التشفير المتوازي مع دعم تعدد المستخدمين" ضمن ميزاتها، ومنذ دمج تغيير في نوفمبر 2025 تبدأ بـ 4 فتحات افتراضياً (حيث تضبط -np افتراضياً على -1، أي تلقائي). صُممت vLLM حول هذه الفكرة. كما تخدم Ollama الطلبات المتوازية أيضاً. القيمة الافتراضية تحدد العدد بواحد فقط، وهذا هو السبب في استنتاج الكثيرين أن أجهزتهم لا تدعم التزامن، بينما كان إعدادهم هو الذي يمنع ذلك.
عادةً ما تُقاس نتائج المعالجة الدفعية المستمرة المنشورة على بطاقات مراكز البيانات التي تمتلك فائضاً في قدرة الحوسبة وعشرات الجيجابايت لذاكرة التخزين المؤقت. شكل تلك النتائج ينطبق على جهازك، لكن حجمها لا ينطبق، وقسم الذاكرة أدناه يوضح السبب.
تتنافس مرحلة التعبئة المسبقة (Prefill) مع فك التشفير (Decode) على نفس موارد الحوسبة
عند وصول طلب جديد بينما يجري بث أربعة ردود، يجب أولاً إجراء تعبئة مسبقة (prefill) للمطالبة (prompt) الخاصة به، وتستهلك هذه العملية موارد حوسبة مكثفة. إذا خصص المجدول خطوة مستقلة لهذه التعبئة، فلن يتلقى المستخدمون الأربعة الذين يشاهدون البث أي رمز (token) خلال تلك الفترة. في حال كانت المطالبة طويلة، سيظهر توقف ملحوظ في كل نافذة مفتوحة. هذا هو التلعثم الذي يقصده المستخدمون عندما يقولون إن الخادم "يتعثر" في كل مرة يرسل فيها شخص آخر طلباً جديداً.
تعمل التعبئة المسبقة المجزأة (Chunked prefill) على تقسيم المطالبة الطويلة إلى أجزاء، ودمج كل جزء في نفس خطوة فك التشفير الجارية. يوضح دليل ضبط vLLM المقايضة بشكل مباشر: ميزانيات الأجزاء الأصغر "تحقق زمن استجابة أفضل لكل رمز (ITL) لأن عمليات التعبئة المسبقة التي تبطئ فك التشفير تصبح أقل"، بينما القيم الأعلى "تحقق وقتاً أفضل للرمز الأول (TTFT) حيث يمكنك معالجة المزيد من رموز التعبئة المسبقة في دفعة واحدة". أنت هنا تختار تجربة من تريد حمايتها: الشخص الذي ينتظر بدء الرد، أم الأشخاص الذين يشاهدون تدفق النص.
يحدد طول المطالبة مدى تأثير ذلك. فالمطالبة التي تتكون من 6,000 رمز مع إجابة من 200 رمز تعني 6,000 رمز من عمل التعبئة المسبقة مقابل 200 خطوة فك تشفير. تدفعك المحادثات المعززة بالاسترجاع (RAG) والمطالبات النظامية الطويلة إلى هذا النطاق، حيث تتوقف التعبئة المسبقة عن كونها مجرد هامش خطأ وتصبح هي العائق الذي ينتظره المستخدمون. يساعد التخزين المؤقت للبادئة (Prefix caching) عندما يتكرر الجزء الطويل: يوفر vLLM الميزة --enable-prefix-caching، التي تعيد استخدام الذاكرة المؤقتة لبادئة مطالبة مشتركة بدلاً من إعادة حسابها لكل طلب.
الذاكرة التي تنفد أولاً هي ذاكرة التخزين المؤقت KV
يترك كل رمز (token) في أي محادثة نشطة متجه مفتاح (key vector) ومتجه قيمة (value vector) في كل طبقة من طبقات النموذج. هذا هو ما يُعرف بـ KV cache (ذاكرة التخزين المؤقت للمفاتيح والقيم)، وهو ما يسمح لعملية فك التشفير (decode) بتجنب إعادة حساب كامل المطالبة (prompt) لكل رمز جديد. عدد القيم المخزنة مؤقتاً لكل رمز ثابت وفقاً لهيكل النموذج: 2 (مفتاح واحد وقيمة واحدة) مضروباً في عدد الطبقات، ومضروباً في عدد رؤوس المفاتيح/القيم، ومضروباً في أبعاد الرأس. اقرأ هذه الأرقام من config.json الخاص بالنموذج. اضربها في عدد البايتات لكل قيمة للحصول على الحجم الإجمالي، وهذا العامل الأخير هو إعداد وقت التشغيل وليس جزءاً من النموذج: يقوم llama-server بتخزين ذاكرة التخزين المؤقت بصيغة f16 (بايتان) افتراضياً، ويقبل أنواعاً أصغر مثل q8_0 وصولاً إلى -ctk و-ctv.
احسبها مرة واحدة وسيتوقف السقف عن كونه لغزاً. نموذج نموذجي بحجم 8B يحتوي على 36 طبقة، و8 رؤوس مفاتيح/قيم، وأبعاد رأس تبلغ 128، مع تخزين الذاكرة المؤقتة بصيغة 16-بت، يتطلب 2 36 8 128 2 بايت لكل رمز. هذا يعادل 147,456 بايت، أي حوالي 144 KiB. لذا، فإن محادثة واحدة بطول 8,192 رمزاً تحتاج إلى حوالي 1.2 GB من ذاكرة التخزين المؤقت. خمس محادثات من هذا النوع تحتاج إلى حوالي 6 GB، بالإضافة إلى أوزان النموذج، وهذه هي الإجابة الحقيقية حول عدد المستخدمين الذين يمكن استيعابهم.
التزامن يضاعف السياق، والأدوات تصرح بذلك بوضوح. تنص أسئلة FAQ الخاصة بـ Ollama على: "تؤدي معالجة الطلبات المتوازية لنموذج معين إلى زيادة حجم السياق بمقدار عدد الطلبات المتوازية. على سبيل المثال، سياق بحجم 2K مع 4 طلبات متوازية سيؤدي إلى سياق بحجم 8K وتخصيص ذاكرة إضافي." تتناسب ذاكرة الوصول العشوائي (RAM) المطلوبة مع OLLAMA_NUM_PARALLEL مضروباً في OLLAMA_CONTEXT_LENGTH. في llama-server، يقوم عدد الفتحات (slots) التلقائي الافتراضي بتشغيل كل فتحة على مخزن مؤقت موحد لـ KV، لذا فإن كامل سياق -c هو تجمع واحد تسحب منه الفتحات. إذا قمت بضبط -np بنفسك، فسيتم إيقاف المخزن المؤقت الموحد ما لم تقم أيضاً بتمرير --kv-unified، وبالتالي يتم تقسيم السياق الذي تطلبه عبر -c على -np من الفتحات، وزيادة عدد الفتحات بمفردها تقلل ما يمكن لكل طلب الاحتفاظ به. اقرأ سياق كل فتحة من سجل بدء التشغيل بدلاً من افتراضه.
بدلاً من ذلك، يقوم vLLM بالتخصيص المسبق. --gpu-memory-utilization (الافتراضي 0.92) هو "جزء ذاكرة GPU الذي سيتم استخدامه لمشغل النموذج". ما لم تقم بضبط --kv-cache-memory-bytes، يستنتج vLLM حجم ذاكرة KV المؤقتة من ذلك الجزء: أي مساحة متبقية في الميزانية بعد حساب الأوزان وذاكرة العمل التي حددها المحرك تصبح تجمع KV المصفح (paged KV pool)، وعندما ينفد هذا التجمع، يقوم المجدول بطرد طلب بدلاً من فشله:
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، وضع الطرد الافتراضي هو RECOMPUTE، لذا يتخلص الطلب المطرود من ذاكرته المؤقتة ويعيد ملء البيانات (prefill) مرة أخرى عند إعادة قبوله. يتم تنفيذ هذا العمل مرتين. تحذر الوثائق من أن "الطرد وإعادة الحساب يمكن أن يؤثرا سلباً على زمن الاستجابة الكلي"، وهذا السطر في السجل هو أفضل تفسير لسبب انتظار مستخدم سيئ الحظ لفترة أطول بكثير من الآخرين بينما كان متوسط الأداء يبدو طبيعياً. اضبط disable_log_stats=False لتسجيل العدد التراكمي، أو اقرأ عداد الطرد من مقاييس Prometheus التي يكشف عنها vLLM.
ما الذي يتغير عند 2 و5 و20 مستخدماً متزامناً
مستخدمان. يكاد التأثير يكون غير مرئي على وحدة معالجة رسومية (GPU) ذات ذاكرة مؤقتة فائضة، لأن تدفق فك التشفير الثاني يعمل بالتوازي مع الأول بزيادة طفيفة جداً في الوقت. أما على خادم افتراضي (VPS) يعتمد على المعالج المركزي فقط (CPU-only) بذاكرة RAM تتراوح بين 4 و8 جيجابايت، فالأمر ليس مجانياً: يتشارك التدفقان نفس العدد المحدود من أنوية المعالج (vCPUs) ونفس نطاق تمرير الذاكرة، لذا يرى كل مستخدم نصف عدد الرموز (tokens) في الثانية تقريباً، ويتضاعف الطلب على الذاكرة المؤقتة مقابل ميزانية أصغر بكثير.
خمسة مستخدمين. هنا تتوقف الإعدادات الافتراضية عن كونها كافية، وتبدأ المشكلة كأزمة طابور انتظار. مع ضبط OLLAMA_NUM_PARALLEL على 1، ينتظر أربعة أشخاص الشخص الذي طلب إجابة طويلة، ويحصل كل منهم على سرعة طبيعية بمجرد حلول دوره. عند رفع عدد العمليات المتوازية يتغير شكل المشكلة: خمس فتحات بسياق 8K لكل منها تعني الحاجة إلى ذاكرة مؤقتة بحجم 40K. إذا لم تتسع الذاكرة الرسومية (VRAM) لذلك، يقوم المحرك بترحيل الطبقات إلى ذاكرة النظام (RAM)، وإذا لم تتسع الذاكرة أيضاً، يبدأ النظام في استخدام مساحة التبادل (swap) وتنهار سرعة الرموز في الثانية.
عشرون مستخدماً. وجود عشرين شخصاً في واجهة دردشة لا يعني عادةً وجود عشرين طلباً متزامناً، وهذه أهم نقطة يجب فهمها قبل شراء العتاد. يقرأ الشخص الرد ويفكر لمدة تتراوح بين 20 إلى 60 ثانية بين كل رد وآخر، لذا فإن معظم وقت جلسته يكون خاملاً. أما عشرون وكيلاً (agents)، أو عشرون مهمة تلخيص مستندات، فهي عشرون تدفقاً حقيقياً دون أي وقت خمول على الإطلاق. هذا يتطلب جهازاً مختلفاً. المطور الذي وجّه وكيلاً برمجياً إلى خادم Ollama الخاص به يكون أقرب إلى الحالة الثانية منه إلى الأولى، لأن الوكيل يستمر في إرسال الطلبات طالما أن المهمة قيد التنفيذ، ولا يترك أي فترات توقف للقراءة كما يفعل البشر.
هل مستخدموك متزامنون، أم أنهم مسجلون للدخول فقط؟
احسب عدد الطلبات قيد المعالجة قبل تحديد حجم أي شيء. الحساب بسيط: الطلبات قيد المعالجة تساوي عدد المستخدمين، مضروباً في الثواني المستغرقة للتوليد في كل دورة، مقسوماً على الثواني بين الدورات.
- قِس سرعة التدفق الفردي الخاصة بك أولاً، مع مراعاة التعبئة المسبقة وفك التشفير. لا تستخدم رقماً من بطاقة شخص آخر: قِس عدد الرموز في الثانية على جهازك الخاص واستخدم النتيجة التي تحصل عليها.
- قدّر دورة العمل. عشرون مستخدم دردشة، 12 ثانية من التوليد لكل دورة، ودورة واحدة كل 90 ثانية، تعطي 20 * 12 / 90، أي حوالي 2.7 طلباً قيد المعالجة.
- اضبط عدد الفتحات (slots) أعلى من ذلك بقليل، ثم قارنه بالذاكرة: يجب أن يتسع حاصل ضرب الفتحات في سياق الطلب الواحد داخل رموز الذاكرة المؤقتة (cache tokens) المتاحة لديك فعلياً.
- اجعل قائمة الانتظار قصيرة حتى يفشل التجاوز بسرعة وبشكل واضح.
رموز الذاكرة المؤقتة المتاحة هي الذاكرة الحرة بعد تحميل الأوزان، مقسومة على تكلفة الرمز الواحد من القسم أعلاه. بطاقة بسعة 24 GB تشغل نموذج 8B بدقة 16-bit تستهلك حوالي 16 GB للأوزان، ولديها على الأكثر حوالي 6 GB من الذاكرة المؤقتة القابلة للاستخدام عند مستوى الاستخدام الافتراضي، وأقل من ذلك بقليل بمجرد تخصيص ذاكرة العمل، وهو ما يعادل حوالي خمس محادثات بحجم 8K. لاستيعاب المزيد، قلل سياق الطلب الواحد، أو خزّن الذاكرة المؤقتة بدقة 8-bit (تستخدم llama-server القيمة --cache-type-k q8_0). كلاهما يوفر التزامن مقابل التنازل عن شيء ما، والنسخة الصادقة من هذه المقايضة تستحق القراءة قبل أن تلتزم مالياً بالأجهزة: متى تصبح خوادم GPU الافتراضية أكثر جدوى من رموز API.
حيث تتوقف الإعدادات الافتراضية لـ Ollama عن كونها كافية
ارفع عدد العمليات المتوازية من خلال وحدة الخدمة، لأن تصدير المتغيرات في الصدفة (shell export) لن يصل إلى الخدمة التي يديرها 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 رمزاً من الذاكرة المؤقتة (cache) بجانبها. إذا أظهر عمود PROCESSOR جزءاً من النموذج على المعالج (CPU) بينما كنت تتوقع وجوده بالكامل على معالج الرسوميات (GPU)، فهذا يعني أنك طلبت ذاكرة مؤقتة أكبر مما تبقى في البطاقة. قلل أحد الرقمين. عادة ما يكون تقليل سياق الذاكرة (context) هو الخيار الأكثر أماناً، لكن النافذة الصغيرة جداً تقوم باقتطاع المطالبات الطويلة بصمت بدلاً من إظهار خطأ، لذا من الأفضل تحديد حجم num_ctx بعناية بدلاً من خفضه عشوائياً حتى يتناسب مع النموذج.
يستحق الإعداد الافتراضي لطابور الانتظار نظرة ثانية. تضع Ollama ما يصل إلى OLLAMA_MAX_QUEUE طلباً في الطابور، و"القيمة الافتراضية هي 512". بعد ذلك، تستجيب "بخطأ 503 يشير إلى أن الخادم محمّل فوق طاقته". إن طابور انتظار بعمق 512 على جهاز يخدم أربعة طلبات في وقت واحد هو وعد لا يمكنك الوفاء به، لأن العميل في الموقع 300 ستنتهي مهلته قبل أن يحين دوره بوقت طويل. الطابور القصير يعيد خطأ يمكن لتطبيقك إعادة المحاولة بناءً عليه أو الإبلاغ عنه، وهو أفضل من مؤشر تحميل لا ينتهي أبداً.
اختبر ذلك بشكل فعلي. أرسل طلبين في نفس اللحظة من طرفيتين وراقب كليهما. إذا لم ينتج الثاني شيئاً حتى ينتهي الأول، فإن إعداد التوازي لم يدخل حيز التنفيذ.
متى يبدأ محرك تقديم فعلي بتحقيق جدواه
يبرر vLLM إعدادَه الإضافي عندما تكون لديك GPU بسعة احتياطية، ويكون أكثر من نحو four طلبات قيد التنفيذ فعلياً. يعمل المجدول لديه على مستوى الرمز، وتستخدم ذاكرته المخبئية صفحات بحيث يعاد استخدام الأجزاء الحرة، كما يحوّل VRAM غير المستغلة إلى تزامن بدلاً من تركها خاملة. اعتباراً من October 2026، يتكون التثبيت والتشغيل الموثقان لـGPU من three أوامر. يجلب vllm download-kernels kernels مترجمة مسبقاً كان vLLM سيقوم بخلاف ذلك بتنزيلها وترجمتها عند بدء التشغيل:
uv pip install vllm --torch-backend=auto
vllm download-kernels
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 المجزأ الموصوفة سابقاً.
عندما يكون أقل من نحو four طلبات قيد التنفيذ، يضيف vLLM تعقيداً ولا يقدم فائدة كبيرة. لم تعد GPU مطلوبة: اعتباراً من October 2026، يوثق دليل تثبيت CPU «استدلال النماذج وتقديمها الأساسيين» على معالجات x86 وArm، مع حزم x86 مبنية مسبقاً منذ الإصدار 0.17.0 وصورة Docker لـCPU. لكن عادات استخدام الذاكرة تبقى كما هي. على CPU، يطبق النسبة نفسها البالغة 0.92 من الاستخدام على ذاكرة النظام RAM، ويمنح ذاكرة KV cache كل ما تتركه الأوزان، ولذلك يستحوذ افتراضياً على معظم ذاكرة الجهاز عند بدء التشغيل، ما لم تحد حجم الذاكرة المخبئية باستخدام --kv-cache-memory-bytes. هذه مقايضة خاطئة على VPS بسعة 4 إلى 8 GB. الحل هناك هو نموذج أصغر بسياق أقصر وطابور تتحكم فيه. يشرح الفرق بين Ollama وvLLM كمحركَي تقديم الاختيار كاملاً، ويوضح تشغيل Qwen 3 8B على VPS ما يتطلبه نموذج متوسط الحجم قبل إضافة حتى مستخدم إضافي واحد.
المقايضة التي تخفيها الشائعات
تزيد المعالجة المجمعة المستمرة (Continuous batching) من إجمالي الإنتاجية، وعادة ما تحسّن متوسط زمن الاستجابة أيضاً، لأن الطلب الموجود في قائمة الانتظار يبدأ في وقت أبكر. لكن زمن استجابة الذيل (Tail latency) يسير في الاتجاه المعاكس، ونادراً ما يتم ذكر هذا الجانب.
كل تسلسل إضافي في الخطوة يضيف قدراً بسيطاً من العمل، لذا يرتفع زمن الاستجابة بين الرموز (ITL) للجميع مع امتلاء الدفعة. تأخذ مرحلة التعبئة المسبقة (prefill) لطلب جديد جزءاً من الخطوة التي كان سيحصل عليها المستخدمون الذين يتلقون البث. تحت ضغط الذاكرة المؤقتة (cache pressure)، يقوم المجدول بعملية استباق (preempt)، مما يعيد طلباً تم توليد نصفه إلى بداية مرحلة التعبئة المسبقة الخاصة به.
تُظهر واجهات الدردشة ذيول التوزيع، لا المتوسطات. البث الذي يتوقف لثانيتين في منتصف الجملة يبدو معطلاً حتى لو كان الوقت الإجمالي للإكمال جيداً. قم بقياس p95 TTFT و p95 ITL تحت الحمل الذي تتوقعه، وتعامل مع متوسط الرموز في الثانية كرقم يمثل السعة وليس وصفاً لتجربة المستخدم.
الإعداد العملي يستنتج من ذلك. حدد التزامن (concurrency) بأقل قليلاً مما تسمح به الذاكرة، حتى لا يضطر المحرك إلى الاستباق أبداً. قائمة انتظار قصيرة ومتوقعة أفضل من دفعة كبيرة تسبب اضطراباً (thrashing)، لأن المستخدم الذي ينتظر أربع ثوانٍ ثم يتلقى بثاً سلساً يكون أكثر رضا من مستخدم يبدأ فوراً ثم يتوقف مرتين.
ما الذي يجب التحقق منه عند بطء الأداء
كل مستخدم يعمل بشكل طبيعي، لكن وقت الانتظار طويل. هذه مشكلة طابور انتظار وليست مشكلة سرعة. تحقق من إعداد التوازي أولاً. النموذج يعمل بشكل صحيح، لكنه يعالج طلباً واحداً في كل مرة.
خطأ HTTP 503 من Ollama. طابور الانتظار ممتلئ. إما أن الخادم وصل إلى طاقته الاستيعابية الفعلية، أو أن OLLAMA_MAX_QUEUE مضبوط على قيمة منخفضة عمداً لتخفيف الحمل، وهو السلوك المطلوب في هذه الحالة.
انهيار عدد الرموز (tokens) في الثانية تحت الضغط على خادم يعتمد على المعالج (CPU). شغّل vmstat 1 أثناء حدوث ذلك. وجود قيم غير صفرية في عمودي si و so يعني أن الجهاز يقوم بعملية التبديل (swapping)، مما يعني قراءة أوزان النموذج من القرص مع كل رمز. لا يوجد تغيير في الإعدادات يحل هذه المشكلة. قلل حجم النموذج أو عدد الفتحات (slots).
مستخدم واحد من بين عشرة ينتظر لفترة أطول بكثير من البقية. ابحث في سجلات vLLM عن preempted. السبب المعتاد هو الاستباق (preemption) وإعادة الحساب المرتبطة به، وهذا يعني أن ذاكرة التخزين المؤقت (cache) تجاوزت طاقتها بالنسبة لطول السياق المسموح به.
زمن الوصول للرمز الأول (TTFT) سيء حتى عندما يكون الخادم في حالة خمول. هذه مشكلة مرحلة التعبئة المسبقة (prefill) وليست مشكلة تزامن. الطلبات الطويلة (prompts) تستغرق وقتاً فعلياً قبل ظهور الرمز الأول، لذا تحقق من حجم الطلب والتخزين المؤقت للبادئات (prefix caching) قبل فحص العتاد. إذا كان الانتظار الطويل يؤثر فقط على أول مستخدم بعد فترة من الهدوء بينما يعمل الجميع بعده بشكل جيد، فهذا ليس بسبب التعبئة المسبقة، بل لأن Ollama يقوم بإلغاء تحميل النموذج من الذاكرة وإعادة قراءة الأوزان من القرص، وهو أمر يستحق المعالجة عبر إبقاء النموذج في الذاكرة بين الطلبات.
FAQ
لماذا يتباطأ نموذج LLM المستضاف ذاتياً عند استخدامه من قبل شخص ثانٍ؟
في أغلب الحالات، لا يتباطأ النموذج، بل يدخل في طابور انتظار. يأتي Ollama مع إعداد OLLAMA_NUM_PARALLEL مضبوط على 1، لذا ينتظر الطلب الثاني حتى ينتهي الطلب الأول من إصدار الرمز (token) الأخير. يمكنك التمييز بين الحالتين عبر قياس سرعة تدفق بيانات مستخدم واحد بينما ينتظر الآخر: إذا كانت سرعة الرموز في الثانية طبيعية بمجرد البدء، فأنت أمام طابور انتظار، وسيؤدي رفع عدد الطلبات المتوازية إلى حل المشكلة. أما إذا كان كلا التدفقين يعملان بنصف السرعة، فأنت تشارك فعلياً عرض نطاق الذاكرة (memory bandwidth)، وهذا حدٌّ عتادي.
كم عدد المستخدمين المتزامنين الذين يمكن لمعالج رسوميات (GPU) صغير خدمتهم؟
احسب الذاكرة، لا المستخدمين. ابدأ بحساب الأوزان (weights)، ثم ذاكرة التخزين المؤقت KV، والتي تكلفتها تساوي 2 ضرب عدد الطبقات ضرب رؤوس المفاتيح/القيم ضرب أبعاد الرأس ضرب البايتات، لكل رمز، لكل محادثة نشطة. نموذج 8B نموذجي بـ 36 طبقة، و8 رؤوس مفاتيح/قيم، وأبعاد رأس 128، يستهلك حوالي 144 KiB لكل رمز بدقة 16-bit، لذا فإن محادثة بطول 8,192 رمزاً تحتاج إلى حوالي 1.2 GB. بطاقة بسعة 24 GB تحمل هذا النموذج بدقة 16-bit يتبقى لها 6 GB كحد أقصى لذاكرة التخزين المؤقت، وهو ما يكفي لحوالي خمس محادثات بكامل السياق، أو أكثر إذا قمت بتقصير السياق.
هل تجعل المعالجة المجمعة المستمرة (continuous batching) رد كل مستخدم أبطأ؟
عادةً ما يتحسن متوسط زمن الاستجابة (median latency)، لأن الطلبات تتوقف عن انتظار انتهاء الدفعة بأكملها. لكن زمن استجابة الذيل (tail latency) يزداد سوءاً. كل تسلسل إضافي يضيف عبئاً على كل خطوة فك ترميز، كما أن مرحلة التعبئة المسبقة (prefill) لطلب جديد تستهلك جزءاً من خطوة المستخدمين الذين يتلقون تدفقاً، والطلب الذي يتم استباقه (preempted) يحتاج إلى إعادة التعبئة مرتين. قم بقياس زمن الاستجابة بين الرموز عند النسبة المئوية 95 (p95)، وليس المتوسط، لأن نافذة الدردشة تجعل التوقفات واضحة بطريقة تخفيها المتوسطات الحسابية.
هل يجب أن أرفع OLLAMA_NUM_PARALLEL أم أنتقل إلى vLLM؟
ارفع عدد الطلبات المتوازية أولاً. هذا الإجراء مجاني ويتطلب ملف إعداد بسيطاً، وهو يحل الحالة الشائعة حيث يصطف أربعة أشخاص خلف إجابة واحدة طويلة. الذاكرة هي العائق: الطلبات المتوازية تضاعف السياق الذي يجب عليك الاحتفاظ به، لذا راقب تسرب الطبقات إلى المعالج المركزي (CPU). انتقل إلى vLLM عندما يكون لديك معالج رسوميات بذاكرة VRAM فائضة وأكثر من أربعة طلبات قيد التنفيذ فعلياً، حيث أن هذه هي النقطة التي توفر فيها ذاكرة التخزين المؤقت المصفحة (paged cache) وجدولة الرموز عائداً أكبر من تكلفتها.
هل ستؤدي زيادة أنوية المعالج المركزي (CPU) إلى إصلاح خادم LLM بطيء؟
ليس للجزء الذي يلاحظه المستخدمون أكثر من غيره. عملية فك الترميز تقرأ النموذج بالكامل من الذاكرة لكل رمز، لذا فهي مقيدة بعرض نطاق الذاكرة (RAM bandwidth)، وتتوقف الأنوية الإضافية عن المساعدة بمجرد تشبع عرض النطاق. مرحلة التعبئة المسبقة (prefill) تتناسب طردياً مع عدد الأنوية، لذا فإن زيادتها تقصر الوقت اللازم للحصول على الرمز الأول في المطالبات الطويلة. على خادم افتراضي (VPS) بسعة 4 إلى 8 GB، يكون القيد الأساسي عادةً هو سعة الذاكرة، والحل الفعال هو استخدام نموذج أصغر أو سياق أقصر بدلاً من زيادة عدد vCPUs.