← voidwest ember engineering earlier SIMD work

Q8 decode أسرع بدون إبقاء نسختين من النموذج في الذاكرة

2026-07-30 · تقرير هندسي عن دورة حياة packed Q8_0 · نموذج Llama-3.2-1B على معالج Tiger Lake واحد

مسار packed Q8 الجديد رفع median للـ full-model decode من 21.968 إلى 30.888 tok/s في تشغيل Llama-3.2-1B المقاس هنا. التحسن 40.6%. لكن النسخة الأولى بدت أيضاً كأنها تحتاج نسخة إضافية بحجم نموذج تقريباً داخل الذاكرة. رقم السرعة كان حقيقياً. قراءة الذاكرة الأولى كانت ناقصة.

السؤال صار أضيق: هل أقدر أحتفظ بالـ layout المناسب للـ decode، ثم أخرج صفحات source projections بعد prefill، بدون ما يتغير الخرج؟ في هذا النموذج وعلى هذا المعالج، نعم. هذا دليل هندسي محلي، مو claim عامة عن Q8 inference.

النتيجة المقاسة21.968 → 30.888 tok/s في median full-model decode، بزيادة 40.6%.
أفضل اختيار جزئي30.199 tok/s بدون down projections؛ احتفظ بـ 92.9% من التحسن الكامل المقاس.
كلفة البدايةpacking لكل projections أخذ قرابة 450 ms؛ الاختيار الجزئي أخذ 350 ms.
حد الادعاءنموذج Llama-3.2-1B Q8_0 واحد، لابتوب Intel بأربع cores، وثلاث إعادات بترتيب عشوائي.

ما الذي تغير في hot path

التحديث السابق خفف الشغل حول matrix kernels. single-token decode صار يعيد استخدام workspace محاذي للـ cache بدل بناء tensors وسيطة في كل طبقة. KV cache صار FP16. مسارات Q/K/V وgate/up تشارك activation quantization واحدة. والـ LM head الكبير أخذ interleaved VNNI layout خاص. كذلك kernel الصفوف العادي توسع من 4 إلى 8 output rows وأضاف prefetch صريح.

التحديث الأخير يغير layout الأوزان نفسها. Llama projections المؤهلة تنحزم في tiles من 16 output rows. لكل block من 32 input coordinates، بايتات نفس الأربع coordinates عبر الصفوف الستة عشر تجلس بجانب بعض. بعدها يقدر AVX-512 VNNI يبث أربع activation bytes، يقرأ weight group واحد بحجم 64 bytes، ويجمع 16 outputs.

row-contiguous source
row 0  [scale][q0 q1 ... q31]
row 1  [scale][q0 q1 ... q31]
...

packed 16-row tile
[rows 0..15, q0..q3]
[rows 0..15, q4..q7]
...
[rows 0..15, q28..q31]
[16 fp16 scales]

كل packed tile/block حجمه 544 bytes، وهو نفس الحجم المشفر لـ 16 Q8_0 blocks عادية. packing ما يضغط الوزن أكثر؛ فقط يغير أي بايتات تصل مع بعض. المعالجات غير المدعومة، ومسار Qwen ذي split-half RoPE، والـ projections التي فيها bias، ومفتاح الرجوع EMBER_LLAMA_PACKED_Q8=0 تبقى على المسار العام.

القياس يغطي decode loop كامل

كل trial اشتغل في process جديد على Intel i5-1135G7. ثبتت أربعة Rayon workers على physical CPUs من 0 إلى 3 واستبعدت SMT siblings. حرارة package عند البداية كانت 80 °C أو أقل. prompt من ستة tokens مر عبر generic prefill، ثم النموذج ولد 128 greedy tokens بحتمية. أوضاع lifecycle واختيارات projections العشرة تغير ترتيبها ببذرة ثابتة عبر ثلاث إعادات.

كل الـ 30 trials المقبولة أعطت نفس hash للـ generated tokens: fnv1a64:9f8e8158645ba677. تشغيل أقدم جمع الأوضاع بدل خلطها، ووضع إعدادين متطابقين على فرق 13%. رفضت أرقامه. في التشغيل العشوائي صار الفرق بين نفس الإعدادين 0.2%.

