SSD Nodes Learn 🎉 VPS من $4.99/شهر
الأدلة Matt Connorبقلم Matt Connor

لماذا يتباطأ نموذج LLM عند زيادة عدد المستخدمين؟

هل يتوقف نموذجك عند 5 مستخدمين؟ السبب هو إعداد num_parallel الافتراضي في Ollama. تعرف على دور الـ KV cache وعمليات الـ Prefill في تحديد سعة خادمك وكيفية معالجة الطلبات المتزامنة.

لماذا يتباطأ نموذج اللغة (LLM) ذاتي الاستضافة عند زيادة عدد المستخدمين؟

يتوقف نموذج اللغة ذاتي الاستضافة عن الاستجابة بسلاسة عند وصول 5 مستخدمين متزامنين لأن الخادم لا يزال يولد رداً واحداً في كل مرة، بينما ينتظر المستخدمون الأربعة الآخرون في طابور. توضح وثائق Ollama صراحةً الإعداد الافتراضي: OLLAMA_NUM_PARALLEL هو "الحد الأقصى لعدد الطلبات المتوازية التي سيعالجها كل نموذج في نفس الوقت، والقيمة الافتراضية هي 1". لا يوجد عطل في النظام؛ أربعة من مستخدميك الخمسة ينتظرون دورهم فقط.

نادراً ما يكون الحل هو الحصول على خادم أكبر. الحل هو استخدام محرك تقديم (serving engine) يدفع طلبات متعددة عبر النموذج في نفس تمريرة المعالجة الأمامية (forward pass)، بالإضافة إلى توفير ذاكرة كافية للاحتفاظ بمحادثة كل مستخدم أثناء المعالجة. كلا الجزأين مهمان، والجزء الثاني هو ما يحدد فعلياً سقف قدرة خادمك.

المرحلتان اللتان يمر بهما كل طلب

تقوم مرحلة Prefill بقراءة كامل المطالبة (prompt) دفعة واحدة وبناء ذاكرة التخزين المؤقت للانتباه (attention cache) الخاصة بها. تمر كل رموز المطالبة عبر النموذج معاً، لذا تُعد مرحلة Prefill عملية ضرب مصفوفات كبيرة، وتكون محدودة بسرعة المعالجة الحسابية. أما مرحلة Decode فتقوم بكتابة الإجابة رمزاً برمز. يتطلب كل رمز قراءة أوزان النموذج بالكامل من الذاكرة مجدداً، بينما تكون العمليات الحسابية المنفذة على ذلك الرمز الواحد ضئيلة جداً. لذا، تكون مرحلة Decode محدودة بعرض نطاق الذاكرة (memory bandwidth).

هذا التباين هو السبب الجوهري لفعالية التجميع (batching). فعملية فك التشفير (decoding) لمستخدم واحد تقرأ، على سبيل المثال، 5 جيجابايت من الأوزان لكل رمز وتترك معظم وحدات الحساب خاملة. عند إضافة طلب ثانٍ، يقرأ المحرك نفس الـ 5 جيجابايت مرة واحدة، ثم يحسب رمزين منها. لا يتطلب المستخدم الثاني أي وقت إضافي تقريباً. إن خدمة الطلبات واحداً تلو الآخر بشكل صارم تهدر هذه الميزة.

يصف رقمان ما يشعر به المستخدم. TTFT (الوقت المستغرق للرمز الأول) هو مجموع وقت الانتظار في الطابور ومرحلة Prefill. أما ITL (زمن الانتقال بين الرموز) فهو الفجوة الزمنية بين الرموز المتدفقة، ويتم تحديده بواسطة مرحلة Decode. عادة ما يكون الخادم البطيء بطيئاً في إحدى هاتين المرحلتين، ولا تكون حلول المشكلتين متطابقة.

التجميع الثابت (Static batching) يجعل الجميع ينتظرون أبطأ استجابة

التجميع الثابت هو النسخة البدائية، وهو ما تحصل عليه إذا قمت بتجميع الطلبات بنفسك في كود التطبيق. يجمع المحرك N من الطلبات، ويشغلها معاً، ويحتفظ بكل خانة (slot) حتى ينتهي أطول توليد في المجموعة.

مستخدم يطلب ملخصاً بطول 1,200 رمز (token) يُبقي أربع إجابات قصيرة محتجزة في الدفعة، لأن الدفعة لا تحرر أي خانة حتى ينتهي أبطأ عضو فيها.

