KV cache مقابل prompt cache: الفرق وأيهما قد يفشل؟
تعرّف إلى الفرق العملي بين KV cache وprompt cache: الأولى تستهلك RAM أو VRAM وقد تمنع تحميل النموذج، والثانية تخفّض الفاتورة دون نفاد مساحة.
مقارنة بين KV cache وprompt cache: الإجابة المختصرة
تشترك KV cache وprompt cache لدى مزود الخدمة في كلمة واحدة تقريباً، ولا تكاد تتشابهان في شيء آخر. KV cache هي ذاكرة عمل خاصة بكل طلب. تبقى في RAM أو VRAM على خادمك طوال مدة طلب واحد، وتزداد مع طول السياق ومع عدد الطلبات التي تشغّلها في الوقت نفسه. أما التخزين المؤقت للمطالبات لدى مزود الخدمة فهو ميزة للفوترة وتقليل زمن الاستجابة. يُخزَّن جزء ثابت في بداية prompt على خوادم مزود الخدمة، ثم تُحاسَب عليه بسعر مخفّض عند إرساله مرة أخرى.
الأولى ذاكرة تشتريها كعتاد. والثانية ذاكرة يحتفظ بها طرف آخر ويحاسبك مقابل استخدامها.
الفرق العملي أهم من التعريف. قد تنفد مساحة KV cache، وعندها يرفض النموذج التحميل أو يُرفض الطلب. لا يمكن أن تنفد مساحة prompt cache. قد تفشل فقط في الاستفادة منها، وعندها تدفع السعر الكامل دون تنبيه.
ما الذي تحتفظ به KV cache ولماذا توجد
عند توليد Transformer للرمز المميز رقم 500، يجب أن ينتبه إلى الرموز المميزة الـ499 السابقة كلها. وتحتاج كل طبقة لكل رمز مميز منها إلى متجه key ومتجه value. ستؤدي إعادة حساب هذه المتجهات كلها لكل رمز مميز جديد إلى جعل مدة التوليد تنمو تربيعياً مع طول التسلسل، لذلك يحتفظ بها وقت التشغيل بدلاً من ذلك. ويُسمى هذا المخزن KV cache، أي ذاكرة التخزين المؤقت لـkey/value.
تمثل هذه الحالة حالة خاصة بكل طلب، لأنها تُبنى من تسلسل الرموز المميزة الدقيق لذلك الطلب. ولا يمكن لمستخدمين يرسلان prompt مختلفاً مشاركة هذه الحالة، إلا إذا كان وقت التشغيل يستخدم prefix caching، وهي ميزة منفصلة يرد وصفها لاحقاً.
تتم عملية التقديم على مرحلتين. يقرأ Prefill كامل prompt ويملأ ذاكرة التخزين المؤقت، وتحده قدرة المعالجة. وينتج Decode رمزاً مميزاً واحداً في كل مرة ويضيفه إلى ذاكرة التخزين المؤقت، ويحده عرض نطاق الذاكرة. ولهذا السبب تعرض معالجة prompt وتوليد الرموز المميزة سرعات مختلفة عند قياس عدد الرموز المميزة في الثانية على جهازك.
ما مقدار الذاكرة التي يستخدمها KV cache؟
لا تبحث عن جدول من الشركة المورّدة. الحجم عملية حسابية يمكنك إعادة إجرائها لأي نموذج:
bytes per token = 2 * layers * kv_heads * head_dim * bytes per elementيمثل الرقم 2 المفتاح والقيمة. تأتي كل الأرقام الأخرى من config.json الخاص بالنموذج، والمنشور في صفحة النموذج على Hugging Face.
خذ Llama 3.1 8B مثالاً. يدرج ملف الإعداد الخاص به num_hidden_layers بقيمة 32 وnum_key_value_heads بقيمة 8. ويعطي hidden_size البالغ 4096، موزعاً على 32 رأساً للانتباه، بُعد رأس يساوي 128. في f16، يشغل كل عنصر 2 بايت:
2 * 32 * 8 * 128 * 2 = 131072 bytes = 128 KiB per tokenاضرب هذه القيمة في السياق الذي تطلبه، ثم في عدد الطلبات التي تشغّلها في الوقت نفسه.
The data behind this chart
[
{
"label": "2k context",
"one_request_gib": 0.25,
"four_requests_gib": 1
},
{
"label": "8k context",
"one_request_gib": 1,
"four_requests_gib": 4
},
{
"label": "32k context",
"one_request_gib": 4,
"four_requests_gib": 16
},
{
"label": "128k context",
"one_request_gib": 16,
"four_requests_gib": 64
}
]عند استخدام سياق بحجم 8k، يكون حجم الذاكرة المؤقتة 1 GiB لطلب واحد. وعند 32k يصبح 4 GiB، وهو ضمن النطاق نفسه تقريباً لوزن النموذج بدقة 4-bit. وعند استخدام السياق الكامل للنموذج، البالغ 128k، يصبح الحجم 16 GiB لطلب واحد، و64 GiB إذا ملأ كل طلب من أربعة طلبات هذا السياق. لم تتغير الأوزان. الذي تغير هو الذاكرة المؤقتة فقط.
تؤدي آلية grouped query attention (GQA) دوراً كبيراً في هذه القيمة. يضم Llama 3.1 8B ثمانية رؤوس للمفتاح والقيمة تخدم 32 رأساً للاستعلام، لذلك يشترك كل أربعة رؤوس استعلام في زوج مفتاح وقيمة مخزّن واحد. ويستخدم النموذج الذي تتساوى فيه num_key_value_heads وnum_attention_heads أربعة أضعاف الذاكرة المؤقتة عند عدد المعلمات نفسه. افحص هذا الحقل قبل أن تفترض أن تكلفة تشغيل نموذجين بحجم 8B متساوية.
لماذا يرفض نموذج عمل عند 2k التحميل عند 32k
لأن بيئة التشغيل تحجز ذاكرة KV cache عند تحميل النموذج، ويُحدَّد حجمها وفق طول السياق الذي ضبطته، لا وفق طول المطالبة التي ترسلها فعلياً. يبلغ حجم نافذة السياق الافتراضية في Ollama مقدار 4096 رمزاً. عند رفعها إلى 32k، فإنك تطلب 4 GiB من التخصيص الإضافي قبل وصول أي رمز.
OLLAMA_CONTEXT_LENGTH=32768 ollama serveالإعداد نفسه لكل جلسة، من المطالبة التفاعلية:
ollama run llama3.1:8b
/set parameter num_ctx 32768يختلف شكل الفشل في كل مكدس. يتحقق vLLM من الحسابات عند بدء التشغيل ويرفض العمل:
ValueError: The model's max seq len (131072) is larger than the maximum number of tokens that can be stored in KV cache (78336). Try increasing `gpu_memory_utilization` or decreasing `max_model_len` when initializing the engine.على VPS يعمل بالمعالج فقط، لا يحدث هذا التحقق لأن التخصيص يستخدم ذاكرة النظام العادية. بدلاً من ذلك، تقتل آلية نفاد الذاكرة في النواة العملية، وتترك الدليل في المخزن الحلقي للنواة:
dmesg -T | grep -i "killed process"يعني السطر الذي يذكر عملية تقديم النموذج أن الخادم التزم بذاكرة أكبر من المتاح لديه. الحل هو استخدام سياق أصغر، وليس ملف swap أكبر: إذ تُقرأ KV cache المرحّلة إلى القرص عند توليد كل رمز، فتتباطأ عملية التوليد إلى حد يجعلها غير مفيدة. يوضّح دليلنا إلى num_ctx وطول السياق في Ollama كيفية اختيار قيمة مناسبة.
ما الذي يفعله التزامن بعدد الطلبات
يحمل كل طلب قيد التنفيذ ذاكرة KV cache الخاصة به. هذه هي النقطة التي تغفل عنها معظم خطط السعة. يحتاج 4 مستخدمين، يحتفظ كل منهم بسياق بحجم 32k، إلى 16 GiB فيما بينهم، إضافةً إلى أوزان النموذج.
تختلف بيئات التشغيل في مدى صرامتها في هذا الجانب. يحجز Ollama وllama.cpp حجم السياق الذي طلبته عند تحميل النموذج، لذلك تُخصَّص الذاكرة سواء استخدمها أحد أم لا. أما vLLM فيقسّم التجميعة إلى كتل ثابتة الحجم ويوزّعها مع نمو كل طلب، لذلك يشغل طلب بحجم 500 رمز مساحة تعادل 500 رمز فقط. في كلتا الحالتين، تكون التجميعة محدودة، وعندما تمتلئ تصطف الطلبات الجديدة في طابور بدلاً من تنفيذها. نوضّح تأثير هذا الاصطفاف في الطابور على أزمنة الاستجابة في عدد المستخدمين المتزامنين الذين يستطيع نموذج LLM مستضاف ذاتياً خدمته.
Four ways to make the KV cache smaller
- Lower the context length. This is the biggest lever and usually the cheapest. Most chat workloads never come close to 32k.
- Quantise the cache itself. Ollama's
OLLAMA_KV_CACHE_TYPEdefaults tof16and acceptsq8_0, which uses about half the memory, andq4_0, which uses about a quarter. The llama.cpp equivalents are-ctk q8_0and-ctv q8_0. - Pick a model with fewer key/value heads or fewer layers. Read
config.jsonbefore you download 40 GB of weights. - Serve fewer requests at once and queue the rest.
At q4_0 the Llama 3.1 8B figure drops from 128 KiB per token to roughly 32 KiB, so 32k of context costs about 1 GiB instead of 4 GiB. That saving is not free. The keys and values are stored with less precision, so compare output on your own prompts before you keep it.
ما الذي يقدّمه التخزين المؤقت للمطالبات لدى موفّر الخدمة فعلياً
التخزين المؤقت للمطالبات لدى موفّر الخدمة منتج مختلف، وله وحدة احتساب مختلفة. تحدد بادئة مستقرة، ويخزّنها موفّر الخدمة، ثم تُحتسب الاستدعاءات اللاحقة التي تكرر البادئة نفسها تماماً بسعر مخفّض بدلاً من سعر الإدخال الكامل.
المضاعفات المنشورة لدى Anthropic، حتى August 2026، هي كما يلي: تكلف كتابة ذاكرة تخزين مؤقت لمدة 5 minutes مقدار 1.25 ضعف سعر رموز الإدخال الأساسي. وتكلف الكتابة لمدة 1 hour مقدار ضعفين، بينما تكلف قراءة الذاكرة المؤقتة مقدار 0.1 ضعف. إذا وضعت مطالبة نظام مؤلفة من 20,000 token ضمن هذه الأرقام، يتضح شكل الصفقة مباشرة.
The data behind this chart
[
{
"label": "No caching, every call",
"billed_token_equivalents": "20,000"
},
{
"label": "First call, 5 minute cache write",
"billed_token_equivalents": "25,000"
},
{
"label": "First call, 1 hour cache write",
"billed_token_equivalents": "40,000"
},
{
"label": "Every later call, cache hit",
"billed_token_equivalents": "2,000"
}
]اقرأ ذلك باعتباره عملية حسابية. تبلغ علاوة الكتابة لمدة 5 minutes ما يعادل 5,000 token في الاستدعاء الأول: 25,000 مقابل 20,000 عند إرسالها من دون تخزين مؤقت. في كل استدعاء لاحق ضمن النافذة، تُحتسب 2,000 بدلاً من 20,000، أي بتوفير قدره 18,000. لذلك يصبح التخزين المؤقت لمدة 5 minutes موفراً ابتداءً من الاستدعاء الثاني.
التخزين المؤقت لمدة 1 hour رهان مختلف. إذ تُحتسب 40,000 عند الكتابة، أي بعلاوة قدرها 20,000 من مكافئات الرموز، ولذلك يحتاج إلى إصابتين ضمن الساعة قبل أن يصبح موفراً. هذا يتعلق بنمط حركة المرور لديك، لا بالنموذج. يوضّح الحساب الرياضي لنقطة التعادل في تخزين مطالبات Claude مؤقتاً العملية الحسابية كاملة، بما في ذلك طريقة اختيار النافذة.
هناك تفصيلان يحددان ما إذا كنت ستصيب الذاكرة المؤقتة أصلاً. أولاً، لا تُخزَّن البادئة التي يقل طولها عن الحد الأدنى للنموذج مؤقتاً، من دون إصدار خطأ: حتى August 2026، يبلغ الحد الأدنى الموثّق 512 token لـ Claude Opus 5 و1,024 token لـ Claude Sonnet 5، ويُعالَج الطلب الأقصر بصورة عادية من دون إرجاع خطأ. ثانياً، تُقاس مدة الصلاحية من بداية الطلب الذي يكتب الإدخال أو يقرأه، وتؤدي كل قراءة إلى تجديدها من دون تكلفة إضافية. لذلك يحافظ endpoint كثير الاستخدام على التخزين المؤقت لمدة 5 minutes إلى أجل غير محدد. أما endpoint الذي يُستدعى مرة كل ten minutes، فيدفع علاوة الكتابة في كل مرة ولا يستفيد من أي قراءة.
تحقق من الاستجابة بدلاً من الافتراض. يعرض الكائن usage كلاً من cache_creation_input_tokens وcache_read_input_tokens. إذا كان عدد القراءات صفراً في كل استدعاء، فهذا يعني أنك تشتري عمليات كتابة ولا تحصل على أي نتيجة منها.
عندما تتقاطع ذاكرة التخزين المؤقت المحلية والبعيدة
تلتقيان عند موجه نظام طويل، ويُحتسب عليك استهلاك الموارد من الجانبين في الوقت نفسه.
محلياً، يشغل موجه نظام مؤلف من 20,000 رمزاً نحو 2.4 GiB من ذاكرة KV المؤقتة على خادم Llama 3.1 8B باستخدام f16، ويُحجز ذلك بشكل مستقل لكل طلب متزامن يتضمنه. عن بُعد، يكلّف هذا الجزء السابق نفسه عملية كتابة واحدة في ذاكرة التخزين المؤقت، ثم 0.1 من تكلفة الإدخال في كل استدعاء لاحق. تتناسب التكلفة المحلية مع عدد المستخدمين. أما التكلفة البعيدة فتتناسب مع حجم حركة المرور، وتُعاد تهيئتها أثناء فترات الخمول.
توجد ميزة محلية تشبه التخزين المؤقت لموجهات provider، ويحدث الخلط بينها وبينه باستمرار: التخزين المؤقت للبادئة. تصف وثائق vLLM التخزين المؤقت التلقائي للبادئة بأنه تخزين مؤقت لـ"ذاكرة KV للاستعلامات الموجودة، بحيث يمكن للاستعلام الجديد إعادة استخدام ذاكرة KV مباشرةً إذا شارك إحدى الاستعلامات الموجودة في البادئة نفسها". يحتفظ خادم llama.cpp بذاكرة مؤقتة للموجه لكل slot افتراضياً، ويحدد --cache-reuse N أصغر جزء سيحاول إعادة استخدامه.
يوفّر التخزين المؤقت للبادئة ما قبل المعالجة فقط. تتم معالجة موجه النظام المؤلف من 20,000 رمز مرة واحدة بدلاً من معالجته مع كل طلب، ما يقلل زمن الوصول إلى أول رمز بدرجة كبيرة. في vLLM، يُعاد استخدام الكتل المشتركة بدلاً من تكرارها، ولذلك تتحسن كفاءة الذاكرة أيضاً. لكنه لا يقلل أبداً حجم ذاكرة التخزين المؤقت اللازمة للاحتفاظ بالرموز النشطة حالياً. ويُعد إبقاء الأوزان محمّلة في الذاكرة بين الطلبات آلية مرتبطة لكنها منفصلة، وقد تناولناها في إبقاء نموذج Ollama محمّلاً بين الطلبات.
ما الذي ينبغي قياسه على خادمك
حمّل النموذج باستخدام قيمة السياق المستهدفة، ثم اقرأ الأرقام الفعلية بدلاً من الاعتماد على التقدير.
ollama ps
nvidia-smi --query-gpu=memory.used,memory.total --format=csv
free -gيعرض ollama ps النموذج المحمّل مع حجمه وما إذا كان يعمل على GPU أو CPU. إذا كان من المتوقع أن يعمل النموذج بالكامل على GPU، لكنه يعرض توزيعاً جزئياً على CPU، فهذا يعني أن ذاكرة التخزين المؤقت KV دفعت جزءاً منه إلى CPU، ولذلك ستنخفض سرعة التوليد. يعرض nvidia-smi القيمة الفعلية لذاكرة VRAM، ويؤدي free -g الوظيفة نفسها على VPS يعمل باستخدام CPU فقط. ارفع قيمة السياق تدريجياً، وأعد التحميل، وراقب تغيّر الرقم. ينبغي أن تكون حساباتك والرقم المعروض متقاربين. عندما لا يحدث ذلك، تكون الفجوة عادةً بسبب مخازن الحساب الخاصة ببيئة التشغيل نفسها، وليست خطأً في الصيغة.
إذا دفعتك هذه الأرقام إلى اختيار عتاد لا تفضّل استئجاره، فستجد مقارنة التكلفة مع الدفع لكل token في مقارنة GPU VPS بتكلفة tokens عبر API.
FAQ
هل ذاكرة KV المؤقتة هي نفسها التخزين المؤقت للمطالبات؟
لا. ذاكرة KV المؤقتة هي ذاكرة خاصة بالطلب داخل عملية تقديم النموذج، وتحتفظ بمتجهات key وvalue لكل token في السياق الحالي. توجد في RAM أو VRAM وتُحرَّر عند انتهاء الطلب. أما التخزين المؤقت للمطالبة لدى المزوّد فهو ميزة فوترة تخزّن بادئة مستقرة للمطالبة على البنية التحتية للمزوّد، وتفرض سعراً مخفّضاً عند إرسالها مرة أخرى. يؤدي نفاد ذاكرة KV المؤقتة إلى منع تحميل النموذج. أما غياب التخزين المؤقت للمطالبة فلا يؤدي إلا إلى زيادة الفاتورة ووقت الوصول إلى أول token.
لماذا يُحمَّل النموذج عند سياق 2k لكنه يفشل عند 32k؟
لأن runtime يخصّص ذاكرة KV المؤقتة كاملة وقت التحميل، ويحدد حجمها وفق طول السياق الذي ضبطته، لا وفق حجم المطالبة التي ترسلها. بالنسبة إلى Llama 3.1 8B عند f16، تستهلك الذاكرة المؤقتة 128 KiB لكل token، ولذلك يستهلك سياق بحجم 2k مقدار 0.25 GiB، بينما يستهلك سياق بحجم 32k مقدار 4 GiB. تتسع الذاكرة لكلتا الحالتين. لكن الحجز هو الذي يفشل. يعرض vLLM ذلك كخطأ ValueError يذكر الحد الأقصى لعدد tokens التي كان يمكنه تخزينها، ويقترح رفع gpu_memory_utilization أو خفض max_model_len. في خادم يعمل على CPU فقط، تنهي أداة kernel out-of-memory killer العملية بدلاً من ذلك. ويمكنك تأكيد ذلك باستخدام dmesg -T | grep -i "killed process".
كيف أحسب حجم ذاكرة KV المؤقتة لنموذجي؟
اضرب 2 في عدد الطبقات، وعدد رؤوس key/value، وأبعاد الرأس، وعدد bytes لكل عنصر. يعطيك ذلك عدد bytes لكل token. ثم اضرب الناتج في طول السياق وعدد الطلبات المتزامنة. اقرأ أعداد الطبقات والرؤوس من config.json الخاص بالنموذج. استخدم 2 bytes لكل عنصر عند f16 أو bf16. تبلغ ذاكرة q8_0 المؤقتة نحو نصف ذلك، بينما تبلغ q4_0 نحو ربعه.
هل يقلل التخزين المؤقت للمطالبات مقدار الذاكرة التي يحتاج إليها خادمي؟
لا يؤثر التخزين المؤقت للمطالبات لدى المزوّد في أجهزتك، لأن التخزين موجود لدى المزوّد. البديل المحلي هو prefix caching، الذي يوفّره كل من vLLM وخادم llama.cpp. يعيد استخدام متجهات key وvalue المحسوبة مسبقاً لبادئة مشتركة، ما يوفر حسابات prefill ويقلل وقت الوصول إلى أول token. في vLLM، يُعاد استخدام الكتل المشتركة بدلاً من تكرارها، ولذلك تتحسن الاستفادة من الذاكرة أيضاً. لا تقلل أي من الميزتين حجم الذاكرة المؤقتة المطلوبة للـtokens الموجودة حالياً قيد المعالجة، لذلك يظل حساب السياق والتزامن هو الذي يحدد الحد الأدنى.
هل يستحق الأمر تخزين مطالبة مؤقتاً إذا كنت سأرسلها مرة واحدة فقط؟
لا. تكلّف كتابة التخزين المؤقت أكثر من الإدخال العادي؛ فهي تكلّف 1.25 ضعف السعر الأساسي لخيار 5-minute اعتباراً من August 2026. لذلك فإن البادئة التي لن تعيد إرسالها خلال النافذة الزمنية تمثل خسارة مباشرة. يصبح التخزين المؤقت مجدياً عند تكرار البادئة نفسها، مثل مطالبة نظام طويلة أو مستند ستطرح عنه عدة أسئلة. تحقّق من cache_read_input_tokens في استجابة API للتأكد من حصولك على hits بدلاً من دفع تكلفة writes.