رسم أعمدة أفقي لسرعة full-model decode: control عند 21.968 token في الثانية، gate وup عند 27.659، attention وgate وup بدون down عند 30.199، وكل projections المؤهلة عند 30.827. رسم أعمدة أفقي لسرعة full-model decode: control عند 21.968 token في الثانية، gate وup عند 27.659، attention وgate وup بدون down عند 30.199، وكل projections المؤهلة عند 30.827.
median لثلاث processes جديدة بترتيب عشوائي. هذا full-model decode بعد generic prefill، وليس kernel microbenchmark. قيمة packed MiB هي حجم representation للـ projections المختارة.

packing لكل شيء وصل إلى 30.827 tok/s داخل selection matrix. لما حزمت gate/up وQ/K/V/O وتركت down على layout الصفوف، وصلت النتيجة إلى 30.199 tok/s. الاختيار الأصغر حجمه 714 MiB واحتفظ بـ 92.9% من التحسن، مع خفض packing time من 452.9 إلى 350.3 ms وخفض transient peak RSS من 3,104.5 إلى 2,827.4 MiB.

down هو negative result المفيد. إضافته فوق gate/up حجزت 272 MiB أخرى، لكن median ارتفع فقط 0.393 tok/s. operator profile يعطي نفس الصورة المحلية: Q وO وgate وup تحسنت تقريباً 1.7–1.8x داخل projection calls، بينما down كان 1.03x والـ LM head المنفصل 1.01x. هذا لا يفسر السبب. ما زلت أحتاج hardware counters وحجم نموذج ثانٍ.

ليش كانت قراءة RSS الأولى ناقصة

GGUF loader يبقي أوزان Q8_0 الأصلية row-contiguous داخل read-only mmap. الأوزان packed تذهب إلى anonymous memory. بعد packing مباشرة يقدر Ember يستدعي MADV_DONTNEED على source ranges. في decode-only run، هذا يبدو مثل استبدال layout بآخر.

generation العادي يبدأ بـ prefill. generic prefill يقرأ source layout ويرجع صفحاته إلى الذاكرة. إذا ما صار eviction ثانية عند حد الانتقال، يبقى layoutان resident. لذلك نتيجة decode-only قليلة الزيادة في الذاكرة ما كانت تصف دورة التشغيل العادية.

  1. packابنِ layout الستة عشر صفاً من projection bytes الموجودة في mmap.
  2. generic prefillاستخدم source layout عندما تعيد صفوف prompt المتعددة استخدام الأوزان.
  3. re-evictأخرج mapped projection pages بعد prefill مع إبقاء mapping صالحاً.
  4. packed decodeولد token واحداً في كل مرة بدون إعادة fault لتلك source pages.

في durable all-projection mode هبط file-backed PSS من 1,265.1 إلى 279.0 MiB بعد re-eviction، وبقي هناك خلال 127 packed decode evaluations. final RSS كان 2,113.6 MiB. أما duplicate-layout mode المقصود للمقارنة فانتهى عند 3,099.7 MiB بدون decode أسرع. هذه هي النتيجة التي تدعم replacement-style residency.

كلفة البداية لازم تسترد نفسها

decode الأسرع في steady state لا يعني أن generation القصير يصير أسرع. all-projection mode أضاف قرابة 450 ms من packing إلى time-to-first-token، ووصل break-even عند قرابة 36 generated tokens لهذا prompt. استبعاد down خفض النقطة المقاسة إلى 29 tokens.

ممكن نؤجل packing إلى ما بعد prefill ونخرج token الأول قبل الوقفة. لكن هذا يصنع فجوة تقارب 438 ms بين التوكن الأول والثاني. نقلنا latency من مكان إلى آخر؛ ما أزلناها. السياسة المناسبة تعتمد على عمر process وعدد tokens المتوقع.

حدود النتيجة

الادعاء الأضيق يكفي الآن: packed kernel سرّع decode، وحد المرحلة جعل السرعة متوافقة مع durable source-page eviction في هذا workload. الاختبار التالي هو Llama-3.2-3B، قبل تعديل kernel مرة ثانية.