Skip to main content

eredu_runtime/
expert.rs

1//! Runtime ownership boundary for routed expert acquisition and residency.
2
3use eredu_nn::{
4    DistributedNeuralBackend, GroupSelection, GroupedGatedProductOperator, GroupedNeuralBackend,
5    GroupedRelu2Operator, Tensor, TensorParallelGroupedOutput,
6};
7
8use crate::ExpertPass;
9
10mod route_intervention;
11use crate::{
12    observe_and_intervene, ActivationObserver, ParameterBankAccess, ParameterBankKey,
13    ReplicatedTextMaterializationTask, ReplicatedTextParameterOwner, RoutingObservation,
14    WeightLoweringKind,
15};
16pub use route_intervention::{select_routes_with_observer, select_routes_with_provider};
17
18/// One exact selected parameter in an independently addressable bank member.
19#[derive(Debug, Clone, Eq, PartialEq)]
20pub struct AddressableBankParameter {
21    binding_name: String,
22    task: ReplicatedTextMaterializationTask,
23    recipe: eredu_checkpoint::recipe::DerivedWeightRecipe,
24    source_output: eredu_checkpoint::recipe::RecipeMetadata,
25    selected_bytes: u64,
26    quantization_companions: Option<crate::QuantizationCompanionBindings>,
27}
28
29impl AddressableBankParameter {
30    /// Retains and validates one selected task and its member-local recipe.
31    pub fn new(
32        binding_name: impl Into<String>,
33        task: ReplicatedTextMaterializationTask,
34        recipe: eredu_checkpoint::recipe::DerivedWeightRecipe,
35        source_output: eredu_checkpoint::recipe::RecipeMetadata,
36        selected_bytes: u64,
37        quantization_companions: Option<crate::QuantizationCompanionBindings>,
38    ) -> Result<Self, AddressableBankMemberError> {
39        let binding_name = binding_name.into();
40        if binding_name.trim().is_empty() {
41            return Err(AddressableBankMemberError::InvalidParameter {
42                parameter: task.name().to_owned(),
43                detail: "addressable binding name is empty".into(),
44            });
45        }
46        task.source_recipe()
47            .map_err(|error| AddressableBankMemberError::InvalidParameter {
48                parameter: task.name().to_owned(),
49                detail: error.to_string(),
50            })?;
51        let descriptor = task.lowering_descriptor();
52        if descriptor.source() != task.source_encoding()
53            || descriptor.executable() != task.executable()
54            || descriptor.physical_shape() != task.physical_shape()
55            || descriptor.logical_shape() != task.logical_shape()
56        {
57            return Err(AddressableBankMemberError::InvalidParameter {
58                parameter: task.name().to_owned(),
59                detail: "selected source, executable, or lowering descriptor drifted".into(),
60            });
61        }
62        let declared_sources = task
63            .sources()
64            .iter()
65            .map(String::as_str)
66            .collect::<std::collections::BTreeSet<_>>();
67        let recipe_sources = recipe
68            .source_keys()
69            .into_iter()
70            .collect::<std::collections::BTreeSet<_>>();
71        if recipe_sources.is_empty() || !recipe_sources.is_subset(&declared_sources) {
72            return Err(AddressableBankMemberError::InvalidParameter {
73                parameter: task.name().to_owned(),
74                detail: "member recipe consumes sources outside the selected task".into(),
75            });
76        }
77        if source_output.byte_len() == 0 {
78            return Err(AddressableBankMemberError::ZeroSourceBytes {
79                parameter: task.name().to_owned(),
80            });
81        }
82        if selected_bytes == 0 {
83            return Err(AddressableBankMemberError::ZeroSelectedBytes {
84                parameter: task.name().to_owned(),
85            });
86        }
87        let transforms = matches!(
88            task.lowering(),
89            WeightLoweringKind::Transform | WeightLoweringKind::DerivedTransform
90        );
91        if !transforms && quantization_companions.is_some() {
92            return Err(AddressableBankMemberError::InvalidParameter {
93                parameter: task.name().to_owned(),
94                detail: "non-transform lowering declared local transform companions".into(),
95            });
96        }
97        if transforms
98            && quantization_companions.is_none()
99            && source_output.dtype() != &eredu_checkpoint::recipe::RecipeDtype::F4
100        {
101            return Err(AddressableBankMemberError::InvalidParameter {
102                parameter: task.name().to_owned(),
103                detail: "floating transform omitted its local output companions".into(),
104            });
105        }
106        if transforms
107            && source_output.dtype() != &eredu_checkpoint::recipe::RecipeDtype::F4
108            && task.output_companions().is_empty()
109        {
110            return Err(AddressableBankMemberError::InvalidParameter {
111                parameter: task.name().to_owned(),
112                detail: "floating transform omitted its exact selected output companions".into(),
113            });
114        }
115        if let Some(companions) = quantization_companions.as_ref() {
116            let declared_roles = task
117                .output_companions()
118                .iter()
119                .map(|companion| companion.role())
120                .collect::<std::collections::BTreeSet<_>>();
121            let mut bound_roles =
122                std::collections::BTreeSet::from([eredu_nn::LinearCompanionRole::Scale]);
123            if companions.affine_bias().is_some() {
124                bound_roles.insert(eredu_nn::LinearCompanionRole::AffineBias);
125            }
126            if declared_roles != bound_roles {
127                return Err(AddressableBankMemberError::InvalidParameter {
128                    parameter: task.name().to_owned(),
129                    detail: "selected quantization companion roles differ from exact outputs"
130                        .into(),
131                });
132            }
133            for companion in task.output_companions() {
134                let local = match companion.role() {
135                    eredu_nn::LinearCompanionRole::Scale => companions.scale(),
136                    eredu_nn::LinearCompanionRole::AffineBias => companions
137                        .affine_bias()
138                        .expect("validated affine-bias role has one binding"),
139                };
140                if companion.name() != local && !companion.name().ends_with(&format!(".{local}")) {
141                    return Err(AddressableBankMemberError::InvalidParameter {
142                        parameter: task.name().to_owned(),
143                        detail: format!(
144                            "local companion {local:?} differs from selected output {:?}",
145                            companion.name()
146                        ),
147                    });
148                }
149                let owner_matches = match (task.owner(), companion.owner()) {
150                    (
151                        ReplicatedTextParameterOwner::ExecutionUnit { group, unit },
152                        crate::ParameterGroupOwner::ExecutionUnit {
153                            group: companion_group,
154                            global_unit,
155                        },
156                    ) => group == companion_group.as_str() && unit == global_unit,
157                    (
158                        ReplicatedTextParameterOwner::StaticRole(role),
159                        crate::ParameterGroupOwner::StaticRole(companion_role),
160                    ) => role == companion_role,
161                    _ => false,
162                };
163                if !owner_matches {
164                    return Err(AddressableBankMemberError::InvalidParameter {
165                        parameter: task.name().to_owned(),
166                        detail: format!(
167                            "selected companion {:?} has a different owner",
168                            companion.name()
169                        ),
170                    });
171                }
172            }
173        }
174        let expected = selected_addressable_parameter_bytes(&task, &source_output)?;
175        if selected_bytes != expected {
176            return Err(AddressableBankMemberError::SelectedByteMismatch {
177                parameter: task.name().to_owned(),
178                expected,
179                actual: selected_bytes,
180            });
181        }
182        Ok(Self {
183            binding_name,
184            task,
185            recipe,
186            source_output,
187            selected_bytes,
188            quantization_companions,
189        })
190    }
191
192    /// Returns the local grouped-operator binding name.
193    pub fn binding_name(&self) -> &str {
194        &self.binding_name
195    }
196
197    /// Returns the complete authoritative selected materialization task.
198    pub const fn task(&self) -> &ReplicatedTextMaterializationTask {
199        &self.task
200    }
201
202    /// Returns the exact member-local source recipe.
203    pub const fn recipe(&self) -> &eredu_checkpoint::recipe::DerivedWeightRecipe {
204        &self.recipe
205    }
206
207    /// Returns admitted metadata for the member-local source recipe.
208    pub const fn source_output(&self) -> &eredu_checkpoint::recipe::RecipeMetadata {
209        &self.source_output
210    }
211
212    /// Returns source bytes before an optional lowering.
213    pub const fn source_bytes(&self) -> u64 {
214        self.source_output.byte_len()
215    }
216
217    /// Returns executable bytes after the selected lowering.
218    pub const fn selected_bytes(&self) -> u64 {
219        self.selected_bytes
220    }
221
222    /// Returns exact local scale and affine-bias binding names for a transform.
223    pub const fn quantization_companions(&self) -> Option<&crate::QuantizationCompanionBindings> {
224        self.quantization_companions.as_ref()
225    }
226}
227
228/// Exact generic storage member projected from an architecture-owned bank catalog.
229#[derive(Debug, Clone, Eq, PartialEq)]
230pub struct AddressableBankMember {
231    key: ParameterBankKey,
232    placement: AddressableBankMemberPlacement,
233    parameters: Vec<AddressableBankParameter>,
234    source_bytes: u64,
235    selected_bytes: u64,
236}
237
238/// Neutral placement class for one independently addressable member.
239#[derive(Debug, Clone, Copy, Eq, PartialEq)]
240#[non_exhaustive]
241pub enum AddressableBankDistribution {
242    /// The member is present on every execution rank.
243    Replicated,
244    /// The member follows an architecture-selected expert partition.
245    ExpertParallel,
246}
247
248/// Architecture-selected ownership retained with an addressable member.
249#[derive(Debug, Clone, Eq, PartialEq)]
250pub struct AddressableBankMemberPlacement {
251    owner_group: crate::ExecutionGroupId,
252    owner_unit: usize,
253    unit_path: String,
254    distribution: AddressableBankDistribution,
255    owner_rank: Option<usize>,
256}
257
258impl AddressableBankMemberPlacement {
259    /// Creates exact architecture-global member placement.
260    pub fn new(
261        owner_group: crate::ExecutionGroupId,
262        owner_unit: usize,
263        unit_path: impl Into<String>,
264        distribution: AddressableBankDistribution,
265    ) -> Result<Self, AddressableBankMemberError> {
266        let unit_path = unit_path.into();
267        if unit_path.trim().is_empty() {
268            return Err(AddressableBankMemberError::InvalidPlacement(
269                "addressable member unit path is empty".into(),
270            ));
271        }
272        Ok(Self {
273            owner_group,
274            owner_unit,
275            unit_path,
276            distribution,
277            owner_rank: None,
278        })
279    }
280
281    /// Binds this selected member projection to one global partition rank.
282    pub fn with_owner_rank(mut self, owner_rank: usize) -> Self {
283        self.owner_rank = Some(owner_rank);
284        self
285    }
286
287    /// Returns the architecture execution group that owns this member.
288    pub const fn owner_group(&self) -> &crate::ExecutionGroupId {
289        &self.owner_group
290    }
291    /// Returns the architecture-global execution-unit index.
292    pub const fn owner_unit(&self) -> usize {
293        self.owner_unit
294    }
295    /// Returns the stable architecture path of the owning unit.
296    pub fn unit_path(&self) -> &str {
297        &self.unit_path
298    }
299    /// Returns the architecture-selected distribution class.
300    pub const fn distribution(&self) -> AddressableBankDistribution {
301        self.distribution
302    }
303    /// Returns the global partition rank after rank-local projection.
304    pub const fn owner_rank(&self) -> Option<usize> {
305        self.owner_rank
306    }
307}
308
309impl AddressableBankMember {
310    /// Validates one atomic member and all of its selected parameter tasks.
311    pub fn new(
312        key: ParameterBankKey,
313        placement: AddressableBankMemberPlacement,
314        parameters: impl IntoIterator<Item = AddressableBankParameter>,
315    ) -> Result<Self, AddressableBankMemberError> {
316        let parameters = parameters.into_iter().collect::<Vec<_>>();
317        if parameters.is_empty() {
318            return Err(AddressableBankMemberError::EmptyMember { key });
319        }
320        if placement.owner_unit() != key.unit() {
321            return Err(AddressableBankMemberError::InvalidPlacement(format!(
322                "addressable member unit {} differs from placement unit {}",
323                key.unit(),
324                placement.owner_unit()
325            )));
326        }
327        let mut bindings = std::collections::BTreeSet::new();
328        let mut targets = std::collections::BTreeSet::new();
329        let mut source_bytes = 0u64;
330        let mut selected_bytes = 0u64;
331        for parameter in &parameters {
332            if !bindings.insert(parameter.binding_name())
333                || !targets.insert(parameter.task().name())
334            {
335                return Err(AddressableBankMemberError::DuplicateParameter { key });
336            }
337            if !matches!(
338                parameter.task().owner(),
339                ReplicatedTextParameterOwner::ExecutionUnit { group, unit }
340                    if *unit == placement.owner_unit()
341                        && group == placement.owner_group().as_str()
342            ) {
343                return Err(AddressableBankMemberError::InvalidParameter {
344                    parameter: parameter.task().name().to_owned(),
345                    detail: "selected task has a non-bank owner".into(),
346                });
347            }
348            source_bytes = source_bytes
349                .checked_add(parameter.source_bytes())
350                .ok_or(AddressableBankMemberError::SourceByteOverflow { key })?;
351            selected_bytes = selected_bytes
352                .checked_add(parameter.selected_bytes())
353                .ok_or(AddressableBankMemberError::SelectedByteOverflow { key })?;
354        }
355        Ok(Self {
356            key,
357            placement,
358            parameters,
359            source_bytes,
360            selected_bytes,
361        })
362    }
363
364    /// Returns the generic bank key selected by neutral composition.
365    pub const fn key(&self) -> ParameterBankKey {
366        self.key
367    }
368
369    /// Exact group/unit/distribution/rank ownership selected for this member.
370    pub const fn placement(&self) -> &AddressableBankMemberPlacement {
371        &self.placement
372    }
373
374    /// Retains the global rank selected by a partition projection.
375    pub fn with_owner_rank(mut self, owner_rank: usize) -> Self {
376        self.placement = self.placement.with_owner_rank(owner_rank);
377        self
378    }
379
380    /// Returns every exact selected parameter in deterministic binding order.
381    pub fn parameters(&self) -> &[AddressableBankParameter] {
382        &self.parameters
383    }
384
385    /// Returns admitted source bytes before optional lowerings.
386    pub const fn source_bytes(&self) -> u64 {
387        self.source_bytes
388    }
389
390    /// Returns executable bytes after selected lowerings.
391    pub const fn selected_bytes(&self) -> u64 {
392        self.selected_bytes
393    }
394}
395
396/// Backend-neutral transform retained for one addressable binding.
397#[derive(Debug, Clone, Eq, PartialEq)]
398pub struct AddressableBindingTransform {
399    quantization: eredu_checkpoint::WeightQuantization,
400    companion_dtype: eredu_checkpoint::recipe::RecipeDtype,
401}
402
403impl AddressableBindingTransform {
404    /// Selected packed executable format.
405    pub const fn quantization(&self) -> eredu_checkpoint::WeightQuantization {
406        self.quantization
407    }
408    /// Selected scale and affine-bias scalar representation.
409    pub const fn companion_dtype(&self) -> &eredu_checkpoint::recipe::RecipeDtype {
410        &self.companion_dtype
411    }
412}
413
414/// Canonical binding and transformation plan for one addressable member.
415#[derive(Debug, Clone)]
416pub struct AddressableBankBindingPlan {
417    key: ParameterBankKey,
418    bindings: Vec<crate::WeightBinding>,
419    transformations: std::collections::BTreeMap<String, AddressableBindingTransform>,
420    selected_bytes: u64,
421    placement: AddressableBankMemberPlacement,
422}
423
424impl AddressableBankBindingPlan {
425    /// Generic member identity.
426    pub const fn key(&self) -> ParameterBankKey {
427        self.key
428    }
429    /// Source-side canonical bindings.
430    pub fn bindings(&self) -> &[crate::WeightBinding] {
431        &self.bindings
432    }
433    /// Per-binding transforms selected by architecture admission.
434    pub const fn transformations(
435        &self,
436    ) -> &std::collections::BTreeMap<String, AddressableBindingTransform> {
437        &self.transformations
438    }
439    /// Exact executable bytes after all transforms.
440    pub const fn selected_bytes(&self) -> u64 {
441        self.selected_bytes
442    }
443    /// Exact rank-local architecture placement.
444    pub const fn placement(&self) -> &AddressableBankMemberPlacement {
445        &self.placement
446    }
447    /// Consumes the complete canonical member plan.
448    #[allow(clippy::type_complexity)]
449    pub fn into_parts(
450        self,
451    ) -> (
452        ParameterBankKey,
453        Vec<crate::WeightBinding>,
454        std::collections::BTreeMap<String, AddressableBindingTransform>,
455        u64,
456        AddressableBankMemberPlacement,
457    ) {
458        (
459            self.key,
460            self.bindings,
461            self.transformations,
462            self.selected_bytes,
463            self.placement,
464        )
465    }
466}
467
468/// Validates exact addressable tasks and derives their singular canonical binding plans.
469pub fn plan_addressable_bank_bindings<L, E>(
470    members: &[AddressableBankMember],
471    source: &dyn eredu_checkpoint::store::CheckpointSource,
472    mut lower_mxfp4: L,
473) -> Result<Vec<AddressableBankBindingPlan>, AddressableBankMemberError>
474where
475    L: FnMut(
476        &ReplicatedTextMaterializationTask,
477        eredu_checkpoint::recipe::DerivedWeightRecipe,
478        &dyn eredu_checkpoint::store::CheckpointSource,
479    ) -> Result<eredu_checkpoint::recipe::DerivedWeightRecipe, E>,
480    E: std::fmt::Display,
481{
482    let mut plans = Vec::with_capacity(members.len());
483    for member in members {
484        let mut bindings = Vec::with_capacity(member.parameters().len());
485        let mut transformations = std::collections::BTreeMap::new();
486        for parameter in member.parameters() {
487            let task = parameter.task();
488            let declared = task
489                .sources()
490                .iter()
491                .map(String::as_str)
492                .collect::<std::collections::BTreeSet<_>>();
493            let physical = task
494                .physical_sources()
495                .iter()
496                .map(|item| item.catalog_key())
497                .collect::<std::collections::BTreeSet<_>>();
498            if declared != physical || physical.len() != task.physical_sources().len() {
499                return Err(AddressableBankMemberError::InvalidParameter {
500                    parameter: task.name().to_owned(),
501                    detail: "selected physical provenance does not exactly cover task sources"
502                        .into(),
503                });
504            }
505            for admitted in task.physical_sources() {
506                let actual = source
507                    .source_provenance(admitted.catalog_key())
508                    .map_err(|error| AddressableBankMemberError::InvalidParameter {
509                        parameter: task.name().to_owned(),
510                        detail: error.to_string(),
511                    })?;
512                let metadata = source
513                    .source_metadata(admitted.catalog_key())
514                    .map_err(|error| AddressableBankMemberError::InvalidParameter {
515                        parameter: task.name().to_owned(),
516                        detail: error.to_string(),
517                    })?;
518                if actual.catalog_key != admitted.catalog_key()
519                    || actual.physical_tensor != admitted.tensor()
520                    || actual.output != admitted.output()
521                    || actual.backing_shard.as_deref() != Some(admitted.shard())
522                    || actual.source_encoding != *admitted.source_encoding()
523                    || metadata.encoded_byte_len != admitted.encoded_byte_len()
524                {
525                    return Err(AddressableBankMemberError::InvalidParameter {
526                        parameter: task.name().to_owned(),
527                        detail: format!(
528                            "source {:?} differs from admitted provenance",
529                            admitted.catalog_key()
530                        ),
531                    });
532                }
533            }
534            let mut recipe = parameter.recipe().clone();
535            let inferred = recipe.infer(source).map_err(|error| {
536                AddressableBankMemberError::InvalidParameter {
537                    parameter: task.name().to_owned(),
538                    detail: error.to_string(),
539                }
540            })?;
541            if &inferred != parameter.source_output() {
542                return Err(AddressableBankMemberError::InvalidParameter {
543                    parameter: task.name().to_owned(),
544                    detail: "member-local recipe output drifted".into(),
545                });
546            }
547            if task.executable() == eredu_checkpoint::LinearFormat::MxFp4
548                && inferred.dtype() == &eredu_checkpoint::recipe::RecipeDtype::F4
549                && parameter.quantization_companions().is_none()
550            {
551                recipe = lower_mxfp4(task, recipe, source).map_err(|error| {
552                    AddressableBankMemberError::InvalidParameter {
553                        parameter: task.name().to_owned(),
554                        detail: error.to_string(),
555                    }
556                })?;
557            }
558            let metadata = recipe.infer(source).map_err(|error| {
559                AddressableBankMemberError::InvalidParameter {
560                    parameter: task.name().to_owned(),
561                    detail: error.to_string(),
562                }
563            })?;
564            let mut binding = crate::WeightBinding::from_recipe(
565                parameter.binding_name(),
566                recipe,
567                metadata.byte_len(),
568            )
569            .and_then(|binding| binding.with_logical_target(task.name()))
570            .map_err(|error| AddressableBankMemberError::InvalidParameter {
571                parameter: task.name().to_owned(),
572                detail: error.to_string(),
573            })?;
574            if let Some(companions) = parameter.quantization_companions() {
575                let quantization = task.executable().weight_quantization().ok_or_else(|| {
576                    AddressableBankMemberError::InvalidParameter {
577                        parameter: task.name().to_owned(),
578                        detail: "transformed task has no packed format".into(),
579                    }
580                })?;
581                transformations.insert(
582                    parameter.binding_name().to_owned(),
583                    AddressableBindingTransform {
584                        quantization,
585                        companion_dtype: parameter.source_output().dtype().clone(),
586                    },
587                );
588                binding = binding
589                    .with_quantization_companions(
590                        companions.scale(),
591                        companions.affine_bias().map(str::to_owned),
592                    )
593                    .map_err(|error| AddressableBankMemberError::InvalidParameter {
594                        parameter: task.name().to_owned(),
595                        detail: error.to_string(),
596                    })?;
597            }
598            bindings.push(binding);
599        }
600        crate::WeightBindingPlan::new(&bindings).map_err(|error| {
601            AddressableBankMemberError::InvalidParameter {
602                parameter: format!("{:?}", member.key()),
603                detail: error.to_string(),
604            }
605        })?;
606        plans.push(AddressableBankBindingPlan {
607            key: member.key(),
608            bindings,
609            transformations,
610            selected_bytes: member.selected_bytes(),
611            placement: member.placement().clone(),
612        });
613    }
614    Ok(plans)
615}
616
617/// Computes executable storage bytes for one admitted member-local task output.
618///
619/// A task's own derived output describes whole-parameter derivation before a
620/// member projection. `metadata` instead describes the exact member-local
621/// recipe retained by [`AddressableBankParameter`], including rank-local
622/// sharding. This function is the single neutral authority for their selected
623/// executable byte geometry.
624pub fn selected_addressable_parameter_bytes(
625    task: &ReplicatedTextMaterializationTask,
626    metadata: &eredu_checkpoint::recipe::RecipeMetadata,
627) -> Result<u64, AddressableBankMemberError> {
628    if !matches!(
629        task.lowering(),
630        WeightLoweringKind::Transform | WeightLoweringKind::DerivedTransform
631    ) {
632        return Ok(metadata.byte_len());
633    }
634    let quantization = task.executable().weight_quantization().ok_or_else(|| {
635        AddressableBankMemberError::InvalidParameter {
636            parameter: task.name().to_owned(),
637            detail: "transform lowering has no packed executable format".into(),
638        }
639    })?;
640    if matches!(
641        quantization,
642        eredu_checkpoint::WeightQuantization::GgufIQuant { .. }
643    ) {
644        return Err(AddressableBankMemberError::InvalidParameter {
645            parameter: task.name().to_owned(),
646            detail: "load-time transform selected checkpoint-native GGUF encoding".into(),
647        });
648    }
649    if task.lowering_descriptor().packed_axis() != metadata.shape().len().checked_sub(1) {
650        return Err(AddressableBankMemberError::InvalidParameter {
651            parameter: task.name().to_owned(),
652            detail: "transform packed axis is not the final logical matrix axis".into(),
653        });
654    }
655    let shape = metadata.shape();
656    let (&columns, row_shape) =
657        shape
658            .split_last()
659            .ok_or_else(|| AddressableBankMemberError::InvalidParameter {
660                parameter: task.name().to_owned(),
661                detail: "transform target is not a matrix".into(),
662            })?;
663    let rows = row_shape
664        .iter()
665        .try_fold(1u64, |total, dimension| {
666            total.checked_mul(*dimension as u64)
667        })
668        .ok_or_else(|| AddressableBankMemberError::InvalidParameter {
669            parameter: task.name().to_owned(),
670            detail: "transform row geometry overflowed".into(),
671        })?;
672    let group = usize::try_from(quantization.group_size()).map_err(|_| {
673        AddressableBankMemberError::InvalidParameter {
674            parameter: task.name().to_owned(),
675            detail: "transform group size is invalid".into(),
676        }
677    })?;
678    if group == 0 || !columns.is_multiple_of(group) || !columns.is_multiple_of(32) {
679        return Err(AddressableBankMemberError::InvalidParameter {
680            parameter: task.name().to_owned(),
681            detail: "transform geometry is incompatible with its packed format".into(),
682        });
683    }
684    let groups = (columns / group) as u64;
685    let packed = (columns as u64)
686        .checked_mul(quantization.bits() as u64)
687        .and_then(|bits| bits.checked_div(8))
688        .ok_or_else(|| AddressableBankMemberError::InvalidParameter {
689            parameter: task.name().to_owned(),
690            detail: "packed row byte geometry overflowed".into(),
691        })?;
692    let scalar_bytes = metadata.dtype().bit_width().map_err(|error| {
693        AddressableBankMemberError::InvalidParameter {
694            parameter: task.name().to_owned(),
695            detail: error.to_string(),
696        }
697    })? / 8;
698    let companion = if matches!(quantization, eredu_checkpoint::WeightQuantization::MxFp4) {
699        groups
700    } else {
701        groups.checked_mul(scalar_bytes).ok_or_else(|| {
702            AddressableBankMemberError::InvalidParameter {
703                parameter: task.name().to_owned(),
704                detail: "scale byte geometry overflowed".into(),
705            }
706        })?
707    };
708    let bias = if quantization.has_biases() {
709        companion
710    } else {
711        0
712    };
713    rows.checked_mul(
714        packed
715            .checked_add(companion)
716            .and_then(|bytes| bytes.checked_add(bias))
717            .ok_or_else(|| AddressableBankMemberError::InvalidParameter {
718                parameter: task.name().to_owned(),
719                detail: "selected row byte geometry overflowed".into(),
720            })?,
721    )
722    .ok_or_else(|| AddressableBankMemberError::InvalidParameter {
723        parameter: task.name().to_owned(),
724        detail: "selected byte geometry overflowed".into(),
725    })
726}
727
728/// Invalid generic addressable-bank member projection.
729#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
730pub enum AddressableBankMemberError {
731    /// Member placement was empty or disagreed with task ownership.
732    #[error("invalid addressable bank member placement: {0}")]
733    InvalidPlacement(String),
734    /// A member contained no parameter task.
735    #[error("addressable bank member {key:?} is empty")]
736    EmptyMember {
737        /// Invalid member identity.
738        key: ParameterBankKey,
739    },
740    /// A member repeated a local binding or logical task.
741    #[error("addressable bank member {key:?} repeats a parameter")]
742    DuplicateParameter {
743        /// Invalid member identity.
744        key: ParameterBankKey,
745    },
746    /// One parameter's exact task closure was inconsistent.
747    #[error("invalid addressable bank parameter {parameter:?}: {detail}")]
748    InvalidParameter {
749        /// Invalid logical parameter.
750        parameter: String,
751        /// Exact validation failure.
752        detail: String,
753    },
754    /// A source recipe selected no bytes.
755    #[error("addressable bank parameter {parameter:?} source byte geometry is zero")]
756    ZeroSourceBytes {
757        /// Invalid logical parameter.
758        parameter: String,
759    },
760    /// Source binding byte accounting overflowed.
761    #[error("addressable bank member {key:?} source byte geometry overflowed")]
762    SourceByteOverflow {
763        /// Invalid member.
764        key: ParameterBankKey,
765    },
766    /// Selected executable byte accounting overflowed.
767    #[error("addressable bank member {key:?} selected byte geometry overflowed")]
768    SelectedByteOverflow {
769        /// Invalid member identity.
770        key: ParameterBankKey,
771    },
772    /// Selected executable storage was empty.
773    #[error("addressable bank parameter {parameter:?} selected byte geometry is zero")]
774    ZeroSelectedBytes {
775        /// Invalid parameter.
776        parameter: String,
777    },
778    /// Selected executable byte geometry differed from the task.
779    #[error("addressable bank parameter {parameter:?} selected bytes differ: expected {expected}, got {actual}")]
780    SelectedByteMismatch {
781        /// Invalid logical parameter.
782        parameter: String,
783        /// Authoritative computed byte total.
784        expected: u64,
785        /// Supplied selected byte total.
786        actual: u64,
787    },
788}
789
790/// Generic indexed tensor movement required by bounded grouped execution.
791///
792/// Implementations expose integer-index discovery and tensor movement without
793/// receiving architecture plans, bank meaning, or text lifecycle policy.
794pub trait IndexedMovement<B>
795where
796    B: GroupedNeuralBackend,
797{
798    /// Indexed movement failure.
799    type Error;
800
801    /// Returns deterministic demand counts for integer indices below `upper_bound`.
802    fn index_demands(
803        &mut self,
804        indices: &B::Tensor,
805        upper_bound: usize,
806        context: &<B::Tensor as Tensor>::Context,
807    ) -> Result<Vec<(usize, u64)>, Self::Error>;
808
809    /// Rewrites source indices through one exact source-to-compact mapping.
810    fn remap_indices(
811        &mut self,
812        indices: &B::Tensor,
813        mapping: &[(usize, usize)],
814        context: &<B::Tensor as Tensor>::Context,
815    ) -> Result<B::Tensor, Self::Error>;
816
817    /// Selects a contiguous range along the leading row axis.
818    fn select_rows(
819        &mut self,
820        value: &B::Tensor,
821        start: usize,
822        end: usize,
823        context: &<B::Tensor as Tensor>::Context,
824    ) -> Result<B::Tensor, Self::Error>;
825
826    /// Concatenates row partitions in their original order.
827    fn concatenate_rows(
828        &mut self,
829        values: &[B::Tensor],
830        context: &<B::Tensor as Tensor>::Context,
831    ) -> Result<B::Tensor, Self::Error>;
832}
833
834/// Backend-neutral tensor movement needed by an expert-exchange protocol.
835///
836/// Architecture code supplies already validated row and flattened-route
837/// indices. Implementations retain tensor storage and completion ownership;
838/// they do not receive expert identities, topology, or model-family policy.
839pub trait ExpertRouteTensorMovement<T> {
840    /// Tensor movement failure.
841    type Error;
842
843    /// Returns the logical tensor shape without materializing its values.
844    fn shape(&self, value: &T) -> Vec<usize>;
845
846    /// Duplicates and reorders leading-axis rows in the supplied order.
847    fn gather_rows(&mut self, value: &T, rows: &[usize]) -> Result<T, Self::Error>;
848
849    /// Selects flattened route scalars and returns them as `[routes, 1]`.
850    fn gather_route_values(
851        &mut self,
852        value: &T,
853        flattened_routes: &[usize],
854    ) -> Result<T, Self::Error>;
855
856    /// Additively combines route rows into their architecture source rows.
857    ///
858    /// Every input row must be consumed exactly once. Repeated destination
859    /// rows are intentional and implement weighted routed-expert summation.
860    fn scatter_add_rows(
861        &mut self,
862        value: T,
863        destination_rows: &[usize],
864        output_rows: usize,
865    ) -> Result<T, Self::Error>;
866}
867
868/// Opaque variable-count transport used by architecture-owned expert routing.
869///
870/// Implementations must preserve peer-block and within-block order, validate
871/// every tensor against the selected communication requirement, and retain all
872/// native resources until the exact completion has finished.
873pub trait ExpertRouteExchange<T> {
874    /// Communication or metadata transport failure.
875    type Error;
876
877    /// Exchanges one tensor whose leading rows match the supplied peer counts.
878    fn exchange_tensor(
879        &mut self,
880        counts: &crate::CommunicationPeerCounts,
881        value: T,
882    ) -> Result<T, Self::Error>;
883
884    /// Exchanges one unsigned metadata value per leading tensor row.
885    fn exchange_indices(
886        &mut self,
887        counts: &crate::CommunicationPeerCounts,
888        values: Vec<usize>,
889    ) -> Result<Vec<usize>, Self::Error>;
890}
891
892/// Architecture-selected combination for one expert-exchange batch.
893#[derive(Debug, Clone, Copy, Eq, PartialEq)]
894#[non_exhaustive]
895pub enum ExpertRouteCombination {
896    /// Apply each route coefficient once, then add routes targeting one token.
897    CoefficientWeightedSum,
898}
899
900/// One owner-local grouped batch submitted after expert exchange.
901pub struct AddressableExpertRouteRequest<'a, T> {
902    /// Global execution unit containing the addressable expert bank.
903    pub unit: usize,
904    /// Rows received from every source peer.
905    pub input: &'a T,
906    /// Checkpoint-global expert identity for every received row.
907    ///
908    /// Addressable storage keys must be derived from this identity. It is
909    /// deliberately kept separate from `owner_local_experts`, whose values
910    /// are valid only as indices into the rank-local grouped operator.
911    pub global_experts: &'a [usize],
912    /// Dense owner-local expert identity for every received row.
913    pub owner_local_experts: &'a [usize],
914    /// Selected router scores aligned one-for-one with received rows.
915    pub selected_scores: &'a T,
916    /// Final route coefficients aligned one-for-one with received rows.
917    pub coefficients: &'a T,
918    /// Prefill or decode execution classification.
919    pub pass: ExpertPass,
920    /// Storage access classification derived from `pass`.
921    pub access: ParameterBankAccess,
922    /// Architecture-declared route combination.
923    pub combination: ExpertRouteCombination,
924}
925
926impl<T> AddressableExpertRouteRequest<'_, T> {
927    /// Returns the only valid addressable-bank key for one routed row.
928    ///
929    /// The owner-local ID is intentionally not accepted here: it addresses the
930    /// compact grouped operator, not checkpoint-global storage.
931    pub fn addressable_bank_key(&self, row: usize) -> Option<ParameterBankKey> {
932        self.global_experts
933            .get(row)
934            .copied()
935            .map(|global| ParameterBankKey::new(self.unit, global))
936    }
937
938    /// Returns the rank-local grouped-operator ID for one routed row.
939    pub fn owner_local_execution_id(&self, row: usize) -> Option<usize> {
940        self.owner_local_experts.get(row).copied()
941    }
942}
943
944/// Local addressable grouped execution used by expert exchange.
945///
946/// The provider must consume every submitted row exactly once, select its
947/// corresponding owner-local expert, and apply its route coefficient exactly
948/// once. Acquired bank resources remain provider-owned until the returned
949/// tensor is natively complete.
950pub trait AddressableExpertRouteProvider<T> {
951    /// Acquisition or grouped execution failure.
952    type Error;
953
954    /// Executes one owner-local grouped batch.
955    fn execute_addressable_routes(
956        &mut self,
957        request: AddressableExpertRouteRequest<'_, T>,
958    ) -> Result<T, Self::Error>;
959
960    /// Executes one owner-local grouped batch while retaining tensor-parallel
961    /// reduction structure.
962    ///
963    /// Providers without rank-local TP work inherit complete-output behavior.
964    /// A TP provider overrides this method and returns its reducible activation
965    /// contribution plus the optional selection-weighted post-reduction bias.
966    /// The exchange protocol returns both values to their source-token order;
967    /// it must not add the bias before the caller's tensor all-sum.
968    fn execute_addressable_routes_tensor_parallel(
969        &mut self,
970        request: AddressableExpertRouteRequest<'_, T>,
971    ) -> Result<RoutedExpertTensorParallelOutput<T>, Self::Error> {
972        self.execute_addressable_routes(request)
973            .map(RoutedExpertTensorParallelOutput::Complete)
974    }
975}
976
977/// Exact generic request for an independently addressable bank acquisition.
978#[derive(Debug, Clone, Copy)]
979pub struct ParameterBankAcquisition<'a> {
980    entries: &'a [(ParameterBankKey, u64)],
981    access: ParameterBankAccess,
982}
983
984impl<'a> ParameterBankAcquisition<'a> {
985    /// Creates one deterministic acquisition request in compact-bank order.
986    pub const fn new(entries: &'a [(ParameterBankKey, u64)], access: ParameterBankAccess) -> Self {
987        Self { entries, access }
988    }
989
990    /// Returns generic bank keys and duplicate-preserving demand counts.
991    pub const fn entries(&self) -> &'a [(ParameterBankKey, u64)] {
992        self.entries
993    }
994
995    /// Returns the selected generic storage access class.
996    pub const fn access(&self) -> ParameterBankAccess {
997        self.access
998    }
999}
1000
1001/// Generic addressable storage and grouped-operator construction mechanisms.
1002///
1003/// The mechanism receives already translated bank keys, compact specifications,
1004/// and access classes. Architecture identity, routing policy, global identity
1005/// mapping, chunking, and text-session behavior remain outside this contract.
1006pub trait AddressableGroupedBank<B>
1007where
1008    B: GroupedNeuralBackend,
1009{
1010    /// Live native storage retained across grouped execution.
1011    type Acquisition;
1012    /// Generic bank telemetry snapshot.
1013    type Report;
1014    /// Storage, transfer, lowering, or construction failure.
1015    type Error;
1016
1017    /// Returns the selected byte geometry for one admitted bank member.
1018    fn member_bytes(&self, key: ParameterBankKey) -> Option<u64>;
1019
1020    /// Acquires exact generic keys in caller-supplied compact order.
1021    fn acquire(
1022        &mut self,
1023        request: ParameterBankAcquisition<'_>,
1024        context: &<B::Tensor as Tensor>::Context,
1025    ) -> Result<Self::Acquisition, Self::Error>;
1026
1027    /// Constructs one compact gated-product operator from acquired bindings.
1028    fn gated_product_groups(
1029        &mut self,
1030        acquisition: &Self::Acquisition,
1031        spec: &eredu_nn::GroupedGatedProductSpec,
1032        context: &<B::Tensor as Tensor>::Context,
1033    ) -> Result<B::GatedProductGroups, Self::Error>;
1034
1035    /// Constructs one compact ReLU-squared operator from acquired bindings.
1036    fn relu2_groups(
1037        &mut self,
1038        acquisition: &Self::Acquisition,
1039        spec: &eredu_nn::GroupedRelu2Spec,
1040        context: &<B::Tensor as Tensor>::Context,
1041    ) -> Result<B::Relu2Groups, Self::Error>;
1042
1043    /// Retains acquired storage until the grouped output is natively complete.
1044    fn complete(
1045        &mut self,
1046        acquisition: Self::Acquisition,
1047        output: &B::Tensor,
1048        context: &<B::Tensor as Tensor>::Context,
1049    ) -> Result<(), Self::Error>;
1050
1051    /// Returns generic key, byte, tier, acquisition, and eviction telemetry.
1052    fn report(&self) -> Result<Self::Report, Self::Error>;
1053}
1054
1055/// Mechanism-only lookup of one grouped operator in an addressable parameter bank.
1056pub trait AddressableGatedProductBank<B>
1057where
1058    B: GroupedNeuralBackend,
1059{
1060    /// Bank lookup or construction failure.
1061    type Error;
1062
1063    /// Resolves one generic bank key and exact grouped construction specification.
1064    fn acquire(
1065        &mut self,
1066        key: ParameterBankKey,
1067        spec: &eredu_nn::GroupedGatedProductSpec,
1068        context: &<B::Tensor as Tensor>::Context,
1069    ) -> Result<&mut B::GatedProductGroups, Self::Error>;
1070}
1071
1072/// One architecture route batch submitted to a runtime expert provider.
1073pub struct RoutedExpertRequest<'a, T> {
1074    /// Global decoder layer requesting experts.
1075    pub layer: usize,
1076    /// Flattened token rows submitted to the selected experts.
1077    pub input: &'a T,
1078    /// Backend-native selected expert IDs, scores, and weights.
1079    pub routes: &'a GroupSelection<T>,
1080    /// Whether this route batch belongs to prefill or decode.
1081    pub pass: ExpertPass,
1082}
1083
1084impl<T> RoutedExpertRequest<'_, T> {
1085    /// Projects architecture execution semantics into the storage workload
1086    /// class exposed to backend parameter-bank mechanisms.
1087    pub const fn parameter_bank_access(&self) -> ParameterBankAccess {
1088        self.pass.parameter_bank_access()
1089    }
1090}
1091
1092/// Provider result that distinguishes complete outputs from rank-local TP work.
1093pub enum RoutedExpertTensorParallelOutput<T> {
1094    /// Provider already completed every required collective and bias addition.
1095    Complete(T),
1096    /// Caller must all-sum `reducible`, then add `post_reduce` exactly once.
1097    Partial(TensorParallelGroupedOutput<T>),
1098}
1099
1100/// Completes one rank-local expert output with one all-sum and one post-bias add.
1101pub fn reduce_tensor_parallel_expert_output<B>(
1102    output: TensorParallelGroupedOutput<B::Tensor>,
1103    parallel: &B::ParallelContext,
1104    context: &<B::Tensor as Tensor>::Context,
1105) -> Result<B::Tensor, eredu_nn::Error>
1106where
1107    B: GroupedNeuralBackend + DistributedNeuralBackend,
1108{
1109    let reduced = B::sum_parallel(output.reducible().clone(), parallel, context)?;
1110    match output.post_reduce().cloned() {
1111        Some(bias) => reduced.add(&bias, context),
1112        None => Ok(reduced),
1113    }
1114}
1115
1116/// Combines two rank-local expert partials without introducing another collective.
1117pub fn combine_tensor_parallel_expert_outputs<B>(
1118    left: TensorParallelGroupedOutput<B::Tensor>,
1119    right: TensorParallelGroupedOutput<B::Tensor>,
1120    context: &<B::Tensor as Tensor>::Context,
1121) -> Result<TensorParallelGroupedOutput<B::Tensor>, eredu_nn::Error>
1122where
1123    B: GroupedNeuralBackend,
1124{
1125    let post_reduce = match (left.post_reduce().cloned(), right.post_reduce().cloned()) {
1126        (Some(left), Some(right)) => Some(left.add(&right, context)?),
1127        (Some(bias), None) | (None, Some(bias)) => Some(bias),
1128        (None, None) => None,
1129    };
1130    Ok(TensorParallelGroupedOutput::new(
1131        left.reducible().add(right.reducible(), context)?,
1132        post_reduce,
1133    ))
1134}
1135
1136/// Combines routed/shared provider outputs while requiring one coherent TP mode.
1137pub fn combine_routed_expert_tensor_parallel<B>(
1138    left: RoutedExpertTensorParallelOutput<B::Tensor>,
1139    right: RoutedExpertTensorParallelOutput<B::Tensor>,
1140    context: &<B::Tensor as Tensor>::Context,
1141) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, eredu_nn::Error>
1142where
1143    B: GroupedNeuralBackend,
1144{
1145    match (left, right) {
1146        (
1147            RoutedExpertTensorParallelOutput::Complete(left),
1148            RoutedExpertTensorParallelOutput::Complete(right),
1149        ) => Ok(RoutedExpertTensorParallelOutput::Complete(
1150            left.add(&right, context)?,
1151        )),
1152        (
1153            RoutedExpertTensorParallelOutput::Partial(left),
1154            RoutedExpertTensorParallelOutput::Partial(right),
1155        ) => combine_tensor_parallel_expert_outputs::<B>(left, right, context)
1156            .map(RoutedExpertTensorParallelOutput::Partial),
1157        _ => Err(eredu_nn::Error::backend(
1158            "provider mixed complete and rank-local expert outputs in one block",
1159        )),
1160    }
1161}
1162
1163/// Completes a provider TP result while preserving provider-owned collectives.
1164pub fn reduce_routed_expert_tensor_parallel<B>(
1165    output: RoutedExpertTensorParallelOutput<B::Tensor>,
1166    parallel: &B::ParallelContext,
1167    context: &<B::Tensor as Tensor>::Context,
1168) -> Result<B::Tensor, eredu_nn::Error>
1169where
1170    B: GroupedNeuralBackend + DistributedNeuralBackend,
1171{
1172    match output {
1173        RoutedExpertTensorParallelOutput::Complete(output) => Ok(output),
1174        RoutedExpertTensorParallelOutput::Partial(output) => {
1175            reduce_tensor_parallel_expert_output::<B>(output, parallel, context)
1176        }
1177    }
1178}
1179
1180/// Runtime boundary for resident or independently cached routed experts.
1181///
1182/// Implementations own identity ordering, acquisition, leases, chunking,
1183/// budgets, and residency reports. They keep every lease alive until the
1184/// backend-native routed result is safe to return. The backend retains tensor
1185/// storage, transfers, compact-bank construction, and execution kernels.
1186pub trait RoutedExpertProvider<B>
1187where
1188    B: GroupedNeuralBackend,
1189{
1190    /// Provider-specific acquisition or execution failure.
1191    type Error;
1192
1193    /// Optional pre-dispatch control supplied by an instrumented provider adapter.
1194    fn routing_control(
1195        &mut self,
1196        _token_rows: u64,
1197    ) -> Result<Option<eredu_nn::routing_intervention::GroupSelectionControl>, Self::Error> {
1198        Ok(None)
1199    }
1200
1201    /// Consumes bounded decision evidence before dispatch. Shared experts are not included.
1202    fn routing_applied(
1203        &mut self,
1204        _original: Option<crate::RoutingDecision<'_, B::Tensor>>,
1205        _effective: crate::RoutingDecision<'_, B::Tensor>,
1206    ) -> Result<(), Self::Error> {
1207        Ok(())
1208    }
1209
1210    /// Attributes selector failure without claiming cache rollback.
1211    fn routing_failed(&mut self, _message: &str) {}
1212
1213    /// Executes one typed route batch while retaining its acquired resources.
1214    fn forward_grouped(
1215        &mut self,
1216        resident_bank: &mut B::GatedProductGroups,
1217        request: RoutedExpertRequest<'_, B::Tensor>,
1218        context: &<B::Tensor as Tensor>::Context,
1219    ) -> Result<B::Tensor, Self::Error>;
1220
1221    /// Executes destination-local rows that were already expanded to one
1222    /// owner-local expert per row by the neutral expert exchange.
1223    ///
1224    /// The compact request deliberately has route cardinality one; providers
1225    /// must not compare it with the architecture's original top-k cardinality.
1226    fn forward_compact_grouped(
1227        &mut self,
1228        resident_bank: &mut B::GatedProductGroups,
1229        request: RoutedExpertRequest<'_, B::Tensor>,
1230        context: &<B::Tensor as Tensor>::Context,
1231    ) -> Result<B::Tensor, Self::Error> {
1232        self.forward_grouped(resident_bank, request, context)
1233    }
1234
1235    /// Executes one ReLU-squared route batch through the same residency boundary.
1236    fn forward_relu2_routed(
1237        &mut self,
1238        resident_bank: &mut B::Relu2Groups,
1239        request: RoutedExpertRequest<'_, B::Tensor>,
1240        context: &<B::Tensor as Tensor>::Context,
1241    ) -> Result<B::Tensor, Self::Error>;
1242}
1243
1244/// Additive provider mechanism for tensor-parallel grouped partials.
1245pub trait TensorParallelRoutedExpertProvider<B>: RoutedExpertProvider<B>
1246where
1247    B: GroupedNeuralBackend,
1248{
1249    /// Executes a rank-local gated-product contribution.
1250    fn forward_grouped_tensor_parallel(
1251        &mut self,
1252        resident_bank: &mut B::GatedProductGroups,
1253        request: RoutedExpertRequest<'_, B::Tensor>,
1254        partitions: usize,
1255        context: &<B::Tensor as Tensor>::Context,
1256    ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error>;
1257
1258    /// Executes destination-local, one-expert-per-row contributions while
1259    /// preserving the backend's TP reduction and post-bias structure.
1260    fn forward_compact_grouped_tensor_parallel(
1261        &mut self,
1262        resident_bank: &mut B::GatedProductGroups,
1263        request: RoutedExpertRequest<'_, B::Tensor>,
1264        partitions: usize,
1265        context: &<B::Tensor as Tensor>::Context,
1266    ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error> {
1267        self.forward_grouped_tensor_parallel(resident_bank, request, partitions, context)
1268    }
1269
1270    /// Executes a rank-local ReLU-squared contribution.
1271    fn forward_relu2_routed_tensor_parallel(
1272        &mut self,
1273        resident_bank: &mut B::Relu2Groups,
1274        request: RoutedExpertRequest<'_, B::Tensor>,
1275        partitions: usize,
1276        context: &<B::Tensor as Tensor>::Context,
1277    ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error>;
1278}
1279
1280/// Stable routing metadata supplied by an architecture composition at one
1281/// canonical unit boundary.
1282#[derive(Debug, Clone, Eq, PartialEq)]
1283pub struct RoutedObservationPoint {
1284    path: String,
1285    expert_count: i32,
1286}
1287
1288impl RoutedObservationPoint {
1289    /// Creates one routed observation point.
1290    pub fn new(path: impl Into<String>, expert_count: i32) -> Self {
1291        Self {
1292            path: path.into(),
1293            expert_count,
1294        }
1295    }
1296
1297    /// Returns the stable routed-module path.
1298    pub fn path(&self) -> &str {
1299        &self.path
1300    }
1301
1302    /// Returns the total number of routed experts.
1303    pub const fn expert_count(&self) -> i32 {
1304        self.expert_count
1305    }
1306}
1307
1308/// Failure from either canonical expert execution or its observation hook.
1309#[derive(Debug)]
1310pub enum ObservedExpertProviderError<P, O> {
1311    /// The wrapped provider rejected or failed the expert request.
1312    Provider(P),
1313    /// The observer rejected the normalized routing event.
1314    Observer(O),
1315}
1316
1317impl<P, O> std::fmt::Display for ObservedExpertProviderError<P, O>
1318where
1319    P: std::fmt::Display,
1320    O: std::fmt::Display,
1321{
1322    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1323        match self {
1324            Self::Provider(error) => write!(formatter, "routed expert provider failed: {error}"),
1325            Self::Observer(error) => write!(formatter, "routed expert observer failed: {error}"),
1326        }
1327    }
1328}
1329
1330impl<P, O> std::error::Error for ObservedExpertProviderError<P, O>
1331where
1332    P: std::error::Error + 'static,
1333    O: std::error::Error + 'static,
1334{
1335}
1336
1337/// Decorates a routed provider with normalized routing observation.
1338///
1339/// The decorator sees the exact request and output of canonical provider
1340/// execution. It therefore adds observation without reimplementing a model
1341/// family's block, routing, shape, or residency lifecycle. Tensor-parallel
1342/// requests are delegated without an event because their provider result may
1343/// still require an architecture-owned reduction before it is observable.
1344pub struct ObservedExpertProvider<'a, P, O: ?Sized, E> {
1345    provider: &'a mut P,
1346    observer: &'a mut O,
1347    point: RoutedObservationPoint,
1348    error: std::marker::PhantomData<fn() -> E>,
1349}
1350
1351impl<'a, P, O: ?Sized, E> ObservedExpertProvider<'a, P, O, E> {
1352    /// Wraps `provider` for one canonical routed module invocation.
1353    pub fn new(provider: &'a mut P, observer: &'a mut O, point: RoutedObservationPoint) -> Self {
1354        Self {
1355            provider,
1356            observer,
1357            point,
1358            error: std::marker::PhantomData,
1359        }
1360    }
1361
1362    fn observe<T, ObservationError>(
1363        &mut self,
1364        routes: &eredu_nn::GroupSelection<T>,
1365        output: &T,
1366    ) -> Result<T, ObservationError>
1367    where
1368        T: Clone,
1369        O: ActivationObserver<T, ObservationError>,
1370    {
1371        self.observer.observe_routing(RoutingObservation {
1372            path: self.point.path(),
1373            selected_experts: routes.group_indices(),
1374            selected_scores: routes.selected_scores(),
1375            coefficients: routes.coefficients(),
1376            routed_output: output,
1377            local_routed_output: None,
1378            reduced_routed_output: None,
1379            shared_output: None,
1380            combined_output: None,
1381            expert_count: self.point.expert_count(),
1382        })?;
1383        observe_and_intervene(
1384            self.observer,
1385            &format!("{}.output", self.point.path()),
1386            output,
1387        )
1388    }
1389}
1390
1391impl<B, P, O, E> RoutedExpertProvider<B> for ObservedExpertProvider<'_, P, O, E>
1392where
1393    B: GroupedNeuralBackend,
1394    P: RoutedExpertProvider<B>,
1395    O: ActivationObserver<B::Tensor, E> + ?Sized,
1396{
1397    type Error = ObservedExpertProviderError<P::Error, E>;
1398
1399    fn routing_control(
1400        &mut self,
1401        token_rows: u64,
1402    ) -> Result<Option<eredu_nn::routing_intervention::GroupSelectionControl>, Self::Error> {
1403        self.observer
1404            .routing_control(self.point.path(), token_rows)
1405            .map_err(ObservedExpertProviderError::Observer)
1406    }
1407
1408    fn routing_applied(
1409        &mut self,
1410        original: Option<crate::RoutingDecision<'_, B::Tensor>>,
1411        effective: crate::RoutingDecision<'_, B::Tensor>,
1412    ) -> Result<(), Self::Error> {
1413        self.observer
1414            .routing_applied(self.point.path(), original, effective)
1415            .map_err(ObservedExpertProviderError::Observer)
1416    }
1417
1418    fn routing_failed(&mut self, message: &str) {
1419        self.observer.routing_failed(self.point.path(), message);
1420    }
1421
1422    fn forward_grouped(
1423        &mut self,
1424        resident_bank: &mut B::GatedProductGroups,
1425        request: RoutedExpertRequest<'_, B::Tensor>,
1426        context: &<B::Tensor as Tensor>::Context,
1427    ) -> Result<B::Tensor, Self::Error> {
1428        let routes = request.routes;
1429        let output = self
1430            .provider
1431            .forward_grouped(resident_bank, request, context)
1432            .map_err(ObservedExpertProviderError::Provider)?;
1433        self.observe(routes, &output)
1434            .map_err(ObservedExpertProviderError::Observer)
1435    }
1436
1437    fn forward_relu2_routed(
1438        &mut self,
1439        resident_bank: &mut B::Relu2Groups,
1440        request: RoutedExpertRequest<'_, B::Tensor>,
1441        context: &<B::Tensor as Tensor>::Context,
1442    ) -> Result<B::Tensor, Self::Error> {
1443        let routes = request.routes;
1444        let output = self
1445            .provider
1446            .forward_relu2_routed(resident_bank, request, context)
1447            .map_err(ObservedExpertProviderError::Provider)?;
1448        self.observe(routes, &output)
1449            .map_err(ObservedExpertProviderError::Observer)
1450    }
1451}
1452
1453impl<B, P, O, E> TensorParallelRoutedExpertProvider<B> for ObservedExpertProvider<'_, P, O, E>
1454where
1455    B: GroupedNeuralBackend,
1456    P: TensorParallelRoutedExpertProvider<B>,
1457    O: ActivationObserver<B::Tensor, E> + ?Sized,
1458{
1459    fn forward_grouped_tensor_parallel(
1460        &mut self,
1461        resident_bank: &mut B::GatedProductGroups,
1462        request: RoutedExpertRequest<'_, B::Tensor>,
1463        partitions: usize,
1464        context: &<B::Tensor as Tensor>::Context,
1465    ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error> {
1466        self.provider
1467            .forward_grouped_tensor_parallel(resident_bank, request, partitions, context)
1468            .map_err(ObservedExpertProviderError::Provider)
1469    }
1470
1471    fn forward_relu2_routed_tensor_parallel(
1472        &mut self,
1473        resident_bank: &mut B::Relu2Groups,
1474        request: RoutedExpertRequest<'_, B::Tensor>,
1475        partitions: usize,
1476        context: &<B::Tensor as Tensor>::Context,
1477    ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error> {
1478        self.provider
1479            .forward_relu2_routed_tensor_parallel(resident_bank, request, partitions, context)
1480            .map_err(ObservedExpertProviderError::Provider)
1481    }
1482}
1483
1484/// Provider for a fully resident expert bank.
1485#[derive(Debug, Default, Clone, Copy)]
1486pub struct ResidentExpertProvider;
1487
1488impl<B> RoutedExpertProvider<B> for ResidentExpertProvider
1489where
1490    B: GroupedNeuralBackend,
1491{
1492    type Error = eredu_nn::Error;
1493
1494    fn forward_grouped(
1495        &mut self,
1496        resident_bank: &mut B::GatedProductGroups,
1497        request: RoutedExpertRequest<'_, B::Tensor>,
1498        context: &<B::Tensor as Tensor>::Context,
1499    ) -> Result<B::Tensor, Self::Error> {
1500        resident_bank.forward_grouped(request.input, request.routes, context)
1501    }
1502
1503    fn forward_relu2_routed(
1504        &mut self,
1505        resident_bank: &mut B::Relu2Groups,
1506        request: RoutedExpertRequest<'_, B::Tensor>,
1507        context: &<B::Tensor as Tensor>::Context,
1508    ) -> Result<B::Tensor, Self::Error> {
1509        resident_bank.forward_grouped(request.input, request.routes, context)
1510    }
1511}
1512
1513impl<B> TensorParallelRoutedExpertProvider<B> for ResidentExpertProvider
1514where
1515    B: eredu_nn::TensorParallelGroupedNeuralBackend,
1516{
1517    fn forward_grouped_tensor_parallel(
1518        &mut self,
1519        resident_bank: &mut B::GatedProductGroups,
1520        request: RoutedExpertRequest<'_, B::Tensor>,
1521        partitions: usize,
1522        context: &<B::Tensor as Tensor>::Context,
1523    ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error> {
1524        B::gated_product_groups_tensor_parallel(
1525            resident_bank,
1526            request.input,
1527            request.routes,
1528            partitions,
1529            context,
1530        )
1531        .map(RoutedExpertTensorParallelOutput::Partial)
1532    }
1533
1534    fn forward_relu2_routed_tensor_parallel(
1535        &mut self,
1536        resident_bank: &mut B::Relu2Groups,
1537        request: RoutedExpertRequest<'_, B::Tensor>,
1538        partitions: usize,
1539        context: &<B::Tensor as Tensor>::Context,
1540    ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error> {
1541        B::relu2_groups_tensor_parallel(
1542            resident_bank,
1543            request.input,
1544            request.routes,
1545            partitions,
1546            context,
1547        )
1548        .map(RoutedExpertTensorParallelOutput::Partial)
1549    }
1550}