ينتج عن ذلك تكلفتان. تستمر التسلسلات المنتهية في شغل خانات لا تحسب أي شيء مفيد، لذا تنخفض الإنتاجية الفعلية مع تباين أطوال المخرجات، وأطوال مخرجات الدردشة تتباين كثيراً. الطلب الذي يصل بعد خطوة واحدة من تشكّل الدفعة ينتظر حتى تفرغ الدفعة بالكامل قبل أن يبدأ حتى في مرحلة التعبئة المسبقة (prefill)، مما يعني أن زمن الوصول إلى الرمز الأول (TTFT) الخاص به يتحدد بناءً على نص طويل لشخص آخر.

تسمح المعالجة الدفعية المستمرة (Continuous batching) بقبول الطلبات وإنهائها مع كل رمز (token)

تجدول المعالجة الدفعية المستمرة المهام على مستوى خطوة فك التشفير الواحدة. بعد كل خطوة، يُسقِط المجدول التسلسلات التي أصدرت للتو رمز التوقف الخاص بها، ثم يقبل الطلبات المنتظرة في الفتحات الشاغرة. الرد الذي ينتهي عند الخطوة 40 يحرر فتحته عند الخطوة 40، وليس في نهاية الدفعة.

هذا الأمر ليس غريباً. توثّق llama-server الخيار -cb, --cont-batching على أنّه "تحديد ما إذا كان سيتم تمكين المعالجة الدفعية المستمرة (المعروفة أيضاً باسم المعالجة الدفعية الديناميكية) (الافتراضي: ممكّن)"، وقد بُنيت 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 الخاص بالنموذج.

احسبها مرة واحدة وسيتوقف السقف عن كونه لغزاً. نموذج نموذجي بحجم 8B يحتوي على 36 طبقة، و8 رؤوس مفاتيح/قيم، وأبعاد رأس تبلغ 128، مع تخزين الذاكرة المؤقتة بدقة 16-bit، يستهلك 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، يتم توزيع السياق الذي تطلبه عبر -c على -np من الفتحات (slots)، لذا فإن زيادة عدد الفتحات وحدها تقلص ما يمكن لكل طلب استيعابه. اقرأ سياق كل فتحة من سجل بدء التشغيل بدلاً من افتراضه.

بدلاً من ذلك، يقوم vLLM بالتخصيص المسبق. --gpu-memory-utilization (القيمة الافتراضية 0.92) هو "جزء ذاكرة GPU الذي سيتم استخدامه لمنفذ النموذج". كل ما يتبقى بعد تحميل الأوزان يصبح مجمع 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) فقط مع ذاكرة وصول عشوائي (RAM) تتراوح بين 4 إلى 8 جيجابايت، فالأمر ليس مجانياً: يتشارك التدفقان نفس العدد المحدود من أنوية المعالج (vCPUs) ونفس نطاق الذاكرة الترددي، لذا يرى كل مستخدم نصف عدد الرموز (tokens) في الثانية تقريباً، ويتضاعف الطلب على الذاكرة المؤقتة مقابل ميزانية أصغر بكثير.

خمسة مستخدمين. هنا تتوقف الإعدادات الافتراضية عن كونها كافية، وتبدأ المشكلة كأزمة طابور انتظار. مع ضبط OLLAMA_NUM_PARALLEL على 1، ينتظر أربعة أشخاص الشخص الذي طلب إجابة طويلة، ويرى كل منهم سرعة طبيعية بمجرد حلول دوره. عند رفع عدد العمليات المتوازية يتغير شكل المشكلة: خمس فتحات بسياق 8K لكل منها تعني الحاجة إلى 40K من الذاكرة المؤقتة. إذا لم تتسع هذه البيانات في ذاكرة الفيديو (VRAM)، يقوم المحرك بنقل الطبقات إلى ذاكرة النظام (RAM)، وإذا لم تتسع في الذاكرة، يبدأ النظام في استخدام مساحة التبادل (swap) وتنهار سرعة معالجة الرموز في الثانية.

عشرون مستخدماً. وجود عشرين شخصاً في واجهة دردشة لا يعني عادةً وجود عشرين طلباً متزامناً، وهذا أهم شيء يجب فهمه قبل شراء العتاد. يقرأ الشخص الرد ويفكر لمدة تتراوح بين 20 إلى 60 ثانية بين كل دور وآخر، لذا فإن معظم وقت جلسته يكون خاملاً. أما عشرون وكيلاً، أو عشرون مهمة تلخيص مستندات، فهي عشرون تدفقاً حقيقياً دون أي وقت خمول على الإطلاق. هذا يتطلب جهازاً بمواصفات مختلفة تماماً.

