← voidwest    research notes

قبل ما نربط ذاكرات KV بين النماذج، لازم نخليها قابلة للقياس

حد قابل للتحقق قبل تجربة ridge بين نموذجين Qwen
محمد الثبيتي · 2026-08-09
Ember KV cache reproducibility causal diagnostics Qwen LLaMA

الملخص

بدأت هذا العمل من خطوة مغرية: آخذ KV cache من نموذج، وأتعلم linear map صغير، ثم أختبر إذا كان نموذج ثاني يقدر يكمل منه من دون ما يعيد حساب الـprefix كامل.

لكن ما بنيت الـmapper.

بدل ذلك، قضيت اليوم أخلي حالة الـKV شيئاً يقدر Ember يصدّره، ويتحقق منه، ويستورده، ويعيد تشغيله، ويقارنه، ويغيره تحت عقد واضح. النتيجة هي snapshot من ثلاثة ملفات، وإعادة تشغيل مطابقة داخل النموذج نفسه، ومقاييس على مستوى كل طبقة ورأس، وتشخيصات لـattention output والـlogits، ومسار تغيير مضبوط داخل الذاكرة.

هذا أقل إثارة من عرض transfer. لكنه الشغل اللي يخلي أي نتيجة نقل لاحقة قابلة للفهم.

الحالة

يقدر Ember الآن يقيس حالة KV المتوافقة ويطبق عليها تغييراً سببياً مضبوطاً. هذه مو نتيجة نقل بين نموذجين، ومو دليل إن ridge mapper راح ينجح، ومو ادعاء تسريع.

ليش كان لازم نأجل الـmapper؟

الـcache المحوّل ممكن يفشل لأسباب كثيرة ما لها علاقة ببعض. ممكن يكون الـsnapshot ناقص، أو تسلسل الـtokens مختلف، أو مؤشر الـcache متقدم أو متأخر بموقع واحد. ممكن نفسر RoPE أو Q/K normalization في فضاء غلط، أو يتغير بت واحد عند الاستيراد، أو يأخذ النموذج مسار تنفيذ مختلف. وممكن ببساطة يكون الـmap المتعلم ضعيفاً.

إذا جمعنا كل هالأخطاء في نص مولّد واحد، فالنتيجة السلبية ما تقول لنا كثير. وحتى النص اللي شكله صحيح ممكن يخفي انحرافاً عددياً، أو تعادلاً محظوظاً في top-1، أو مسار إعادة تشغيل ما استخدم الحالة اللي نقصدها أصلاً.

لذلك صار السؤال الأول أضيق: هل يقدر Ember يجمّد حالة KV الأصلية ويثبت إن استيرادها ما يغير شيئاً تحت عقد صارم داخل النموذج نفسه؟

بعدها يجي السؤال الثاني: لما نغير الحالة عمداً، وين يظهر الفرق أول مرة؟ في إعادة بناء K/V؟ أو في attention output؟ أو في الـlogits؟ أو في مسار التوليد الجشع؟

وش صار جزءاً أساسياً؟

الأثر الجديد له إصدار مستقل باسم ember.kv-snapshot.v1. ما غيّر عقد خطة التنفيذ المجمّد، ولا صيغة حزم التجارب في Ember.

snapshot/
├── manifest.json
├── keys.f16le
└── values.f16le

تُخزّن K وV كبتات f16 مضغوطة بالترتيب [layer][kv_head][position][dimension]. ما نحفظ المساحة غير المستخدمة من الـcache. ويسجل الـmanifest بصمات النموذج والـtokenizer، وهندسة الـcache، وتخطيط RoPE وtheta، ودلالة Q/K normalization، وحالة V، ووضع التنفيذ وبصمته، وبصمة prefix tokens، وresume token، وبصمات الملفات.

الاستيراد صارم عمداً. ما فيه خيار «قريب بما يكفي». أي اختلاف في هوية النموذج، أو الـtokenizer، أو الهندسة، أو دلالة المواقع والتمثيل، أو مصدر التنفيذ العددي يُرفض قبل decode.

