كيف أثمر تنفيذ ذاكرة التخزين المؤقت KV في nanoVLM بنسبة تحسين 38% في جودة التوليد؟
ملخص تطبيق تقنية ذاكرة التخزين المؤقت (KV Caching) في nanoVLM مقدمة النماذج اللغوية الذاتية الانحدارية تولد النص عن طريق اختيار رمز واحد تلو الآخر. أثناء الاستدلال، يقوم النموذج بمعالجة سلسلة مدخلات معينة، توقع الرمز التالي، يضيفه إلى السلسلة، ويكرر هذا العملية حتى يتم تحقيق بعض المعايير التوقفية. هذه العملية التكرارية تؤدي إلى تكرار حسابي غير ضروري، مما يجعلها غير فعالة من حيث الأداء. في هذا المقال، نستكشف تقنية ذاكرة التخزين المؤقت (KV Caching) وكيف يمكنها تحسين كفاءة هذه العملية. إعادة النظر في هيكلية الترانسفورمر قبل الغوص في تقنية التخزين المؤقت، دعنا نعيد النظر في كيفية عمل آلية الانتباه (attention) في نماذج الترانسفورمر. يتكون نموذج الترانسفورمر اللغوي من طبقات متراكمة، كل واحدة منها تتكون من: - آلية الانتباه الذاتية (self-attention) - طبقة التغذية الراجعة (feed-forward layer) - طبقات التطبيع (normalization layers) للفهم الأفضل لمكان مساعدة ذاكرة التخزين المؤقت، نركز على آلية الانتباه الذاتية، وبشكل خاص داخل رأس انتباه واحد. حساب الانتباه الذاتي لسلسلة من تطبيقات المدخلات ( T ) تمثلها ( X \in \mathbb{R}^{T \times D} )، يتم حساب الانتباه الذاتي كما يلي: [ \text{Attention}(X; Q, K, V) = \text{softmax}\left( \frac{QK^\top \cdot M}{\sqrt{d_k}} \right)V ] هنا مثال بسيط باستخدام PyTorch وقناع السببية (causal mask): ```python import torch import torch.nn.functional as F attention_scores = Q @ K.T قناع مثلثي سفلي لمنع الوصول إلى الرموز المستقبلية causal_mask = torch.tril(torch.ones(input_seq_length, input_seq_length)) masked_scores = attention_scores.masked_fill(causal_mask == 0, float('-inf')) attention_weights = F.softmax(masked_scores, dim=-1) output = attention_weights @ V ``` أين تنشأ التكرارات غير الضرورية في توليد النص الذاتي الانحداري، يقوم النموذج بإنتاج رمز واحد تلو الآخر. مع كل خطوة، يقوم بإعادة حساب ( Q ), ( K )، و ( V ) لجميع السلسلة، حتى وإن لم يتغير الرموز السابقة. ```python new_token_emb = torch.randn(1, dim_model) extended_input = torch.cat([input_ids_emb, new_token_emb], dim=0) Q_ext = extended_input @ W_q K_ext = extended_input @ W_k V_ext = extended_input @ W_v ``` لتؤكد على التكرار: python torch.testing.assert_close(K, K_ext[:input_seq_length]) # الاختبار ناجح torch.testing.assert_close(V, V_ext[:input_seq_length]) # الاختبار ناجح هذه الفحوصات تظهر أن لجميع الرموز باستثناء أحدث رمز، ( K ) و ( V ) هي نفسها التي تم حسابها سابقًا. تزداد تكلفة هذه التكرارات بشكل كبير مع زيادة طول السلسلة. كيف تساعد ذاكرة التخزين المؤقت (KV Caching) في حل المشكلة للتخلص من هذه الكفاءة، نستخدم تقنية ذاكرة التخزين المؤقت (KV Caching): عند إنتاج رمز جديد، يتم حساب ( K_{\text{new}} ) و ( V_{\text{new}} ) لهذا الرمز فقط، ثم تضاف إلى القيم المخزنة مؤقتًا. يتم تحديث الذاكرة المؤقتة بشكل تدريجي مع كل رمز جديد. في الممارسة العملية، تكون هذه الذاكرة المؤقتة قاموسًا لكل طبقة، به مفاتيح "key" و "value"، كل منها بحجم ( (\text{batch_size}, \text{num_heads}, \text{seq_len_cached}, \text{head_dim}) ). هذه هي الأساس الذي يتيح لنماذج اللغة الكبيرة الحديثة توليد نواتج طويلة بكفاءة عالية. تطبيق ذاكرة التخزين المؤقت (KV Caching) في nanoVLM: من النظرية إلى الممارسة الآن بعد فهم النظرية وراء ذاكرة التخزين المؤقت، دعنا نرى كيفية تنفيذها في مكتبة nanoVLM. هذه هي البيئة المثالية للتجربة، حيث أنها تحتوي على كود مختصر ومتكامل. 1. تحديث ذاكرة التخزين المؤقت في كتلة الانتباه (Attention Block) في فئة LanguageModelGroupedAttention، نعدل الدالة forward لقبول تحديث ذاكرة التخزين المؤقت للمفاتيح والقيم (block_kv_cache). - سابقًا، كان النموذج يعيد حساب ( K ) و ( V ) في كل خطوة من خطوات التوليد. - الآن، نحسب ( K_{\text{new}} ) و ( V_{\text{new}} ) للرمز الحالي فقط، ثم نضيفها إلى القيم المخزنة مؤقتًا. ```python def forward(self, x, cos, sin, attention_mask=None, block_kv_cache=None): is_prefill = block_kv_cache is None B, T_curr, C = x.size() # تحويل المدخلات إلى Q, K, V q_curr, k_curr, v_curr = project_current_tokens(x) q, k_rotated = apply_rotary_pos_embd(q_curr, k_curr, cos, sin) if not is_prefill and block_kv_cache['key'] is not None: # إضافة المفاتيح والقيم الجديدة إلى الذاكرة المؤقتة k = torch.cat([block_kv_cache['key'], k_rotated], dim=2) v = torch.cat([block_kv_cache['value'], v_curr], dim=2) else: # أول عملية (pre-fill) — لا يوجد ذاكرة مؤقتة k, v = k_rotated, v_curr block_kv_cache = {'key': k, 'value': v} return attention_output, block_kv_cache ``` 2. تتبع الذاكرة المؤقتة عبر الطبقات في فئة LanguageModel، نقدم تتبع الذاكرة المؤقتة لكل طبقة. يساعد الحجة start_pos النموذج في حساب الترميز الموضعي الصحيح للرموز الجديدة المنتجة. ```python def forward(self, x, kv_cache=None, start_pos=0): T_curr = x.size(1) position_ids = torch.arange(start_pos, start_pos + T_curr, device=x.device) cos, sin = self.rotary_embd(position_ids) for i, block in enumerate(self.blocks): # نقل ذاكرة التخزين المؤقت لكل طبقة x, kv_cache[i] = block(x, cos, sin, attention_mask, kv_cache[i]) return x, kv_cache ``` 3. الفصل بين مرحلة الـ prefill ومرحلة الـ decode في حلقة التوليد التغيير الأكبر هو في طريقة generate() لنموذج VisionLanguageModel. نقسم التوليد إلى مرحلتين: - مرحلة الـ prefill (بناء الذاكرة): معالجة الرسالة المبدئية وملء الذاكرة. - مرحلة الـ decode (رمزًا تلو الآخر): إنتاج رمز واحد تلو الآخر باستخدام الذاكرة المخزنة مؤقتًا. هنا الكود المقابل: ```python PRE-FILL: معالجة الرسالة المبدئية، ملء الذاكرة prompt_output, kv_cache_list = self.forward( inputs, kv_cache=None, start_pos=0 ) DECODE: إنتاج رمز واحد تلو الآخر باستخدام الذاكرة المخزنة مؤقتًا for i in range(max_new_tokens): next_token = sample_from(prompt_output) decode_output, kv_cache_list = self.forward( next_token, kv_cache=kv_cache_list, start_pos=current_position # يتم تحديثها مع كل خطوة ) prompt_output = decode_output ``` من خلال فصل هاتين المرحلتين، نتجنب الحسابات التكرارية غير الضرورية، مما يؤدي إلى تحسين كبير في سرعة الاستدلال، خصوصًا للرسائل طويلة الأمد. ملخص التغييرات | الوحدة | السلوك الأصلي | السلوك الجديد | |--------|--------------|-------------| | LanguageModelGroupedAttention.forward | يعيد حساب ( Q ), ( K ), ( V ) في كل خطوة | يستخدم ويحدث ذاكرة التخزين المؤقت | | LanguageModel.forward | لا يحتفظ بذاكرة للحالة السابقة | يتتبع ذاكرة التخزين المؤقت لكل طبقة، ويتعامل مع start_pos | | VisionLanguageModel.generate | حلقة توليد في مرحلة واحدة | مقسمة إلى مرحلتين: prefill و decode | ملخص: لماذا تهم ذاكرة التخزين المؤقت (KV Caching) النمو التدريجي: الذاكرة تزيد بمقدار صف واحد لكل رمز جديد. فك الرموز حسب الموضع: start_pos يضمن صحة حسابات الترميز الموضعي. الكفاءة: تقلل من الحسابات لكل رمز إلى ( O(\text{seq len}) ) بدلاً من التربيعية. ذاكرة التخزين المؤقت (KV Caching) تساعد في القضاء على الحسابات غير الضرورية أثناء توليد النص الذاتي الانحداري، مما يمكن من تحسين سرعة الاستدلال وزيادة الكفاءة، خاصة في سلاسل طويلة وتطبيقات الزمن الحقيقي. هذه التقنية هي توازن بين السرعة والذاكرة، وقد تكون عيوبها تعقيد الكود وتقيد بعض طرق الاستدلال الأكثر تعقيدًا، مثل البحث الشعاعي (beam search). تعتبر ذاكرة التخزين المؤقت طريقة شائعة لتسريع استدلال النماذج اللغوية الكبيرة، مما يجعل تشغيلها ممكنًا على الأجهزة الشخصية، والآن أصبح لديك فكرة عن كيفية عملها أيضًا! تقييم الحدث من قبل المختصين تقنية ذاكرة التخزين المؤقت (KV Caching) تعتبر من التقنيات الأساسية التي تساهم بشكل كبير في تحسين أداء النماذج اللغوية الذاتية الانحدارية. العديد من المختصين في مجال الذكاء الاصطناعي يرون أنها تلعب دورًا حاسمًا في تطبيقات مثل الذكاء الصوتي والترجمة الآلية، حيث تجعل تشغيل النماذج المعقدة ممكنًا على الأجهزة الضعيفة نسبيًا. هذا التحسين في الكفاءة يمكن أن يؤدي إلى توفير كبير في الوقت والموارد، مما يجعل تكنولوجيا النماذج اللغوية أكثر قابلية للتطبيق في مجموعة واسعة من السياقات. نبذة تعريفية عن nanoVLM nanoVLM هي مكتبة صغيرة ومكتوبة بالكامل باستخدام PyTorch، تهدف إلى تدريب نماذج اللغة البصرية (Vision Language Models) الخاصة بك. تم تصميمها لتكون مبسطة وواضحة، مما يجعلها مثالية للمتعلمين والمطورين الراغبين في فهم جوهر هذه النماذج وكيفية تحسين أدائها. بفضل تطبيق تقنية ذاكرة التخزين المؤقت، أصبحت nanoVLM أكثر فعالية وكفاءة في توليد النصوص الطويلة والمعقدة.