SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-23

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

هل يتوقف خادم LLM الخاص بك عند 5 مستخدمين؟ اكتشف كيف تؤثر إعدادات الـ KV cache والـ batching على الأداء، ولماذا يحدد طابور الانتظار سرعة استجابة النموذج لديك فعلياً.

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

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

نادراً ما يكون الحل هو الحصول على خادم أكبر. الحل هو استخدام محرك تقديم (serving engine) يدفع طلبات متعددة عبر النموذج في نفس تمريرة المعالجة (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) الخاص به يتحدد بناءً على مقال شخص آخر.

تسمح المعالجة الدفعية المستمرة بقبول الطلبات وإنهائها عند كل رمز (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، تُضاف إلى حجم الأوزان (weights)، وهذه هي الإجابة الحقيقية حول عدد المستخدمين الذين يمكن استيعابهم.

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

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

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

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

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

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

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

عند وجود أقل من أربعة طلبات قيد المعالجة، أو على أي جهاز لا يحتوي على GPU مدعوم، يضيف vLLM تعقيداً دون فائدة تذكر. يتطلب البرنامج بطاقة من فئة CUDA ويحجز معظم الذاكرة عند بدء التشغيل، وهو خيار غير مناسب لخوادم VPS بسعة 4 إلى 8 جيجابايت. في هذه الحالة، الحل هو استخدام نموذج أصغر مع سياق أقصر وطابور تحكم خاص بك. يغطي الرابط كيف يختلف 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) صغير خدمتهم؟

احسب الذاكرة، لا المستخدمين. ابدأ بحساب الأوزان، ثم ذاكرة التخزين المؤقت (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) لطلب جديد تستهلك جزءاً من خطوة المستخدمين الذين يتلقون البيانات حالياً، والطلب الذي يتم إيقافه مؤقتاً يحتاج إلى إعادة التعبئة مرتين. قم بقياس زمن الاستجابة بين الرموز عند النسبة المئوية 95 (p95)، وليس المتوسط، لأن نافذة الدردشة تجعل التوقفات واضحة بطريقة تخفيها الحسابات المتوسطة.

هل يجب أن أرفع قيمة OLLAMA_NUM_PARALLEL أم أنتقل إلى vLLM؟

ارفع عدد العمليات المتوازية أولاً. هذا الإجراء مجاني ويتطلب ملف إعداد بسيطاً، وهو يحل الحالة الشائعة حيث ينتظر أربعة أشخاص خلف إجابة واحدة طويلة. الذاكرة هي العائق: الطلبات المتوازية تضاعف حجم السياق الذي يجب الاحتفاظ به، لذا راقب تسرب الطبقات إلى المعالج المركزي (CPU). انتقل إلى vLLM عندما يكون لديك فائض في ذاكرة الفيديو (VRAM) وأكثر من أربعة طلبات قيد التنفيذ فعلياً، حيث أن هذه هي النقطة التي توفر فيها ذاكرة التخزين المؤقت المجدولة (paged cache) وجدولة المهام لكل رمز فوائد تفوق تكلفتها.

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

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