ember kv export --model MODEL.gguf --tokenizer TOKENIZER.json   --arch qwen3 --prompt 'prefix' --output runs/kv/prefix
ember kv verify runs/kv/prefix
ember kv replay --snapshot runs/kv/prefix   --model MODEL.gguf --tokenizer TOKENIZER.json --arch qwen3

فرق الموقع الواحد اللي لازم يظل واضح

الـsnapshot يغطي الـprefix المكتمل، لكن الـtoken المختار من prefix-boundary logits ما دخل الـcache إلى الآن. إذا كان طول الـprefix هو P، فالـresume token مكانه المطلق P.

أول forward في إعادة التشغيل يقيّم هذا الـtoken، ويضيف صف K/V الخاص به، وينتج logits للموقع P+1. لو أدخلنا آخر token من الـprompt مرة ثانية، نكون كررناه. ولو قلنا إن الـsnapshot يحتوي boundary logits الأصلية، نكون وصفناه بشكل غلط.

هذا يبدو مثل محاسبة بسيطة للمواقع. لكنه بالضبط نوع الخطأ اللي ممكن يخلي تجربة transfer تبدو فاشلة بينما المشكلة الحقيقية هي محاذاة التسلسل.

كيف تحققنا من إعادة التشغيل المطابقة؟

التحقق بين العمليات ما قارن النص المفكوك، وما استخدم تسامح allclose. حفظ full f32 logits من التوليد المتصل، وحد التصدير، وإعادة التشغيل:

native[N,V] == concatenate(export_boundary[1,V], replay[N-1,V])

المساواة هنا تعني تطابق بتات f32. المصفوفة المكتملة شملت Llama-3.2-1B وQwen2.5-1.5B، وثلاثة مسارات Q8_0/Q6_K/Q4_K_M، وprompts ثابتة بالإنجليزي والعربي.

نتيجة إعادة التشغيل داخل النموذج نفسه

نجحت 12/12 خلية و24/24 ملاحظة زمنية. من أصل 13,449,216 قيمة f32 تمت مقارنتها، ما ظهر أي اختلاف في البتات. ونجحت العمليات الفرعية الـ72 كلها.

هذا يثبت حد الـsnapshot/replay في الحالات المختبرة. ما يثبت صحة المرجع لكل نموذج، وأرقام التوقيت المسجلة ملاحظات فقط، مو ادعاء تسريع.

القياس قبل الـmapping

إعادة التشغيل المطابقة هي حالة الضبط. بعدها أضفت ember kv compare، وهو ما يقارن حالتين إلا إذا ثبت إن إحداثيات النموذج والـprefix متطابقة.

لكل طبقة ورأس KV، التقرير يعطي لـK وV:

الحدود الاختيارية تنتج قائمة فشل ثابتة وأول تجاوز. وتقرير JSON ما يحتوي توقيت، أو اسم جهاز، أو process ID، أو حقول غير مرتبة.

ember kv compare LEFT RIGHT --json --r2   --max-abs 0.001 --min-cosine 0.999

ومع تحميل النموذج الهدف نفسه، يقدر الأمر يدخل مسار reference-greedy مشترك إلى الـcaches. بعدها يقارن ناتج attention O-projection الدلالي في كل طبقة، والـfull logits، واتفاق top-1، وأول اختلاف في التوقع.

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

ضابط سببي من دون mapper provenance وهمي

احتجت أتأكد إن مسار التشخيص يكتشف تغييراً حقيقياً في الـcache قبل ما أعطيه ناتج mapper. الضابط الضيق يصفّر أو يضرب بقيمة ثابتة رأس K/V واحد عبر الـprefix داخل الذاكرة.