هل مستخدموك متزامنون أم مسجلون للدخول فقط؟

احسب عدد الطلبات قيد المعالجة قبل تحديد حجم أي شيء. الحساب بسيط: الطلبات قيد المعالجة تساوي عدد المستخدمين، مضروباً في الثواني المستغرقة للتوليد في كل دورة، ومقسوماً على الثواني بين الدورات.

  1. قِس سرعة التدفق الفردي الخاصة بك أولاً، مع مراعاة التعبئة المسبقة وفك التشفير. لا تستعن برقم من بطاقة شخص آخر: قِس عدد الرموز في الثانية على جهازك الخاص واستخدم النتيجة التي تحصل عليها.
  2. قدّر دورة العمل. عشرون مستخدم دردشة، مع 12 ثانية توليد لكل دورة، ودورة واحدة كل 90 ثانية، يعطي 20 * 12 / 90، أي حوالي 2.7 طلب قيد المعالجة.
  3. اضبط عدد الفتحات (slots) أعلى من ذلك بقليل، ثم قارنه بذاكرة الوصول العشوائي: يجب أن يتسع حاصل ضرب الفتحات في سياق الطلب الواحد داخل رموز الذاكرة المؤقتة (cache tokens) المتاحة لديك فعلياً.
  4. اجعل قائمة الانتظار قصيرة حتى يفشل التجاوز بسرعة وبشكل واضح.

رموز الذاكرة المؤقتة المتاحة هي الذاكرة الحرة بعد تحميل الأوزان، مقسومة على تكلفة الرمز الواحد المذكورة في القسم أعلاه. بطاقة بسعة 24 GB تشغل نموذج 8B بدقة 16-bit تستهلك حوالي 16 GB للأوزان، ولديها ما يقرب من 6 GB كذاكرة مؤقتة قابلة للاستخدام عند مستوى الاستخدام الافتراضي، وهو ما يعادل حوالي خمس محادثات بحجم 8K. لاستيعاب المزيد، قلل سياق الطلب الواحد، أو خزّن الذاكرة المؤقتة بدقة 8-bit (يأخذ llama-server القيمة --cache-type-k q8_0). كلاهما يوفر التزامن مقابل التنازل عن شيء ما، والنسخة الصادقة من هذه المقايضة تستحق القراءة قبل تخصيص ميزانية للعتاد: متى تصبح تكلفة خادم GPU الافتراضي متعادلة مع رموز API.

حيث تتوقف إعدادات Ollama الافتراضية عن كونها كافية

