يصنّف الـ public API عناصر experiment الظاهرة فيه على
أنها experimental، ويستخدم الكود
#[non_exhaustive] حيث يلزم. وحتى تستقر هذه
الـ API، قد تتغير الأسماء والحقول.
لا تشير أسماء الـ hooks إلى خطوات implementation قابلة للتبديل، بل إلى مراحل ذات معنى محدد. وينفذها ember synchronously وبترتيب ثابت، مع احتفاظ كل معمارية بمسارها العددي الصريح إلى الحد نفسه.
الـ lifecycle
في generation run عادي وناجح، يستدعي ember
on_model_loaded() مرة واحدة، ثم
before_prefill() مرة واحدة. بعد ذلك يجري تقييم
prefill واحدًا عبر layer hooks وlogits hooks، ويليه صفر أو
أكثر من تقييمات decode عبر الـ hooks نفسها. وعند اكتمال
المسار، يستدعي on_generation_complete() مرة
واحدة.
on_model_loaded()before_prefill()on_generation_complete()لا يواصل ember التنفيذ إلا بعد عودة الـ hook الحالي. فإذا فشل الـ hook، يتوقف generation الحالي، ولا يستدعي ember الـ hooks اللاحقة.
| النطاق | الـ hook | القيمة المكشوفة | الوصول |
|---|---|---|---|
| النموذج | on_model_loaded |
ModelContext |
metadata |
| الإعداد | before_prefill |
ExecutionContext |
metadata |
| كل طبقة | before_layer |
الـ hidden state الداخلة | view mutable وغير مالكة |
| كل طبقة | after_attention |
مساهمة attention في الـ residual | view mutable وغير مالكة |
| كل طبقة | after_mlp |
مساهمة MLP في الـ residual | view mutable وغير مالكة |
| كل طبقة | after_layer |
الـ hidden state المكتملة للطبقة | view mutable وغير مالكة |
| التقييم | before_logits |
hidden state لآخر token بعد final normalization | view mutable وغير مالكة |
| التقييم | after_logits |
الـ logits التي يعيدها المسار بعد postprocessing الخاص بالعائلة | view mutable وغير مالكة |
| generation | on_generation_complete |
GenerationContext |
metadata |
يبدأ تشغيل الـ hooks الستة التي تحمل tensors تحت
phase=Prefill. ثم يشغلها
ember تحت phase=Decode مع كل تقييم
decode أحادي التوكن. ويبدأ ترقيم الطبقات من الصفر، وفق
ترتيب تنفيذ النموذج.
واجهة الـ trait
لا يلزم تنفيذ سوى name. وتعيد كل method في
الـ lifecycle افتراضيًا نتيجة ناجحة من دون أي عمل؛ وبذلك
لا تطبّق التجربة المدمجة إلا الـ methods التي تحتاج إليها.
pub trait Experiment: Send {
fn name(&self) -> &'static str;
fn on_model_loaded(
&mut self,
ctx: &ModelContext<'_>,
) -> Result<(), ExperimentError> { Ok(()) }
fn before_prefill(
&mut self,
ctx: &ExecutionContext<'_>,
) -> Result<(), ExperimentError> { Ok(()) }
fn before_layer(
&mut self,
ctx: &LayerContext<'_>,
hidden: &mut TensorAccess<'_>,
) -> Result<(), ExperimentError> { Ok(()) }
fn after_attention(
&mut self,
ctx: &LayerContext<'_>,
attention_output: &mut TensorAccess<'_>,
) -> Result<(), ExperimentError> { Ok(()) }
fn after_mlp(
&mut self,
ctx: &LayerContext<'_>,
mlp_output: &mut TensorAccess<'_>,
) -> Result<(), ExperimentError> { Ok(()) }
fn after_layer(
&mut self,
ctx: &LayerContext<'_>,
hidden: &mut TensorAccess<'_>,
) -> Result<(), ExperimentError> { Ok(()) }
fn before_logits(
&mut self,
ctx: &ExecutionContext<'_>,
hidden: &mut TensorAccess<'_>,
) -> Result<(), ExperimentError> { Ok(()) }
fn after_logits(
&mut self,
ctx: &ExecutionContext<'_>,
logits: &mut TensorAccess<'_>,
) -> Result<(), ExperimentError> { Ok(()) }
fn on_generation_complete(
&mut self,
ctx: &GenerationContext<'_>,
) -> Result<(), ExperimentError> { Ok(()) }
}
أنواع الـ context
يعرّف العقد أربعة أنواع context، تأتي جميعها في بُنى خفيفة لا يمكن تعديلها، وتستعير البيانات التي تحتاج إليها. ولا تتيح هذه البُنى token buffers أو metadata GGUF الكاملة أو weight handles أو caches أو تفاصيل الـ backend الداخلية.
| الـ context | الحقول | تُستخدم في |
|---|---|---|
ModelContext |
العائلة، ومعرّف اختياري، والـ architecture، وعدد الطبقات، وحجم hidden state | تحميل النموذج؛ وتدخل ضمن بقية الـ contexts |
ExecutionContext |
النموذج، والـ phase، وموضع البداية، وعدد input tokens، وطول التسلسل الناتج، وحالة tracing | hooks الخاصة بالـ prefill والطبقات والـ logits |
LayerContext |
execution context ورقم طبقة يبدأ ترقيمه من الصفر | أربعة tensor hooks في كل طبقة |
GenerationContext |
النموذج، وعدد prompt tokens، وعدد التوكنات المولدة، وعدد تقييمات decode، وحالة tracing | عند الاكتمال الناجح |
موضع التنفيذ
- start_position
- الموضع المطلق لأول input token في هذا التقييم
- input_token_count
- عدد input tokens التي يعالجها هذا التقييم
- sequence_length
start_position + input_token_countبعد إدخال التوكنات الحالية- token_position()
-
يعيد موضعاً مطلقاً فقط للتقييمات التي تحتوي token
واحداً، كما في decode العادي؛ وإلا يعيد
None
الـ enums الصغيرة
- ModelFamily
Llama,Qwen3,Gemma4- ExecutionPhase
Prefill,Decode- TracingState
Disabled,Enabled- TensorDType
F32في v0.1
TensorAccess
تتيح TensorAccess الوصول إلى activation قائمة
من خلال view لا تملك البيانات، مع السماح بتعديلها. وفي
v0.1، تكون هذه الـ activation contiguous وثنائية
الأبعاد ومن نوع f32، ويثبت shape على
[rows, columns]. وتظل ملكية allocation والـ
buffer للـ runtime، فلا يحتاج الـ hook إلى نسخة مستقلة.
- فحص shape وdtype؛
- قراءة قيم f32 المكشوفة؛
- تعديل القيم داخل الـ slice الموجودة؛
- تصفير القيم الموجودة.
- تغيير الحجم؛
- إعادة تخصيص الذاكرة؛
- تغيير shape أو dtype؛
- استبدال الـ buffer أو أخذ ملكيته؛
- الوصول إلى الأوزان أو caches أو scratch storage أو بيانات التوكنات.
let shape: &[usize; 2] = tensor.shape();
let dtype: TensorDType = tensor.dtype();
let observed: &[f32] = tensor.values();
// explicit intervention on the same allocation:
for value in tensor.values_mut() {
*value *= 0.5;
}
// or replace existing values with zero:
tensor.zero();
لا يفرض نظام الأنواع في الواجهة سلوك observation-only؛
لذلك يتحمل implementation مسؤولية هذا الضمان. يحافظ استخدام
values() على التنفيذ، في حين قد يغيّره استخدام
values_mut() أو zero().
دلالات عائلات النماذج
تدل الأسماء المشتركة على الحدود نفسها، لكنها لا تجعل الـ blocks الداخلية متطابقة. فلكل معمارية ترتيبها وعملياتها العددية في الوصول إلى الحد، وتبقي واجهة الـ hooks هذه الفروق ظاهرة.
| الحد | LLaMA / Qwen3 | Gemma 4 |
|---|---|---|
after_attention |
بعد O projection، وقبل residual addition | بعد O projection وpost-attention RMS norm، وقبل residual addition |
after_mlp |
بعد down projection، وقبل residual addition | بعد down projection وpost-FFN RMS norm، وقبل residual addition |
after_layer |
بعد MLP residual addition | بعد عمليات الـ residual وPLE وscaling لناتج الطبقة |
before_logits |
hidden state لآخر token بعد final normalization | hidden state لآخر token بعد final normalization |
after_logits |
LM head output | ناتج LM head بعد final logit softcap |
يستخدم Qwen3 تنفيذ الـ block المشترك مع عائلة LLaMA، مع الحفاظ على split-half RoPE وQK normalization وترتيب العمليات الخاص به. لذلك لا يعني اشتراكه في اسم الـ hook أن ما تكشفه المعماريتان قابل للتبادل.
تعمل single-token hooks النشطة في LLaMA مباشرة على decode workspace مخصصة مسبقًا، من غير أن تفرض generic tensor path. وفي Gemma، لا يتغير dispatch بين مساري MLP الـ packed والـ generic.
المسار المعطّل
لا يبني generation العادي
ExperimentRunner أصلًا. وتكون دوال النموذج
generic على نوع hooks adapter؛ فعند التعطيل تستخدم
DisabledHooks، وهو adapter بحجم صفر، وتكون
الـ methods داخله #[inline(always)] no-ops.
pub(crate) struct DisabledHooks;
impl<T, E> LayerHooks<T, E> for DisabledHooks {
#[inline(always)]
fn after_attention(
&mut self,
_layer_index: usize,
_tensor: &mut T,
) -> Result<(), E> {
Ok(())
}
// the other disabled methods have the same shape.
}
أظهر فحص المسار المعطّل أن الكود الناتج لا يتضمن أي experiment-related dispatch. كذلك لم ترصد الاختبارات أي نمو في عدد allocations لكل طبقة بعد warm-up، وظلت قياسات A/B المضبوطة للـ prefill والـ single-token decode ضمن ضجيج القياس.
يواصل decode المحسّن في LLaMA استخدام workspace المخصصة مسبقًا، من غير أن تفرض الـ hooks الـ generic tensor path. ولا يتغير في Gemma dispatch الـ MLP بين packed وgeneric.
Failure path
تعيد تنفيذات الـ hooks
ExperimentError، ثم يغلّفه
ExperimentRunner داخل
ExperimentFailure قبل أن يمر إلى مسار الأخطاء
العادي في ember.
| الحقل | يُضمّن عندما يكون متاحاً |
|---|---|
| اسم التجربة | دائماً |
| اسم الـ hook | دائماً |
| مرحلة التنفيذ | أخطاء prefill والطبقات والـ logits |
| رقم الطبقة | أخطاء كل طبقة |
| رسالة الخطأ الأصلية | دائماً |
experiment 'my-experiment' failed in after_attention
(phase=decode, layer=7): expected a finite activation
تعمل الـ hooks synchronously. فإذا فشل أحدها، توقف generation الحالي، ولم يسجل ember اكتماله على أنه ناجح، ولم يستدعِ الـ hooks اللاحقة.
حدود واجهات الفحص
لا تعمل التجارب النشطة في v0.1 مع استخراج
hidden states أو probes أو --dump-layers
أو --dump-logits، ولا مع أوامر demo أو
interactive أو benchmark الفرعية. ويرفض
ember هذه التركيبات حتى لا يعرض قيمًا يلتبس
معناها.
لم يحدد ember بعد ما إذا كانت واجهات الفحص ستعرض القيم قبل intervention أم بعده. فالمسألة تتعلق بمعنى القيم المعروضة، لا بغياب وسيلة لنسخ البيانات.
لا يغيّر هذا العقد tracing العادي من دون تجربة، ولا استخراج
hidden states أو layer dumps أو logits dumps أو تقارير
benchmark أو dispatch للـ packed kernels. ويمكن تشغيل
structured tracing مع تجربة؛ وعندئذ يتيح
ExecutionContext الاطلاع على حالته من دون
تعديلها. ولا تستبدل الـ hooks trace stream أو تعيد تعريفه.