العملية لها إيصال مكتوب يحدد الـsnapshot الأصلي، والطبقة، والرأس، واختيار K/V، وبتات معامل الضرب، وعدد العناصر المتأثرة. ما نقدر نحفظها كـember.kv-snapshot.v1، وما نحط قيمة وهمية في حقل بصمة الـmapper. وإعادة التشغيل العادية ما زالت تقبل native snapshots فقط.

في smoke على Qwen2.5-1.5B Q6_K planned، مقارنة الـsnapshot بنفسه أعطت فروقاً صفرية، واتفاق top-1، ونفس التسلسل الجشع القصير. تصفير K وV في الطبقة 0 والرأس 0 أعطى attention-output cosine يقارب 0.886 في الطبقة الأولى، وfinal-logit cosine يقارب 0.9953. بقي top-1 متفقاً في الأفق القصير جداً.

كيف نقرأ هذا الـsmoke؟

التشغيل المعدّل يثبت إن الأداة تقدر تحدد تغيير KV مضبوط وتتابع انتقال أثره. عدم انقلاب الـtoken خلال توقع واحد مو دليل متانة، وقيم الـcosine مو دليل على جودة نقل بين نموذجين.

وش يثبته الدليل الحالي؟

هذي نتائج عن صلاحية أداة القياس. تخلي تجربة mapper لاحقة أسهل في التشخيص؛ لكنها مو دليل على نجاح الـmapper.

وش ما يثبته هذا العمل؟

attention-output cosine مفيد، لكنه مو احتمال انتباه، ومو تفسير سببي لوحده. وK/V R² إحصاء لإعادة البناء، مو دليل إن النموذج الهدف يستخدم الـcache المعاد بناؤه. واتفاق التوليد لفترة قصيرة سلوك فعلي، لكنه يظل مقياساً محدود الأفق ويعتمد على الـprompt.

التجربة الصغيرة المقصودة لاحقاً

تجربة ridge ممكن تجي لاحقاً، ولازم تظل أصغر من البنية اللي صارت تقيسها.

النطاق المقترح الآن زوج واحد ثابت من Qwen إلى Qwen، بنفس الـtokenizer وهندسة KV متطابقة. تُحوّل المفاتيح في فضاء المحتوى المعلن بعد التطبيع وقبل RoPE، وتُحوّل القيم في حالتها المعلنة. لكل طبقة ورأس هدف ridge map مغلق وثابت، مع تقسيم معايرة مجمّد، وقيمة lambda واحدة، ومن دون بحث عن الطبقات.

التجربة تقارن identity baseline مع إعادة بناء ridge أولاً، ثم تستخدم تشخيصات الانتباه والـlogits على نفس الإدخال، وبعدها المسار الجشع المستقل. ما نعمم الهندسة، وما نحسن مسار التشغيل، وما نضبط kernels، وما ننشئ صيغة إنتاج لـtransformed snapshots.

وإذا بقيت الملفات المحلية الحالية هي الزوج الوحيد، فالمسار من Q4_K_M إلى Q8_0 بيكون تجربة على حد التكميم في نفس النموذج الأساسي، مو دليلاً على نقل بين حجمين مختلفين. لازم هذا الفرق يظل واضحاً في العنوان، والـmetadata، والخلاصة.

الخلاصة

اليوم ما أنتج mapper. أنتج الشروط اللي تخلي فشله صريحاً وقابلاً للقياس.

صار للحالة الأصلية هوية صارمة. وصار لإعادة التشغيل ضابط مطابق على مستوى البتات. وصار لإعادة البناء مقاييس على مستوى الطبقة والرأس. وصار للأثر اللاحق قياس انتباه وlogits على نفس الإدخال. وصار للانحراف السلوكي مسار جشع مستقل. وصار للحالة المعدلة مصدر واضح من دون ما ندّعي إنها أثر متعلم.

هذا هو الحد الممل اللي أبيه قبل ما أسأل إذا كان KV cache من Qwen يقدر يقوم مقام cache من Qwen ثاني.