كل طلب يُرسل إلى خدمة ذكاء اصطناعي مستضافة يغادر شبكتك، ويعبر الإنترنت، وينتهي في مركز بيانات لا تتحكم في موقعه ولا في سياسة الاحتفاظ بالبيانات فيه. وإذا كان الطلب عقداً أو ملفاً طبياً أو شيفرة مصدرية، فهذا يطرح أسئلة تتعلق بالسرية. ويطرح أيضاً أسئلة عن التوافر (ماذا يحدث حين ينقطع الاتصال؟) وعن التكلفة (كيف ستبدو الفاتورة حين تعتمد المؤسسة كلها على الخدمة؟).
تشغيل نموذج على أجهزتك الخاصة ممكن، ولم يعد السؤال "هل هو ممكن" بل "أي جهاز، وأي صيغة، وأي محرك، ولأي حمل". هذا المقال هو دليل تقدير الأحجام الذي تمنيت أن أجده في البداية: معادلات يمكن إعادة حسابها يدوياً، ورتب مقادير موصوفة على حقيقتها، وأوامر تشغيل يجب تكييفها مع إصدارك، والحدود. الأمثلة الرقمية تستخدم تهيئات عامة وهي توضيحية: لا أدّعي هنا أي قياس للأداء. قِس دائماً على عتادك وعلى طلباتك أنت.
أين يعمل الذكاء الاصطناعي الذي تستخدمه؟
الذكاء الاصطناعي "في السحابة" (Cloud) يعمل على أجهزة المزوّد: ترسل نصاً وتستقبل نصاً. والذكاء الاصطناعي "المحلي" يعمل على أجهزة تتحكم فيها: حاسوب محمول، أو خادم في مقرك، أو مستضيف في ولايتك القضائية. المعيار المهم ليس الجغرافيا بل التحكم: من يرى الطلبات، ومن يحتفظ بالسجلات، ومن يقرر التحديثات.
المخطط لا يقول إن أحد النمطين أفضل، بل يبيّن أين تمر البيانات. وبقية المقال تتناول ما يكلّفه إعادة هذه النقطة إلى الداخل من ذاكرة وكمون وتشغيل.
النموذج ملف أوزان
بعد انتهاء التدريب، يُختزل نموذج اللغة في مصفوفات من الأرقام (الأوزان، أي المعاملات Parameters) ووصف للبنية. للاطلاع على أساسيات الشبكات العصبية، راجع الذكاء الاصطناعي ببساطة. المهم هنا أن هذا الملف يمكن نسخه. التدريب يحتاج إلى آلاف المسرّعات لأسابيع، أما الاستدلال (Inference) فيحتاج إلى تحميل الأوزان في الذاكرة وتنفيذ سلسلة من ضرب المصفوفات. والفارق في التكلفة بينهما كبير، وهو ما يجعل الاستدلال المحلي معقولاً.
يبقى أن تتمكن من الحصول على الأوزان. النماذج المملوكة من الصف الأول لا تُتاح إلا عبر واجهة برمجية (API). وإلى جانبها تُنشر عائلات كثيرة من النماذج ذات الأوزان المفتوحة: Llama من Meta، وMistral، وQwen من Alibaba، وGemma من Google، وDeepSeek، وFalcon من أبوظبي، وJais الذي صُمّم للعربية أولاً. وتتوفر كل عائلة بأحجام متعددة.
وهناك احتياطان. الأول أن "الأوزان المفتوحة" ليست "مفتوحة المصدر" بالمعنى الصارم: قد يقيّد الترخيص الاستخدام التجاري أو يفرض عتبات للمستخدمين أو بنود استخدام مقبول، وبيانات التدريب لا تُنشر عادة. اقرأ ترخيص الإصدار الدقيق الذي تنشره. والثاني أن الملف الذي تنزّله شيفرة وبيانات غير موثوقة: فضّل الصيغ التي لا تسمح بتنفيذ شيفرة اعتباطية (safetensors وGGUF) وتحقق من المصدر.
تقدير الذاكرة
ذاكرة المسرّع، لا قدرة الحوسبة الخام، هي التي تحدد أولاً ما يمكن تشغيله. وتنقسم إلى ثلاثة بنود.
الأوزان
المعادلة مباشرة:
weight_memory ≈ number_of_parameters × bytes_per_weight
نموذج من 8 مليارات معامل بدقة 16 بت (بايتان) يشغل نحو 16 GB؛ وبدقة 8 بت نحو 8 GB؛ وبدقة 4 بت أكثر من 4 GB بقليل، لأن الصيغ المكمّمة تخزّن أيضاً عوامل القياس (نحو 4.5 إلى 5 بت فعلية لكل وزن في صيغ 4 بت الشائعة). ونموذج من 70 مليار معامل يشغل نحو 140 GB بدقة 16 بت، ونحو 40 GB بدقة 4 بت. هنا 1 GB يساوي 10^9 بايت.
ذاكرة KV المؤقتة
أثناء التوليد، تحتاج كل طبقة انتباه إلى المفاتيح (K) والقيم (V) لجميع الرموز السابقة. يحتفظ المحرك بها في الذاكرة بدل إعادة حسابها: هذه هي ذاكرة KV المؤقتة (KV cache). وحجمها:
kv_memory = 2 × layers × kv_heads × head_dim × bytes × tokens × sequences
العامل 2 يحسب K وV. و"رؤوس KV" هي رؤوس الانتباه المجمّع للاستعلامات (GQA)، حيث تتشارك عدة رؤوس استعلام رأس مفتاح وقيمة واحداً، مما يقلص الذاكرة المؤقتة كثيراً. القيم الدقيقة موجودة في تهيئة النموذج (ملف config.json لنموذج على Hugging Face). لنأخذ تهيئتين عامتين، بذاكرة مؤقتة بدقة 16 بت:
| التهيئة (عامة) | الطبقات | رؤوس KV | بعد الرأس | البايتات لكل رمز |
|---|---|---|---|---|
| فئة 8 مليارات | 32 | 8 | 128 | 2 x 32 x 8 x 128 x 2 = 131,072 (128 KiB) |
| فئة 70 مليار | 80 | 8 | 128 | 2 x 80 x 8 x 128 x 2 = 327,680 (320 KiB) |
| فئة 70 مليار دون GQA | 80 | 64 | 128 | 2 x 80 x 64 x 128 x 2 = 2,621,440 (2.5 MiB) |
يبيّن الصف الأخير سبب وجود GQA: من دونها، سيكلّف تسلسل واحد من 8,192 رمزاً نحو 21 GB من الذاكرة المؤقتة في تهيئة 70 مليار. أما مع 8 رؤوس KV فيكلّف نحو 2.7 GB.
المجاميع التوضيحية، دون احتساب التنشيطات والهامش:
| السيناريو (توضيحي) | الأوزان | ذاكرة KV | المجموع التقريبي |
|---|---|---|---|
| 8B، 4 بت، تسلسل واحد من 8,192 رمزاً | ≈ 4.5 GB | ≈ 1.1 GB | ≈ 5.6 GB |
| 8B، 16 بت، 16 تسلسلاً من 8,192 رمزاً | 16 GB | ≈ 17.2 GB | ≈ 33 GB |
| 70B، 4 بت، تسلسل واحد من 8,192 رمزاً | ≈ 40 GB | ≈ 2.7 GB | ≈ 42 GB |
| 70B، FP8، 32 تسلسلاً من 8,192 رمزاً | 70 GB | ≈ 86 GB | ≈ 156 GB |
| الحالة نفسها، ذاكرة KV بدقة FP8 | 70 GB | ≈ 43 GB | ≈ 113 GB |
نتيجتان. لمستخدم واحد، تهيمن الأوزان. أما لخدمة مشتركة، فتتجاوز ذاكرة KV الأوزان بسرعة: صف الـ 32 تسلسلاً لا يتسع في وحدتي GPU سعة 80 GB، بينما يتسع الحمل نفسه مع ذاكرة مؤقتة بدقة 8 بت، مع هامش لا يزال ينبغي حجزه للتنشيطات. وتستطيع عدة محركات تكميم ذاكرة KV (مثلاً إلى FP8 أو 8 بت)؛ وأثر ذلك على الجودة يتوقف على النموذج وطول السياق، فلا بد من تقييمه. كما أن السياق الأقصى المعلن للنموذج ليس مجانياً: مضاعفة الطول تضاعف، في أسوأ الأحوال، الذاكرة المؤقتة لكل تسلسل.
أما التنشيطات والمخازن المؤقتة (الرسوم الحسابية، ومساحات عمل النوى، والتجزئة) فبند أصغر، لكنه ليس صفراً أبداً. لذلك تتيح المحركات سقفاً لاستخدام الذاكرة (انظر أدناه) بدل أن تحجز GPU كاملة.
التكميم: بايتات أقل لكل وزن
يستبدل التكميم (Quantization) الأوزان بدقة 16 بت بتمثيلات أقصر. وحدس ضغط الصور صحيح إلا في تفصيل واحد: الهدف ليس توفير المساحة فحسب، بل قراءة بايتات أقل عند كل رمز يُولَّد، وهذا يسرّع أيضاً فك الترميز (انظر القسم التالي).
عائلات الصيغ
- FP16 / BF16: المرجع. يحتفظ BF16 بالمدى الديناميكي لـ FP32، وهو ما تنشره معظم الإصدارات الرسمية.
- FP8 وINT8: بايت واحد لكل وزن. لا يحقق FP8 مكسباً حسابياً أصيلاً إلا على وحدات GPU حديثة تدعمه؛ وعلى غيرها يخفض الذاكرة فقط. تحقق بحسب مسرّعك ومحركك.
- 4 بت: نقطة الانطلاق المعتادة للنماذج الكبيرة على عتاد محدود. وتهيمن ثلاث منظومات: k-quants في صيغة GGUF (llama.cpp وOllama؛ بمتغيرات مثل Q4_K_M وQ5_K_M وQ6_K وQ8_0)، وAWQ، وGPTQ.
تختار AWQ (Lin وآخرون) الأوزان التي تستحق الحماية بناءً على التنشيطات الملاحظة في بيانات معايرة، لا على مقدارها وحده. وتكمّم GPTQ (Frantar وآخرون) طبقة بعد طبقة مع تقليل خطأ إعادة البناء. أما k-quants في GGUF فهي تكميمات على مستوى الكتل، مع عوامل قياس مخزّنة لكل كتلة؛ وتعمل على CPU وعلى GPU، وتتيح نقل بعض الطبقات إلى GPU حين لا تكفي الذاكرة.
لماذا لا تتراجع الجودة بالتساوي
من المريح أن نصدّق أن الخسارة بضع نقاط مئوية في كل مكان. هذا ليس ما نلاحظه عملياً، لأسباب بعضها ميكانيكي:
- تتراكم أخطاء التقريب في المهام الطويلة: الاستدلال متعدد الخطوات أو الحساب أو البرنامج يتطلب أن تبقى كل خطوة صحيحة، بينما يتسامح التلخيص مع بعض عدم الدقة في الألفاظ.
- النماذج الصغيرة أقل تكراراً في بنيتها وتتحمل أقل ما يُحذف من البتات. وعند تساوي الذاكرة، كثيراً ما يتفوق نموذج أكبر مكمَّم بشدة على نموذج أصغر مكمَّم بخفة (قاعدة تجريبية لا قانون، وتُختبر على مهامك).
- بيانات المعايرة، حين تستخدمها الصيغة (AWQ وGPTQ وبعض متغيرات GGUF ذات مصفوفة الأهمية)، تكون في الغالب بالإنجليزية. فرضيتي، التي لا أملك قياساً عاماً يدعمها، أن اللغات والمجالات الأقل تمثيلاً (العربية الدارجة، والمفردات القانونية أو الطبية المحلية) أكثر عرضة للتراجع. وهذا بالضبط نوع الأثر الذي يكشفه اختبار على نصوصك، ولا يراه معيار إنجليزي.
- الحيرة (Perplexity)، التي يكثر الاستشهاد بها لمقارنة التكميمات، تقيس التنبؤ بالرمز التالي لا نجاح المهمة: قد يتصرف تكميمان متقاربان في الحيرة بشكل مختلف في الاستخراج المنظّم أو استدعاء الأدوات.
القاعدة العملية: ابدأ من 8 بت، أو من 4 بت بجودة أعلى (مثل k-quant من فئة Q5 أو Q6) إن سمحت الذاكرة، ثم انزل تدريجياً، وقارن في كل مرة على مجموعة اختبارك (انظر أدناه).
الكمون والإنتاجية وعرض نطاق الذاكرة
مقياسان مختلفان للاستخدام:
- TTFT (الزمن حتى أول رمز): الفاصل بين إرسال الطلب واستلام أول رمز. ويتوقف على مرحلة التعبئة المسبقة.
- إنتاجية التوليد: عدد الرموز في الثانية بعد الأول، من جهة المستخدم (السرعة المحسوسة) أو من جهة الخادم (المجموع على كل الطلبات).
التعبئة المسبقة: يحدّها الحساب
معالجة طلب من N رمز تكلّف تقريباً 2 x المعاملات x N عملية فاصلة عائمة (قاعدة لرتبة المقدار في نموذج كثيف). لنموذج من 8 مليارات وطلب من 4,000 رمز، يعني ذلك نحو 6.4 x 10^13 عملية: وبافتراض حساب مستدام قدره 100 TFLOP/s، نحو نصف ثانية. تستفيد التعبئة المسبقة من وحدات الحساب جيداً لأن جميع رموز الطلب تُعالج معاً. لذلك يطيل الطلب الطويل (RAG أو سجل محادثة) زمن TTFT مباشرة.
فك الترميز: يحدّه عرض نطاق الذاكرة
لتوليد رمز واحد، يجب قراءة كل الأوزان الفعّالة (وذاكرة KV للتسلسل) من الذاكرة، مقابل حساب قليل جداً لكل بايت مقروء. والسقف النظري لتسلسل واحد هو إذن:
tokens_per_second ≲ memory_bandwidth / bytes_read_per_token
توضيح: نموذج مكمَّم حجمه 4.5 GB على ذاكرة بسرعة 100 GB/s يبلغ سقفه نحو 22 رمزاً في الثانية؛ وعلى ذاكرة أسرع بعشر مرات، نحو 220. الواقع أدنى (نوى غير مثالية، وذاكرة KV، وكلفة المحرك)، لكن رتبة المقدار تفسّر واقعتين عمليتين. التكميم يسرّع فك الترميز لأنه يقلل البايتات المقروءة. وجهازان بقدرة حساب متقاربة قد يختلفان كثيراً في سرعة التوليد بحسب ذاكرتيهما.
بالنسبة إلى مستخدم تفاعلي، تكفي بضع عشرات من الرموز في الثانية لتجاوز سرعة القراءة. المشكلة الحقيقية هي الحمل المتزامن.
لماذا يغيّر التجميع كل شيء
في فك الترميز، قراءة الأوزان مرة واحدة يمكن أن تخدم توليد الرمز التالي لعدة تسلسلات في آن واحد: تكلفة الذاكرة تكاد تكون مشتركة، بينما يتراكم الحساب. لذلك تنمو الإنتاجية الإجمالية تقريباً خطياً مع عدد التسلسلات، إلى أن يصبح الحساب أو قراءة ذاكرات KV هو عنق الزجاجة. والثمن كمون لكل طلب يرتفع مع الحمل. وفن خادم الاستدلال كله هو تعظيم هذه الإنتاجية دون أن يتدهور الكمون فوق ما تتحمله خدمتك.
مزيج الخبراء: المعاملات الكلية مقابل الفعّالة
بعض النماذج من نوع مزيج الخبراء (MoE): عند كل رمز، لا يفعّل موجّه (Router) إلا جزءاً من كتل التغذية الأمامية ("الخبراء"). والتمييز هنا بين المعاملات الكلية والمعاملات الفعّالة. مثال افتراضي: نموذج بـ 100 مليار معامل كلي، منها 10 مليارات فعّالة لكل رمز.
- الذاكرة تتبع المعاملات الكلية: يجب تحميل كل الخبراء لأنك لا تعرف مسبقاً أيها سيُستدعى. فهذا النموذج الافتراضي يحتاج إلى ذاكرة من فئة 100 مليار.
- الحساب والبايتات المقروءة لرمز منفرد يتبعان المعاملات الفعّالة: وعند تساوي عرض النطاق، يقترب فك الترميز من نموذج بـ 10 مليارات.
تتآكل هذه الميزة مع التجميع. فمع تسلسلات كثيرة متزامنة، تستدعي رموز مختلفة خبراء مختلفين، وينتهي الأمر بقراءة معظم الخبراء في كل خطوة. لذلك يكون MoE جذاباً خاصة حين تتوفر ذاكرة كبيرة (ذاكرة موحدة واسعة، أو خادم متعدد GPU) مع عرض نطاق قليل لكل رمز، وأقل جاذبية حين تكون الذاكرة هي القيد. كما أن التوزيع على عدة وحدات GPU (توازي الخبراء أو توازي المصفوفات) يضيف كلفة اتصال.
المحركات: مستخدم واحد أو خادم مشترك
تتطلب فئتان من الاستخدام محركين مختلفين.
الاستخدام الفردي أو لمجموعة صغيرة. يشغّل llama.cpp نماذج GGUF على CPU أو GPU أو كليهما، بتثبيت خفيف. ويغلّف Ollama محركاً من عائلة llama.cpp (في الأصل) بكتالوج نماذج وواجهة API محلية وإدارة لتحميل الذاكرة. ويقدّم LM Studio وJan واجهة رسومية. الهدف البساطة ومرونة العتاد، لا أقصى إنتاجية.
الخدمة المشتركة. يستهدف vLLM وSGLang الإنتاجية تحت حمل متزامن. وآليتان تفسران أهميتهما:
- التجميع المستمر (Continuous batching): بدل انتظار انتهاء دفعة كاملة من الطلبات، يُدخل المحرك طلبات جديدة في الدفعة بمجرد انتهاء تسلسل، رمزاً برمز. تبقى GPU مشغولة حتى حين تختلف أطوال المخرجات كثيراً.
- ذاكرة KV المؤقتة المقسّمة إلى صفحات (PagedAttention، التي قُدّمت مع vLLM على يد Kwon وآخرين): لا تُحجز الذاكرة المؤقتة ككتلة متصلة بالحجم الأقصى، بل في كتل ثابتة الحجم يشير إليها جدول لكل تسلسل، على غرار الذاكرة الافتراضية. هذا يقلل الهدر الناتج عن التجزئة ويتيح مشاركة الكتل بين التسلسلات. ويضيف SGLang، عبر RadixAttention، إعادة استخدام البادئات المشتركة (موجّه النظام، والوثائق المشتركة) بين الطلبات. ويتوقف المكسب كثيراً على حصة البادئات المشتركة في حركتك: يُقاس ولا يُفترض.
ويقدّم llama.cpp أيضاً التجميع المستمر وعدة خانات متوازية في خادمه، فيصلح لبضعة مستخدمين متزامنين. ويتسع الفارق مع vLLM أو SGLang كلما زاد التزامن والسياقات.
بعض الأوامر (تحقق منها على إصدارك)
تتغير الخيارات من إصدار إلى آخر: تحقق بالمساعدة المدمجة (--help) وبوثائق الإصدار المثبّت. أسماء النماذج أدناه أمثلة أو عناصر نائبة.
Ollama، استخدام محلي:
# Download and open an interactive session
ollama run llama3.1:8b
# See loaded models and where they run (PROCESSOR column: GPU, CPU, or split)
ollama ps
# Call the OpenAI-compatible API exposed locally (port 11434 by default)
curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model": "llama3.1:8b", "messages": [{"role": "user", "content": "Hello"}]}'
بحسب وثائق Ollama، يستمع الخادم افتراضياً على 127.0.0.1:11434، ويُبقي النموذج في الذاكرة 5 دقائق بعد آخر طلب (OLLAMA_KEEP_ALIVE)، ويعالج طلباً واحداً في كل مرة لكل نموذج افتراضياً (OLLAMA_NUM_PARALLEL)، ويضع في الانتظار حتى 512 طلباً (OLLAMA_MAX_QUEUE). وقد تغيّرت هذه القيم الافتراضية بين الإصدارات. وكشف الخادم على الشبكة عبر OLLAMA_HOST=0.0.0.0 لا يضيف أي مصادقة: ضعه خلف وكيل (Proxy) يوفّرها.
llama.cpp، خادم HTTP:
# -ngl: layers offloaded to the GPU, -c: context, -np: parallel slots
llama-server -m ./model-q4_k_m.gguf -ngl 99 -c 16384 -np 2 --port 8080
يكشف الخادم، من بين ما يكشفه، المسار /v1/chat/completions. وبحسب الوثائق، فإن السياق المطلوب يُقسّم من حيث المبدأ بين الخانات المتوازية: تحقق من هذا السلوك في إصدارك قبل تحديد قيمة -c.
vLLM، واجهة متوافقة مع OpenAI:
# GPU memory usage cap and maximum context length
vllm serve <organization>/<model> \
--max-model-len 16384 \
--gpu-memory-utilization 0.90 \
--api-key "$VLLM_API_KEY"
يستمع الخادم افتراضياً على المنفذ 8000، ويكشف المسار /v1/chat/completions ونقطة /metrics بصيغة Prometheus. يحدّ الخيار --max-model-len ذاكرة التخزين المؤقت لكل تسلسل؛ ويحدد --gpu-memory-utilization الجزء من ذاكرة GPU الذي يحق للمحرك استخدامه؛ وما يبقى بعد الأوزان والتنشيطات يخدم ذاكرة KV. أكّد أسماء الخيارات عبر vllm serve --help.
مستويات العتاد
الحاسوب المحمول. للاستخدام الفردي: مساعدة في الكتابة، وتلخيص، وشيفرة قليلة التعقيد. نموذج من 1 إلى 8 مليارات بدقة 4 بت يتسع في بضعة غيغابايت. والأجهزة ذات الذاكرة الموحدة (أجهزة Mac بشرائح Apple وبعض أجهزة x86 الحديثة) تتقاسم الذاكرة بين CPU وGPU، ويمكنها تحميل نماذج أكبر مما تسمح به بطاقة رسومات مخصصة بالسعر نفسه، مع بقاء عرض النطاق هو العامل المحدِّد في فك الترميز. التزامن هنا منخفض جداً: فكّر في مستخدم واحد.
الحاسوب المكتبي مع بطاقة رسومات. ذاكرة الفيديو في البطاقة تحدد السقف: نموذج متوسط مكمَّم مع سياق معقول. وإن لم تتسع الأوزان كلها، يستطيع llama.cpp نقل جزء من الطبقات إلى GPU وترك الباقي على CPU، مقابل سرعة أقل بوضوح لأن كل رمز يمر بالجزء البطيء. يناسب فريقاً صغيراً بتزامن منخفض، أو محطة عمل للمطورين.
خادم GPU. هنا يدور التقدير حول التزامن: كم تسلسلاً متزامناً، وأي طول للسياق، وأي TTFT مقبول. تحدد الأوزان الحد الأدنى لعدد وحدات GPU؛ وتحدد ذاكرة KV كم تسلسلاً يُضاف فوقها (انظر الجدول أعلاه). وبعد وحدة GPU واحدة، يوزّع توازي المصفوفات الأوزان لكنه يتطلب وصلات سريعة بين البطاقات؛ وعلى بطاقات موصولة بناقل PCIe فقط، قد يصبح الاتصال هو عنق الزجاجة. وخطّط أيضاً للطاقة والتبريد وتوفر البطاقات في سوقك، وهو غير مضمون دائماً.
خادم استدلال مشترك
الانتقال من محطة عمل إلى مؤسسة ليس مجرد تغيير الجهاز: تُضاف طبقات حول المحرك.
تتحدث التطبيقات إلى واجهة API متوافقة مع OpenAI، مما يتيح تغيير المحرك أو النموذج دون إعادة كتابة العملاء، والرجوع إلى خدمة مستضافة عند الحاجة. وتتولى البوابة (وكيل أو بوابة API موجودة) المصادقة والحصص لكل فريق وتحديد المعدل والتسجيل. لا تترك محرك استدلال مكشوفاً مباشرة: فأغلبها لا يقدّم إلا مصادقة دنيا بمفتاح مشترك، ولا إدارة دقيقة للهويات. ولمنح المساعد وصولاً إلى وثائقك، راجع الذكاء الاصطناعي ووثائقك: الاسترجاع يوضع أمام المحرك، فلا يتلقى إلا سياقاً صُفّي مسبقاً بحسب الصلاحيات.
المراقبة
يُراقَب خادم الاستدلال مثل أي خدمة، بمقاييس خاصة به. تابع أولاً:
- TTFT والكمون بين الرموز، كنسب مئوية (p50 وp95 وp99) لا كمتوسطات: المستخدمون يلاحظون أذيال التوزيع.
- الطلبات الجارية والمنتظرة: طابور يطول يشير إلى الإشباع قبل أن يشتكي المستخدمون.
- امتلاء ذاكرة KV: وعند اقترابه من 100% يضطر المحرك إلى إخراج تسلسلات أو رفضها ويتدهور الكمون.
- الرموز في الثانية مجمّعة، دخولاً وخروجاً، لمتابعة الحمل والتكلفة.
- ذاكرة GPU وحرارتها، والأخطاء العتادية، ونسبة أخطاء HTTP.
يكشف vLLM هذه المعلومات بصيغة Prometheus على /metrics (مثل مقاييس باسم vllm:time_to_first_token_seconds أو vllm:num_requests_running وقت الكتابة؛ وتتطور الأسماء بين الإصدارات، ومنها اسم مقياس امتلاء ذاكرة KV). اجمعها مع مقاييس GPU (مثل مصدّر DCGM من NVIDIA إن كان عتادك منها) ولوحة متابعة لكل فريق.
قيّم قبل أن تختار
تساعد التصنيفات العامة للنماذج على وضع قائمة مختصرة، لا على الحسم. فحركتك لها لغاتها وصيغها ووثائقها ومتطلبات دقتها. وتقييم أدنى يتسع له يوم واحد:
- اجمع من 50 إلى 200 طلب ممثِّل ومجهول الهوية، مع إجابة متوقعة أو معيار نجاح قابل للتحقق (استخراج حقول، أو تصنيف، أو استشهاد صحيح، أو شيفرة تجتاز اختبارات).
- شغّل كل مرشح (نموذج، وتكميم، وحجم سياق) على هذه المجموعة بإعدادات توليد واحدة، ويفضّل بحرارة منخفضة لضمان قابلية التكرار.
- قيّم آلياً ما يمكن تقييمه، واطلب مراجعة بشرية للباقي، وافصل النتائج بحسب اللغة ونوع المهمة.
- قِس بالتوازي TTFT والإنتاجية تحت التزامن الفعلي المتوقع، لا على طلب منفرد.
- أعد تشغيل المجموعة نفسها عند كل تغيير في النموذج أو الصيغة أو إصدار المحرك: إنه اختبار الانحدار لديك.
وإن استخدمت نموذجاً حكماً للتقييم، فاعلم أن له انحيازاته (تفضيل الإجابات الطويلة، وتفضيل أسلوبه) وتحقق منه على عينة راجعتها يدوياً. وكثيراً ما سيبيّن الاختبار أن نموذجاً أصغر مكمَّماً جيداً يكفي لمهمة محددة: وهذه أفضل طريقة لتقليل العتاد المطلوب.
السحابة أم المحلي: مقارنة
| ذكاء اصطناعي مستضاف | ذكاء اصطناعي محلي | |
|---|---|---|
| البيانات | تُرسل إلى المزوّد وفق عقده وولايته القضائية | تبقى على بنيتك التحتية |
| دون إنترنت | مستحيل | ممكن |
| التكلفة | بحسب الاستخدام، وتنمو مع الحجم | استثمار وتشغيل، ثابتان نسبياً |
| الجودة | الوصول إلى أكثر النماذج تقدماً | نماذج مفتوحة جيدة، وغالباً أقل بدرجة في أصعب المهام |
| التشغيل | يتكفل به المزوّد | على عاتقك |
| الانطلاق | فوري | من بضع ساعات (محطة عمل) إلى بضعة أسابيع (خدمة) |
التكلفة الإجمالية
مقارنة اشتراك بسعر بطاقة رسومات خطأ كلاسيكي. فتكلفة الاستدلال المحلي تشمل:
- العتاد، مُستهلكاً على عمره الفعلي الواقعي (تتطور وحدات GPU والنماذج بسرعة)؛
- الطاقة والتبريد، بما يتناسب مع الاستطاعة والاستخدام؛
- التشغيل: التثبيت، وتحديث المحركات والنماذج، والمراقبة، والمناوبة، والأمن، ووقت المهندسين الذي يمثله ذلك؛
- الاستضافة: المكان والشبكة والتكرار إن كانت الخدمة حرجة.
والحساب الذي يهم هو التكلفة لكل مليون رمز تُخدَّم فعلاً:
cost_per_million_tokens ≈ (amortized hardware + energy + operations) / tokens_served_over_the_period × 10^6
المقام هو نقطة الضعف: وحدة GPU مستخدمة بنسبة 10% تكلّف عشرة أضعاف لكل رمز مقارنة بوحدة مستخدمة بنسبة 100%. أسعار واجهات API تتغير، وكذلك أسعار العتاد: ولذلك لا أذكر أي رقم عمداً. أجرِ الحساب بعروض الأسعار لديك، وبسعر الكهرباء عندك، وبحركتك المقيسة، وقارنه بسعر الرمز في واجهة API التي ستستبدلها.
متى لا يكون المحلي منطقياً
- حجم منخفض. إذا كان استخدامك عرضياً، فلن يُجدي استهلاك تكلفة خادم أبداً، وستكلّف واجهة API (بعقد يناسب البيانات المعالجة) أقل.
- تحتاج أفضل نموذج. في الاستدلال الطويل أو المهام الوكيلة المعقدة أو التقييمات التي يظهر فيها فارق الجودة، تبقى النماذج المفتوحة غالباً دون أفضل النماذج المملوكة بدرجة. قِس هذا الفارق على حالاتك، فهو يتفاوت.
- لا فريق لتشغيله. خادم بلا مراقبة ولا تحديثات يتحول بسرعة إلى دَين، وإلى خطر أمني.
- حمل شديد التقلب. سعة مقدّرة للذروة تبقى معطلة بقية الوقت، بينما تمتص الخدمة بحسب الاستخدام هذه المرونة.
- المحلي لا يعني الآمن. الخادم داخل مقرك يجب أن يُحمى مثل أي نظام حساس: عزل الشبكة، والمصادقة، والسجلات، والنسخ الاحتياطية، والتحديثات. وواجهة API محلية بلا مصادقة تسريب محتمل.
والعكس موجود أيضاً: بيانات لا يجوز قانوناً أو تعاقدياً أن تخرج، أو تشغيل دون اتصال، أو حجم مرتفع ومستقر، كلها تبرر المحلي في كثير من الأحيان. والمقاربة الهجينة، التي تُبقي البيانات الحساسة محلياً وتُوكل الباقي إلى خدمة مستضافة، شائعة؛ وهي مرتبطة بمسألة سيادة البيانات.
ما يجب تذكّره
باختصار
- ذاكرة الأوزان ≈ المعاملات x بايتات الوزن. وذاكرة KV المؤقتة = 2 x الطبقات x رؤوس KV x بعد الرأس x البايتات x الرموز x التسلسلات. البند الأول ثابت، والثاني ينمو مع الحمل.
- يخفض التكميم الذاكرة ويسرّع فك الترميز، لكن خسارة الجودة ليست منتظمة: تتوقف على المهمة وحجم النموذج واللغة. قِس.
- التعبئة المسبقة يحدّها الحساب، وفك الترميز يحدّه عرض نطاق الذاكرة. والتجميع المستمر وذاكرة KV المقسّمة يجعلان خوادم مثل vLLM وSGLang مناسبة للتزامن؛ أما llama.cpp وOllama فيستهدفان الاستخدام الفردي.
- يحتاج MoE إلى ذاكرة معاملاته الكلية لكنه لا يحسب إلا معاملاته الفعّالة، وميزته تتآكل تحت تزامن كثيف.
- راقب TTFT والطوابير وامتلاء ذاكرة KV. وقيّم على حالاتك قبل أن تختار. واحسب التكلفة لكل رمز يُخدَّم، لا سعر البطاقة.
للبدء: ثبّت Ollama أو llama.cpp على محطة عمل، وحمّل نموذجاً مكمَّماً من 8 مليارات، وشغّل عليه طلباتك أنت مع مجموعة اختبار صغيرة. ستعرف في يوم واحد هل يكفيك المحلي، وأي حجم نموذج وسياق يبرر عتاداً مخصصاً. وما يلي ذلك أسئلة معمارية: البوابة، والوصول إلى الوثائق، والمراقبة. وتُعالج في المقالات الأخرى المرتبطة أعلاه.
المراجع
- Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention: https://arxiv.org/abs/2309.06180
- Zheng et al., SGLang: Efficient Execution of Structured Language Model Programs: https://arxiv.org/abs/2312.07104
- Lin et al., AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration: https://arxiv.org/abs/2306.00978
- Frantar et al., GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers: https://arxiv.org/abs/2210.17323
- llama.cpp server documentation: https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md
- vLLM documentation, online serving: https://docs.vllm.ai/en/latest/serving/online_serving/
- vLLM documentation, metrics: https://docs.vllm.ai/en/latest/design/metrics/
- Ollama FAQ: https://docs.ollama.com/faq