ارفع عدد العمليات المتوازية من خلال وحدة الخدمة (service unit)، لأنّ تصدير المتغيرات عبر الصدفة (shell export) لن يصل إلى البرنامج الخفي (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 رمزاً من الذاكرة المؤقتة (cache) بجانبها. إذا أظهر عمود PROCESSOR جزءاً من النموذج على المعالج (CPU) بينما كنت تتوقع وجوده بالكامل على بطاقة الرسوميات (GPU)، فهذا يعني أنك طلبت ذاكرة مؤقتة أكبر مما هو متاح في البطاقة. قلل أحد هذين الرقمين.

تستحق القيمة الافتراضية لطابور الانتظار نظرة ثانية. يضع Ollama ما يصل إلى OLLAMA_MAX_QUEUE طلباً في الطابور، و"القيمة الافتراضية هي 512". بعد هذا الحد، يستجيب "بخطأ 503 يشير إلى أن الخادم مثقل بالطلبات". إن وجود طابور بعمق 512 على خادم يخدم أربعة طلبات في وقت واحد هو وعد لا يمكنك الوفاء به، لأن العميل في الموقع 300 ستنتهي مهلته الزمنية قبل أن يحين دوره بوقت طويل. الطابور القصير يعيد خطأ يمكن لتطبيقك إعادة المحاولة بناءً عليه أو الإبلاغ عنه، وهو أفضل من مؤشر تحميل لا ينتهي أبداً.

اختبر ذلك بشكل فعلي. أرسل طلبين في اللحظة نفسها من طرفيتين (terminals) وراقب كليهما. إذا لم ينتج الطلب الثاني أي شيء حتى ينتهي الأول، فإن إعداد التوازي لم يدخل حيز التنفيذ.

متى يصبح محرك الخدمة الفعلي مربحاً

يستحق vLLM عناء إعداده الإضافي عندما تمتلك وحدة معالجة رسوميات (GPU) ذات سعة فائضة، وعندما يكون لديك أكثر من أربعة طلبات قيد التنفيذ فعلياً. يعمل المجدول الخاص به على مستوى الـ token، ويتم تقسيم الذاكرة المؤقتة (cache) إلى صفحات بحيث يُعاد استخدام الأجزاء الفارغة، كما يقوم بتحويل الـ 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، وهو "الحد الأقصى لعدد الـ tokens التي يمكن معالجتها في تكرار واحد". يحدد المعيار الأول سقف التزامن، بينما يمثل الثاني ميزانية المعالجة المسبقة (prefill) المجزأة الموضحة سابقاً.

عند وجود أقل من أربعة طلبات قيد التنفيذ، أو على أي جهاز لا يحتوي على وحدة معالجة رسوميات مدعومة، فإن vLLM يضيف تعقيداً دون فائدة تذكر. يتطلب البرنامج بطاقة من فئة CUDA ويحجز معظم الذاكرة عند بدء التشغيل، وهو خيار غير مناسب لخوادم VPS بسعة 4 إلى 8 جيجابايت. في هذه الحالة، الحل هو استخدام نموذج أصغر مع سياق أقصر وطابور تحكم خاص بك. يغطي الرابط كيف يختلف Ollama و vLLM كمحركات خدمة هذا الاختيار بالتفصيل، بينما يوضح تشغيل Qwen 3 8B على خادم VPS المتطلبات التي يحتاجها نموذج متوسط الحجم قبل إضافة مستخدم واحد إضافي.

المقايضة التي تخفيها المفاهيم الشائعة

تزيد المعالجة المجمعة المستمرة (Continuous batching) من إجمالي الإنتاجية، وعادة ما تحسّن متوسط زمن الاستجابة (median latency) لأن الطلب الموجود في قائمة الانتظار يبدأ في وقت أبكر. لكن زمن استجابة الذيل (tail latency) يتأثر سلباً، ونادراً ما يتم التطرق إلى هذا الجانب.

كل تسلسل إضافي في الخطوة يضيف قدراً بسيطاً من العمل، لذا يرتفع زمن استجابة الرمز الواحد (ITL) للجميع مع امتلاء الدفعة. تأخذ مرحلة التعبئة المسبقة (prefill) لطلب جديد جزءاً من الخطوة التي كان سيستفيد منها المستخدمون الذين يتلقون البث. في ظل ضغط الذاكرة المؤقتة، يقوم المجدول بعملية استباق (preemption)، مما يعيد طلباً نصف مكتمل إلى بداية مرحلة التعبئة المسبقة الخاصة به.

تُظهر واجهات الدردشة ذيول التوزيع (tails)، لا المتوسطات. البث الذي يتوقف لثانيتين في منتصف الجملة يبدو معطلاً حتى لو كان الوقت الإجمالي للإكمال جيداً. قِس زمن الوصول للرمز الأول (TTFT) عند النسبة المئوية 95 (p95) وزمن استجابة الرمز الواحد (ITL) عند النسبة المئوية 95 (p95) تحت الحمل المتوقع، وتعامل مع متوسط الرموز في الثانية كرقم يمثل السعة وليس وصفاً لتجربة المستخدم.

الإعداد العملي يتبع ذلك. حدد التزامن (concurrency) بأقل قليلاً مما تسمح به الذاكرة، لكي لا يضطر المحرك إلى إجراء استباق. قائمة انتظار قصيرة ومتوقعة أفضل من دفعة كبيرة تسبب اضطراباً في الأداء، لأن المستخدم الذي ينتظر أربع ثوانٍ ثم يتلقى البث بسلاسة يكون أكثر رضا من المستخدم الذي يبدأ فوراً ثم يتوقف مرتين.

ما الذي يجب التحقق منه عند بطء الأداء

كل مستخدم يعمل بشكل طبيعي، لكن الانتظار طويل. هذه مشكلة طابور انتظار، وليست مشكلة سرعة. تحقق من إعداد التوازي أولاً. النموذج يعمل بشكل صحيح، لكنه يعالج طلباً واحداً في كل مرة.

خطأ HTTP 503 من Ollama. طابور الانتظار ممتلئ. إما أن الخادم وصل إلى طاقته الاستيعابية الفعلية، أو أن OLLAMA_MAX_QUEUE مضبوط على قيمة منخفضة عمداً لتخفيف الحمل، وهو السلوك المطلوب في هذه الحالة.

انهيار عدد الرموز (tokens) في الثانية تحت الحمل على خادم يعتمد على المعالج (CPU). شغّل vmstat 1 أثناء حدوث ذلك. وجود قيم غير صفرية في عمودي si و so يعني أن الجهاز يقوم بعملية تبديل (swapping)، مما يعني قراءة أوزان النموذج من القرص مع كل رمز. لا يوجد تغيير في الإعدادات يحل هذه المشكلة. قلل حجم النموذج أو عدد الفتحات (slots).

مستخدم واحد من كل عشرة ينتظر لفترة أطول بكثير من البقية. ابحث في سجلات vLLM عن preempted. الاستباق (preemption) وإعادة الحساب المرتبطة به هما السبب المعتاد، وهذا يعني أن ذاكرة التخزين المؤقت (cache) تجاوزت طاقتها بالنسبة لطول السياق المسموح به.

زمن الوصول للرمز الأول (TTFT) سيء حتى عندما يكون الخادم في حالة خمول. هذه مشكلة مرحلة التعبئة المسبقة (prefill)، وليست مشكلة تزامن. الطلبات الطويلة (prompts) تستغرق وقتاً فعلياً قبل ظهور الرمز الأول، لذا تحقق من حجم الطلب والتخزين المؤقت للبادئات (prefix caching) قبل فحص العتاد.

FAQ

لماذا يتباطأ نموذج اللغة (LLM) المستضاف ذاتياً عند استخدامه من قبل شخص ثانٍ؟

في أغلب الحالات، لا يتباطأ النموذج فعلياً، بل يدخل في قائمة انتظار. يأتي Ollama مضبوطاً افتراضياً على OLLAMA_NUM_PARALLEL بقيمة 1، لذا ينتظر الطلب الثاني حتى ينتهي الطلب الأول من إصدار آخر رمز (token). يمكنك التمييز بين الحالتين عبر قياس سرعة تدفق بيانات مستخدم واحد بينما ينتظر الآخر: إذا كانت سرعة الرموز في الثانية طبيعية بمجرد بدء التدفق، فأنت تواجه قائمة انتظار، ويمكنك حل ذلك بزيادة عدد العمليات المتوازية. أما إذا كان كلا التدفقين يعملان بنصف السرعة، فأنت تتشارك فعلياً في نطاق ذاكرة الجهاز، وهذا قيد عتادي (hardware limit).

كم عدد المستخدمين المتزامنين الذين يمكن لمعالج رسوميات (GPU) صغير خدمتهم؟

احسب الذاكرة، لا عدد المستخدمين. ابدأ بحساب الأوزان، ثم ذاكرة التخزين المؤقت (KV cache)، التي تكلفتها تساوي 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 عندما يكون لديك معالج رسوميات (GPU) بذاكرة فيديو (VRAM) فائضة وأكثر من أربعة طلبات قيد التنفيذ فعلياً، حيث أن هذه هي النقطة التي توفر فيها ذاكرة التخزين المؤقت المجدولة (paged cache) وجدولة المهام لكل رمز فوائد تفوق تكلفتها.

هل ستؤدي زيادة أنوية المعالج المركزي (CPU) إلى تسريع خادم LLM؟

ليس بالنسبة للجزء الذي يلاحظه المستخدمون أكثر من غيره. عملية فك التشفير تقرأ النموذج بالكامل من الذاكرة لكل رمز، لذا فهي مقيدة بنطاق ترددي للذاكرة (RAM bandwidth)، وتتوقف الأنوية الإضافية عن المساعدة بمجرد تشبع هذا النطاق. أما التعبئة المسبقة (prefill) فتتوسع مع زيادة الأنوية، لذا فإن زيادتها تقلل الوقت اللازم للرمز الأول في المطالبات الطويلة. على خادم افتراضي (VPS) بسعة 4 إلى 8 GB، يكون القيد الفعلي عادةً هو سعة الذاكرة، والحل الفعال هو استخدام نموذج أصغر أو سياق أقصر بدلاً من زيادة عدد الأنوية الافتراضية (vCPUs).

#vllm#ollama#batching#throughput#self-hosted-ai