Skip to main content

eredu_runtime/
layered.rs

1//! Statically dispatched layered-architecture lifecycle and resident execution.
2
3#![allow(clippy::too_many_arguments, clippy::type_complexity)]
4
5use std::collections::BTreeMap;
6
7use eredu_checkpoint::{recipe::DerivedWeightRecipe, store::CheckpointSource};
8use eredu_nn::{NeuralBackend, Parameterized};
9
10use crate::{
11    observe_and_intervene, ActivationObserver, ExecutionGraph, ExecutionGroupSchedule,
12    ExecutionScheduleError, ExecutionUnitLayout, ExpertPass, NoAuxiliaryBoundary,
13    ObservedExpertProvider, RoutedExpertProvider, RoutedObservationPoint, RuntimeState,
14    StateLayout, SubmissionBackend,
15};
16
17/// Statically dispatched visitor over one immutable pinned parameter module.
18pub trait StaticParameterVisitor<B: NeuralBackend> {
19    /// Failure returned by the consumer.
20    type Error;
21
22    /// Visits the module bound to one architecture-declared static role.
23    fn visit<M>(&mut self, role: &str, module: &M) -> Result<(), Self::Error>
24    where
25        M: Parameterized<B::Tensor>;
26}
27
28/// Statically dispatched visitor over one mutable pinned parameter module.
29pub trait StaticParameterVisitorMut<B: NeuralBackend> {
30    /// Failure returned by the consumer.
31    type Error;
32
33    /// Visits the mutable module bound to one architecture-declared static role.
34    fn visit_mut<M>(&mut self, role: &str, module: &mut M) -> Result<(), Self::Error>
35    where
36        M: Parameterized<B::Tensor>;
37}
38
39/// Architecture-owned enumeration and binding of pinned parameter modules.
40///
41/// Parameter descriptions select the roles owned by a partition. This
42/// contract resolves those roles to concrete neutral modules without making a
43/// backend know family fields or checkpoint roots.
44pub trait ArchitectureParameters<B: NeuralBackend> {
45    /// Architecture-owned failure while deriving geometry or topology.
46    type DefinitionError;
47
48    /// Returns the authoritative mutable-state geometry for this realization.
49    fn state_layout(&self) -> Result<StateLayout, Self::DefinitionError>;
50
51    /// Declares cache-relevant architecture identity for one realized state partition.
52    ///
53    /// The partition supplies the exact rank-local layout and architecture-global
54    /// offset. Concrete backends supply only their lowered parallel topology.
55    fn state_identity(
56        &self,
57        state: &crate::PartitionState,
58        topology: eredu_core::cache::PromptCacheTopology,
59    ) -> Result<crate::ModelStateIdentity, Self::DefinitionError>;
60
61    /// Describes every parameter with its canonical graph owner and placement.
62    fn parameter_description(
63        &self,
64        context: &<B::Tensor as eredu_nn::Tensor>::Context,
65    ) -> Result<crate::ArchitectureParameterDescription, Self::DefinitionError>;
66
67    /// Returns architecture-owned checkpoint rewrites for pinned parameters.
68    fn static_parameter_recipes(
69        &self,
70        _source: &dyn CheckpointSource,
71    ) -> Result<BTreeMap<String, DerivedWeightRecipe>, String> {
72        Ok(BTreeMap::new())
73    }
74
75    /// Visits every available pinned parameter module exactly once.
76    fn visit_static_parameters<V>(&self, visitor: &mut V) -> Result<(), V::Error>
77    where
78        V: StaticParameterVisitor<B>;
79
80    /// Mutably visits every available pinned parameter module exactly once.
81    fn visit_static_parameters_mut<V>(&mut self, visitor: &mut V) -> Result<(), V::Error>
82    where
83        V: StaticParameterVisitorMut<B>;
84}
85
86/// Backend-native activation and architecture-owned forward context.
87pub struct LayeredForwardState<T, C> {
88    /// Initial activation supplied to the first execution unit.
89    pub hidden: T,
90    /// Masks, positions, or other architecture-owned forward values.
91    pub context: C,
92}
93
94/// Architecture-authored semantic kind for one transport-visible execution group.
95#[derive(Debug, Clone, Copy, Eq, PartialEq)]
96pub enum ArchitectureGroupKind {
97    /// Primary text decoding.
98    Decoder,
99    /// Embedded prediction after the primary decoder output.
100    Prediction,
101    /// Visual encoding.
102    VisionEncoder,
103    /// Audio encoding.
104    AudioEncoder,
105    /// Learned modality projection.
106    Projector,
107    /// Learned or structural modality merge.
108    Merger,
109    /// Final multimodal assembly.
110    ModalityFinalization,
111}
112
113/// Backend-neutral lifecycle for the pipeline ingress phase of a layered graph.
114///
115/// Architectures declare semantic group kinds and request optionality while
116/// concrete backends report only whether optional encoder roots have work. The
117/// runtime derives all downstream activity, admits dependency-ready compatible
118/// batches, and owns completion transitions. Pipeline backends therefore share
119/// the same graph lifecycle instead of reconstructing it around native streams
120/// and routes.
121#[derive(Debug)]
122pub struct LayeredPipelineSchedule<'a> {
123    graph: &'a ExecutionGraph,
124    schedule: ExecutionGroupSchedule<'a>,
125    active: Vec<bool>,
126    completed: usize,
127}
128
129impl<'a> LayeredPipelineSchedule<'a> {
130    /// Creates the pipeline ingress lifecycle from canonical architecture contracts.
131    ///
132    /// Each contract pairs a group's semantic kind with its declared request
133    /// optionality. `request_group_active` is called only for optional encoder
134    /// roots. Mandatory encoders, decoder ingress, and finalization always run;
135    /// structural merge activity is derived from dependency activity; and
136    /// prediction is a later phase.
137    pub fn try_new<E>(
138        graph: &'a ExecutionGraph,
139        group_contracts: impl IntoIterator<Item = (ArchitectureGroupKind, bool)>,
140        mut request_group_active: impl FnMut(usize) -> Result<bool, E>,
141    ) -> Result<Self, E>
142    where
143        E: From<LayeredPipelineScheduleError>,
144    {
145        let group_contracts = group_contracts.into_iter().collect::<Vec<_>>();
146        if group_contracts.len() != graph.groups().len() {
147            return Err(LayeredPipelineScheduleError::GroupContractCount {
148                graph: graph.groups().len(),
149                declared: group_contracts.len(),
150            }
151            .into());
152        }
153        let mut active = vec![false; group_contracts.len()];
154        for &group in graph.execution_order() {
155            let (kind, request_optional) = group_contracts[group];
156            if request_optional
157                && (!matches!(
158                    kind,
159                    ArchitectureGroupKind::VisionEncoder | ArchitectureGroupKind::AudioEncoder
160                ) || !graph.groups()[group].dependencies().is_empty())
161            {
162                return Err(LayeredPipelineScheduleError::InvalidRequestOptionalGroup {
163                    group,
164                    kind,
165                }
166                .into());
167            }
168            active[group] = match kind {
169                ArchitectureGroupKind::VisionEncoder | ArchitectureGroupKind::AudioEncoder => {
170                    !request_optional || request_group_active(group)?
171                }
172                ArchitectureGroupKind::Projector | ArchitectureGroupKind::Merger => graph
173                    .dependencies(group)
174                    .expect("validated execution order contains a known group")
175                    .iter()
176                    .any(|&dependency| active[dependency]),
177                ArchitectureGroupKind::ModalityFinalization | ArchitectureGroupKind::Decoder => {
178                    true
179                }
180                ArchitectureGroupKind::Prediction => false,
181            };
182        }
183        Ok(Self {
184            graph,
185            schedule: ExecutionGroupSchedule::new(graph),
186            active,
187            completed: 0,
188        })
189    }
190
191    /// Returns whether a group participates in this pipeline ingress pass.
192    pub fn is_active(&self, group: usize) -> Option<bool> {
193        self.active.get(group).copied()
194    }
195
196    /// Returns all group activity in canonical architecture order.
197    pub fn activity(&self) -> &[bool] {
198        &self.active
199    }
200
201    /// Returns dependency-ready groups in stable architecture order.
202    pub fn ready_groups(&self) -> impl Iterator<Item = usize> + '_ {
203        self.schedule.startable_groups()
204    }
205
206    /// Selects a deterministic maximal compatible subset of ready groups.
207    pub fn compatible_batch(&self, mut compatible: impl FnMut(usize, usize) -> bool) -> Vec<usize> {
208        let mut selected = Vec::new();
209        for candidate in self.ready_groups() {
210            if selected
211                .iter()
212                .copied()
213                .all(|group| compatible(group, candidate))
214            {
215                selected.push(candidate);
216            }
217        }
218        selected
219    }
220
221    /// Returns dependency slots in architecture declaration order.
222    pub fn dependencies(&self, group: usize) -> Option<&[usize]> {
223        self.graph.dependencies(group)
224    }
225
226    /// Commits architecture setup for a dependency-ready group.
227    ///
228    /// The returned producer slots no longer need to retain their outputs for
229    /// another consumer after this group has captured its dependencies.
230    pub fn started(&mut self, group: usize) -> Result<Vec<usize>, LayeredPipelineScheduleError> {
231        self.schedule.started(group).map_err(Into::into)
232    }
233
234    /// Commits one successfully submitted group and unlocks its dependents.
235    pub fn ordered(&mut self, group: usize) -> Result<(), LayeredPipelineScheduleError> {
236        self.schedule.ordered(group)?;
237        self.completed += 1;
238        Ok(())
239    }
240
241    /// Returns whether every architecture group has completed this phase.
242    pub fn is_complete(&self) -> bool {
243        self.completed == self.active.len()
244    }
245}
246
247/// Invalid backend-neutral pipeline lifecycle declaration or transition.
248#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
249pub enum LayeredPipelineScheduleError {
250    /// The physical realization did not preserve one contract per canonical group.
251    #[error(
252        "execution graph contains {graph} groups but the pipeline declared {declared} group contracts"
253    )]
254    GroupContractCount {
255        /// Number of canonical graph groups.
256        graph: usize,
257        /// Number of supplied lifecycle contracts.
258        declared: usize,
259    },
260    /// Request optionality was attached to a group which cannot consume request media directly.
261    #[error("execution group {group} of kind {kind:?} cannot be request-optional")]
262    InvalidRequestOptionalGroup {
263        /// Canonical architecture group slot.
264        group: usize,
265        /// Declared semantic group kind.
266        kind: ArchitectureGroupKind,
267    },
268    /// An ordinary execution-group lifecycle invariant failed.
269    #[error(transparent)]
270    Transition(#[from] ExecutionScheduleError),
271}
272
273/// Pipeline ownership policy for one architecture execution group.
274#[derive(Debug, Clone, Copy, Eq, PartialEq)]
275pub enum ArchitectureGroupPlacement {
276    /// Balance the group across every pipeline owner.
277    Pipeline,
278    /// Place the complete group on the architecture output owner.
279    OutputOwner,
280}
281
282/// Architecture-level merge destination resolved by a concrete pipeline topology.
283#[derive(Debug, Clone, Copy, Eq, PartialEq)]
284pub enum ArchitectureMergeDestination {
285    /// Use the group's terminal owner.
286    LastOwner,
287    /// Return the result to the first pipeline owner for dependency assembly.
288    FirstPipelineOwner,
289    /// Deliver the result to the architecture output owner.
290    OutputOwner,
291}
292
293/// Cartesian subgroup semantics required while a group executes.
294#[derive(Debug, Clone, Copy, Eq, PartialEq)]
295pub enum ArchitectureParallelSubgroup {
296    /// Tensor sharding without routed expert exchange.
297    TensorSharded,
298    /// Decoder tensor and routed-expert parallelism.
299    Decoder,
300}
301
302/// Backend-neutral transport and placement semantics for one execution group.
303#[derive(Debug, Clone, Eq, PartialEq)]
304pub struct ArchitectureGroupTransport {
305    /// Physical pipeline ownership policy.
306    pub placement: ArchitectureGroupPlacement,
307    /// Semantic compute kind.
308    pub kind: ArchitectureGroupKind,
309    /// Static roles owned by the group's first physical owner.
310    pub first_owner_static_roles: Vec<String>,
311    /// Static roles owned by the group's terminal physical owner.
312    pub last_owner_static_roles: Vec<String>,
313    /// Dependency merge destination.
314    pub merge_destination: ArchitectureMergeDestination,
315    /// Optional active Cartesian subgroup contract.
316    pub parallel_subgroup: Option<ArchitectureParallelSubgroup>,
317    /// Whether request media may omit this root encoder group entirely.
318    pub request_optional: bool,
319}
320
321/// Stable layered traversal boundary exposed to generic runtime drivers.
322#[derive(Debug, Clone, Copy, Eq, PartialEq)]
323pub enum LayeredTraversalPoint {
324    /// Output of one execution unit before the next unit starts.
325    Unit {
326        /// Execution-group index.
327        group: usize,
328        /// Group-local unit index.
329        index: usize,
330    },
331    /// Output of one completed execution group.
332    Group {
333        /// Execution-group index.
334        group: usize,
335    },
336}
337
338/// Decision returned immediately before one execution unit is acquired.
339#[derive(Debug, Clone, Copy, Eq, PartialEq)]
340pub enum LayeredUnitAction {
341    /// Execute this unit normally.
342    Execute,
343    /// Omit this unit and every remaining unit in the current group.
344    SkipRemainingGroup,
345}
346
347/// Statically dispatched hook shared by resident and bounded layered traversal.
348///
349/// The hook can observe unit/group outputs and can omit only a complete group
350/// tail. Drivers are responsible for proving that an omission preserves their
351/// semantics before returning [`LayeredUnitAction::SkipRemainingGroup`].
352pub trait LayeredTraversalHook<B, C, E>
353where
354    B: NeuralBackend,
355{
356    /// Chooses whether to execute the next unit.
357    fn before_unit(
358        &mut self,
359        _group: usize,
360        _index: usize,
361        _remaining_units: usize,
362        _value: &B::Tensor,
363        _forward: &mut C,
364        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
365    ) -> Result<LayeredUnitAction, E> {
366        Ok(LayeredUnitAction::Execute)
367    }
368
369    /// Observes one executed unit output.
370    fn after_unit(
371        &mut self,
372        _group: usize,
373        _index: usize,
374        _value: &B::Tensor,
375        _forward: &mut C,
376        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
377    ) -> Result<(), E> {
378        Ok(())
379    }
380
381    /// Observes one completed execution-group output.
382    fn after_group(
383        &mut self,
384        _group: usize,
385        _value: &B::Tensor,
386        _forward: &mut C,
387        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
388    ) -> Result<(), E> {
389        Ok(())
390    }
391}
392
393/// Statically combines two traversal hooks over one production forward pass.
394///
395/// Both hooks observe every reached boundary in left-to-right order. A unit is
396/// skipped when either hook proves that the remaining group tail can be
397/// omitted; errors stop delegation before any later callback is invoked.
398pub struct CompositeLayeredTraversalHook<L, R> {
399    left: L,
400    right: R,
401}
402
403impl<L, R> CompositeLayeredTraversalHook<L, R> {
404    /// Creates one ordered pair of traversal hooks.
405    pub const fn new(left: L, right: R) -> Self {
406        Self { left, right }
407    }
408
409    /// Returns both hooks after traversal.
410    pub fn into_parts(self) -> (L, R) {
411        (self.left, self.right)
412    }
413}
414
415impl<B, C, E, L, R> LayeredTraversalHook<B, C, E> for CompositeLayeredTraversalHook<L, R>
416where
417    B: NeuralBackend,
418    L: LayeredTraversalHook<B, C, E>,
419    R: LayeredTraversalHook<B, C, E>,
420{
421    fn before_unit(
422        &mut self,
423        group: usize,
424        index: usize,
425        remaining_units: usize,
426        value: &B::Tensor,
427        forward: &mut C,
428        context: &<B::Tensor as eredu_nn::Tensor>::Context,
429    ) -> Result<LayeredUnitAction, E> {
430        let left = self
431            .left
432            .before_unit(group, index, remaining_units, value, forward, context)?;
433        let right =
434            self.right
435                .before_unit(group, index, remaining_units, value, forward, context)?;
436        Ok(
437            if left == LayeredUnitAction::SkipRemainingGroup
438                || right == LayeredUnitAction::SkipRemainingGroup
439            {
440                LayeredUnitAction::SkipRemainingGroup
441            } else {
442                LayeredUnitAction::Execute
443            },
444        )
445    }
446
447    fn after_unit(
448        &mut self,
449        group: usize,
450        index: usize,
451        value: &B::Tensor,
452        forward: &mut C,
453        context: &<B::Tensor as eredu_nn::Tensor>::Context,
454    ) -> Result<(), E> {
455        self.left
456            .after_unit(group, index, value, forward, context)?;
457        self.right.after_unit(group, index, value, forward, context)
458    }
459
460    fn after_group(
461        &mut self,
462        group: usize,
463        value: &B::Tensor,
464        forward: &mut C,
465        context: &<B::Tensor as eredu_nn::Tensor>::Context,
466    ) -> Result<(), E> {
467        self.left.after_group(group, value, forward, context)?;
468        self.right.after_group(group, value, forward, context)
469    }
470}
471
472struct NoopLayeredTraversalHook;
473
474impl<B, C, E> LayeredTraversalHook<B, C, E> for NoopLayeredTraversalHook where B: NeuralBackend {}
475
476struct AfterUnitTraversalHook<F> {
477    after_unit: F,
478}
479
480struct AfterUnitContextTraversalHook<F> {
481    after_unit: F,
482}
483
484impl<B, C, E, F> LayeredTraversalHook<B, C, E> for AfterUnitTraversalHook<F>
485where
486    B: NeuralBackend,
487    F: FnMut(usize, usize, &B::Tensor, &mut C) -> Result<(), E>,
488{
489    fn after_unit(
490        &mut self,
491        group: usize,
492        index: usize,
493        value: &B::Tensor,
494        forward: &mut C,
495        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
496    ) -> Result<(), E> {
497        (self.after_unit)(group, index, value, forward)
498    }
499}
500
501impl<B, C, E, F> LayeredTraversalHook<B, C, E> for AfterUnitContextTraversalHook<F>
502where
503    B: NeuralBackend,
504    F: FnMut(usize, usize, &mut C) -> Result<(), E>,
505{
506    fn after_unit(
507        &mut self,
508        group: usize,
509        index: usize,
510        _value: &B::Tensor,
511        forward: &mut C,
512        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
513    ) -> Result<(), E> {
514        (self.after_unit)(group, index, forward)
515    }
516}
517
518/// Backend-neutral lifecycle implemented once by a layered architecture.
519///
520/// All hot values remain concrete associated types. Resident and bounded
521/// runtime policies call these same methods without erasing tensors, units, or
522/// mutable layer state.
523pub trait LayeredArchitecture<B, S>:
524    ArchitectureParameters<B, DefinitionError = Self::Error>
525where
526    B: NeuralBackend,
527    S: RuntimeState<B>,
528{
529    /// Borrowed prepared model input.
530    type Input<'a>
531    where
532        Self: 'a;
533    /// Pinned model modules such as embeddings, final normalization, and head.
534    type StaticModules: Parameterized<B::Tensor>;
535    /// One ordered execution unit.
536    type Unit: Parameterized<B::Tensor>;
537    /// Architecture-owned state retained for one complete forward pass.
538    type ForwardContext;
539    /// Allocation-free iterator over transient tensors retained by a unit submission.
540    type RetainedContextValues<'a>: Iterator<Item = &'a B::Tensor>
541    where
542        Self: 'a,
543        B::Tensor: 'a;
544    /// Concrete architecture or backend failure.
545    type Error;
546
547    /// Declares transport and physical placement semantics for one canonical group slot.
548    fn group_transport(&self, group: usize) -> ArchitectureGroupTransport;
549
550    /// Returns the stable identifier of the primary pipeline execution group.
551    ///
552    /// Pipeline composition resolves this identifier against [`Self::execution_graph`]
553    /// instead of guessing the primary group from its semantic kind. Architectures may
554    /// therefore declare multiple decoder-shaped groups without making composition
555    /// dependent on declaration order.
556    fn primary_execution_group(&self) -> &str;
557
558    /// Returns stable identifiers for ordered embedded-prediction groups.
559    ///
560    /// The order is the architecture's prediction-depth order. Semantic group kinds
561    /// remain lifecycle metadata and are not used as group addresses.
562    fn prediction_execution_groups(&self) -> Vec<String> {
563        Vec::new()
564    }
565
566    /// Declares how the complete mutable-state layout is divided among realized partitions.
567    fn state_partition_plan(&self, layout: &StateLayout) -> crate::ArchitectureStatePartitionPlan;
568
569    /// Declares the dependency graph between ordered execution groups.
570    fn execution_graph(&self) -> Result<ExecutionGraph, Self::Error>;
571
572    /// Returns the number of ordered execution units in one graph group.
573    fn group_unit_count(&self, group: usize) -> Result<usize, Self::Error>;
574
575    /// Returns the stable architecture-owned path of one group-local execution unit.
576    fn unit_path(&self, group: usize, index: usize) -> Result<String, Self::Error>;
577
578    /// Borrows pinned modules for parameter discovery and binding.
579    fn static_modules(&self) -> &Self::StaticModules;
580
581    /// Mutably borrows pinned modules for parameter binding.
582    fn static_modules_mut(&mut self) -> &mut Self::StaticModules;
583
584    /// Builds one unloaded execution unit using backend-native operators.
585    fn build_unit(
586        &self,
587        group: usize,
588        index: usize,
589        context: &<B::Tensor as eredu_nn::Tensor>::Context,
590    ) -> Result<Self::Unit, Self::Error>;
591
592    /// Embeds input and prepares architecture-owned forward values.
593    fn begin_forward<'a>(
594        &mut self,
595        input: Self::Input<'a>,
596        state: &mut S,
597        context: &<B::Tensor as eredu_nn::Tensor>::Context,
598    ) -> Result<LayeredForwardState<B::Tensor, Self::ForwardContext>, Self::Error>;
599
600    /// Selects or merges the activation consumed by one ready execution group.
601    fn begin_execution_group(
602        &mut self,
603        group: usize,
604        initial: &B::Tensor,
605        dependencies: &[&B::Tensor],
606        state: &mut S,
607        forward: &mut Self::ForwardContext,
608        context: &<B::Tensor as eredu_nn::Tensor>::Context,
609    ) -> Result<B::Tensor, Self::Error>;
610
611    /// Returns whether one ready group is needed for this forward pass.
612    fn should_execute_group(&self, _group: usize, _forward: &Self::ForwardContext) -> bool {
613        true
614    }
615
616    /// Maps one execution unit to its architecture-global mutable-state slot.
617    ///
618    /// The default matches a single flattened decoder schedule. Composite
619    /// architectures can keep parameter-only groups outside their state layout
620    /// and remap later groups onto their semantic decoder or predictor layers.
621    fn state_ordinal(&self, _group: usize, _index: usize, ordinal: usize) -> usize {
622        ordinal
623    }
624
625    /// Returns every architecture-global state slot retained by one unit.
626    ///
627    /// The default retains the single slot returned by [`Self::state_ordinal`].
628    /// Composite units can return a contiguous range when one residency unit
629    /// internally executes several stateful layers.
630    fn retained_state_ordinals(
631        &self,
632        group: usize,
633        index: usize,
634        ordinal: usize,
635    ) -> std::ops::Range<usize> {
636        let state = self.state_ordinal(group, index, ordinal);
637        state..state + 1
638    }
639
640    /// Executes one ordered unit against its concrete mutable layer state.
641    fn forward_unit(
642        &mut self,
643        group: usize,
644        index: usize,
645        unit: &mut Self::Unit,
646        hidden: &B::Tensor,
647        state: &mut S,
648        forward: &mut Self::ForwardContext,
649        context: &<B::Tensor as eredu_nn::Tensor>::Context,
650    ) -> Result<B::Tensor, Self::Error>;
651
652    /// Converts a completed group's output into its dependency-facing value.
653    fn complete_execution_group(
654        &mut self,
655        _group: usize,
656        hidden: &B::Tensor,
657        _state: &mut S,
658        _forward: &mut Self::ForwardContext,
659        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
660    ) -> Result<B::Tensor, Self::Error> {
661        Ok(hidden.clone())
662    }
663
664    /// Applies final normalization and output projection.
665    fn finish_forward(
666        &mut self,
667        hidden: &B::Tensor,
668        state: &mut S,
669        forward: &Self::ForwardContext,
670        context: &<B::Tensor as eredu_nn::Tensor>::Context,
671    ) -> Result<B::Tensor, Self::Error>;
672
673    /// Borrows transient forward tensors required by one unit's submission.
674    fn retained_context_values<'a>(
675        &'a self,
676        forward: &'a Self::ForwardContext,
677        group: usize,
678        index: usize,
679    ) -> Self::RetainedContextValues<'a>;
680}
681
682/// Optional statically dispatched parallel lifecycle for a layered architecture.
683///
684/// The runtime owns traversal and exact unit completion while the architecture
685/// owns parallel embedding, block, and output semantics. Backend-native
686/// collective contexts cross this boundary unchanged.
687pub trait ParallelLayeredArchitecture<B, S>: LayeredArchitecture<B, S>
688where
689    B: NeuralBackend,
690    S: RuntimeState<B>,
691{
692    /// Embeds input and prepares forward values for rank-local execution.
693    fn begin_forward_parallel<'a>(
694        &mut self,
695        input: Self::Input<'a>,
696        state: &mut S,
697        parallel: &B::ParallelContext,
698        context: &<B::Tensor as eredu_nn::Tensor>::Context,
699    ) -> Result<LayeredForwardState<B::Tensor, Self::ForwardContext>, Self::Error>;
700
701    /// Executes one rank-local unit and its required collectives.
702    fn forward_unit_parallel(
703        &mut self,
704        group_index: usize,
705        index: usize,
706        unit: &mut Self::Unit,
707        hidden: &B::Tensor,
708        state: &mut S,
709        forward: &mut Self::ForwardContext,
710        parallel: &B::ParallelContext,
711        context: &<B::Tensor as eredu_nn::Tensor>::Context,
712    ) -> Result<B::Tensor, Self::Error>;
713
714    /// Selects or merges a ready group's activation under a parallel context.
715    fn begin_execution_group_parallel(
716        &mut self,
717        group_index: usize,
718        initial: &B::Tensor,
719        dependencies: &[&B::Tensor],
720        state: &mut S,
721        forward: &mut Self::ForwardContext,
722        _parallel: &B::ParallelContext,
723        context: &<B::Tensor as eredu_nn::Tensor>::Context,
724    ) -> Result<B::Tensor, Self::Error> {
725        self.begin_execution_group(group_index, initial, dependencies, state, forward, context)
726    }
727
728    /// Converts a completed group's output under a parallel context.
729    fn complete_execution_group_parallel(
730        &mut self,
731        group_index: usize,
732        hidden: &B::Tensor,
733        state: &mut S,
734        forward: &mut Self::ForwardContext,
735        _parallel: &B::ParallelContext,
736        context: &<B::Tensor as eredu_nn::Tensor>::Context,
737    ) -> Result<B::Tensor, Self::Error> {
738        self.complete_execution_group(group_index, hidden, state, forward, context)
739    }
740
741    /// Applies the rank-local output projection and returns complete logits.
742    fn finish_forward_parallel(
743        &mut self,
744        hidden: &B::Tensor,
745        state: &mut S,
746        forward: &Self::ForwardContext,
747        parallel: &B::ParallelContext,
748        context: &<B::Tensor as eredu_nn::Tensor>::Context,
749    ) -> Result<B::Tensor, Self::Error>;
750}
751
752/// Input accepted at a rank-local layered partition boundary.
753///
754/// A partition either embeds borrowed token ids or consumes an owned hidden
755/// tensor prepared by architecture ingress or a preceding pipeline owner.
756#[derive(Debug)]
757pub enum LayeredPartitionInput<'a, T, A = NoAuxiliaryBoundary> {
758    /// Token ids supplied to the architecture input owner.
759    Tokens(&'a T),
760    /// Architecture-prepared or upstream hidden state.
761    Hidden {
762        /// Evolving activation received from the preceding owner.
763        hidden: T,
764        /// Architecture-typed context carried across the partition boundary.
765        auxiliary: A,
766    },
767}
768
769/// Architecture-owned result of completing one layered partition.
770///
771/// The runtime and concrete transports only distinguish a final output from a
772/// transport boundary. Families retain ownership of auxiliary boundary values
773/// and of any hidden activation required by an embedded predictor.
774pub enum LayeredPartitionOutput<T, A = NoAuxiliaryBoundary> {
775    /// Complete architecture output produced by the output owner.
776    Final {
777        /// Projected architecture output, normally vocabulary logits.
778        output: T,
779        /// Optional pre-projection value consumed by an embedded predictor.
780        retained: Option<T>,
781    },
782    /// Values transported to the next pipeline owner.
783    Boundary {
784        /// Evolving activation.
785        hidden: T,
786        /// Architecture-typed auxiliary context.
787        auxiliary: A,
788    },
789}
790
791/// Architecture-owned preparation for rank-local partition execution.
792///
793/// The neutral partition driver owns validation, group sequencing, and output
794/// ownership. Architectures own the semantic conversion of partition inputs,
795/// entry into and completion of their selected execution group, and typed
796/// partition output.
797pub trait PartitionedLayeredArchitecture<B, S>: ParallelLayeredArchitecture<B, S>
798where
799    B: NeuralBackend,
800    S: RuntimeState<B>,
801{
802    /// Architecture-owned schema for primary and auxiliary partition transport.
803    type Boundary: crate::ArchitectureBoundary;
804
805    /// Derives the complete transport schema from the normalized architecture.
806    fn boundary_schema(&self) -> Result<Self::Boundary, Self::Error>;
807
808    /// Prepares a replicated partition from tokens or upstream hidden state.
809    fn begin_partition<'a>(
810        &mut self,
811        input: LayeredPartitionInput<
812            'a,
813            B::Tensor,
814            <Self::Boundary as crate::ArchitectureBoundary>::Boundary<B::Tensor>,
815        >,
816        mask: Option<&B::Tensor>,
817        state: &mut S,
818        expected: &crate::StateLayout,
819        first_state_ordinal: usize,
820        context: &<B::Tensor as eredu_nn::Tensor>::Context,
821    ) -> Result<LayeredForwardState<B::Tensor, Self::ForwardContext>, Self::Error>;
822
823    /// Prepares the tensor-parallel form of the same partition.
824    #[allow(clippy::too_many_arguments)]
825    fn begin_partition_parallel<'a>(
826        &mut self,
827        input: LayeredPartitionInput<
828            'a,
829            B::Tensor,
830            <Self::Boundary as crate::ArchitectureBoundary>::Boundary<B::Tensor>,
831        >,
832        mask: Option<&B::Tensor>,
833        state: &mut S,
834        expected: &crate::StateLayout,
835        first_state_ordinal: usize,
836        parallel: &B::ParallelContext,
837        context: &<B::Tensor as eredu_nn::Tensor>::Context,
838    ) -> Result<LayeredForwardState<B::Tensor, Self::ForwardContext>, Self::Error>;
839
840    /// Enters the selected execution group after the partition input has been
841    /// prepared. The default is the ordinary layered group entry with no graph
842    /// dependencies; graph architectures may override this when their
843    /// partition input is already assembled.
844    #[allow(clippy::too_many_arguments)]
845    fn enter_partition_group(
846        &mut self,
847        group: usize,
848        initial: &B::Tensor,
849        state: &mut S,
850        forward: &mut Self::ForwardContext,
851        parallel: Option<&B::ParallelContext>,
852        context: &<B::Tensor as eredu_nn::Tensor>::Context,
853    ) -> Result<B::Tensor, Self::Error> {
854        match parallel {
855            Some(parallel) => self.begin_execution_group_parallel(
856                group,
857                initial,
858                &[],
859                state,
860                forward,
861                parallel,
862                context,
863            ),
864            None => self.begin_execution_group(group, initial, &[], state, forward, context),
865        }
866    }
867
868    /// Completes the selected execution group before the architecture emits
869    /// its final value or typed pipeline boundary.
870    #[allow(clippy::too_many_arguments)]
871    fn leave_partition_group(
872        &mut self,
873        group: usize,
874        hidden: &B::Tensor,
875        state: &mut S,
876        forward: &mut Self::ForwardContext,
877        parallel: Option<&B::ParallelContext>,
878        context: &<B::Tensor as eredu_nn::Tensor>::Context,
879    ) -> Result<B::Tensor, Self::Error> {
880        match parallel {
881            Some(parallel) => self.complete_execution_group_parallel(
882                group, hidden, state, forward, parallel, context,
883            ),
884            None => self.complete_execution_group(group, hidden, state, forward, context),
885        }
886    }
887
888    /// Emits an architecture partition after its execution group has closed.
889    #[allow(clippy::too_many_arguments)]
890    fn finish_partition(
891        &mut self,
892        hidden: &B::Tensor,
893        state: &mut S,
894        forward: &Self::ForwardContext,
895        owns_output: bool,
896        parallel: Option<&B::ParallelContext>,
897        context: &<B::Tensor as eredu_nn::Tensor>::Context,
898    ) -> Result<
899        LayeredPartitionOutput<
900            B::Tensor,
901            <Self::Boundary as crate::ArchitectureBoundary>::Boundary<B::Tensor>,
902        >,
903        Self::Error,
904    >;
905}
906
907/// Provider-aware unit execution for architectures with routed feed-forward work.
908///
909/// Partition drivers retain ownership of expert residency while the neutral
910/// architecture retains attention, residual, routing, and unit semantics.
911pub trait RoutedLayeredArchitecture<B, S>: LayeredArchitecture<B, S>
912where
913    B: eredu_nn::GroupedNeuralBackend,
914    S: RuntimeState<B>,
915{
916    /// Returns the architecture-owned routing observation point for one unit.
917    ///
918    /// Architectures without observable routed work in the selected unit return
919    /// `None`. Concrete backends must not reconstruct semantic paths or expert
920    /// cardinality.
921    fn routed_observation_point(
922        &self,
923        _group: usize,
924        _index: usize,
925    ) -> Result<Option<RoutedObservationPoint>, Self::Error> {
926        Ok(None)
927    }
928
929    /// Executes one unit through a runtime-supplied routed-expert provider.
930    #[allow(clippy::too_many_arguments)]
931    fn forward_unit_with_provider<P>(
932        &mut self,
933        group: usize,
934        index: usize,
935        unit: &mut Self::Unit,
936        hidden: &B::Tensor,
937        state: &mut S,
938        forward: &mut Self::ForwardContext,
939        pass: ExpertPass,
940        provider: &mut P,
941        context: &<B::Tensor as eredu_nn::Tensor>::Context,
942    ) -> Result<B::Tensor, Self::Error>
943    where
944        P: RoutedExpertProvider<B>,
945        P::Error: std::fmt::Display;
946}
947
948/// Tensor-parallel provider-aware unit execution.
949pub trait ParallelRoutedLayeredArchitecture<B, S>:
950    RoutedLayeredArchitecture<B, S> + ParallelLayeredArchitecture<B, S>
951where
952    B: eredu_nn::GroupedNeuralBackend,
953    S: RuntimeState<B>,
954{
955    /// Executes one tensor-parallel unit through a runtime-supplied provider.
956    #[allow(clippy::too_many_arguments)]
957    fn forward_unit_parallel_with_provider<P>(
958        &mut self,
959        group: usize,
960        index: usize,
961        unit: &mut Self::Unit,
962        hidden: &B::Tensor,
963        state: &mut S,
964        forward: &mut Self::ForwardContext,
965        pass: ExpertPass,
966        provider: &mut P,
967        parallel: &B::ParallelContext,
968        context: &<B::Tensor as eredu_nn::Tensor>::Context,
969    ) -> Result<B::Tensor, Self::Error>
970    where
971        P: crate::TensorParallelRoutedExpertProvider<B>,
972        P::Error: std::fmt::Display;
973}
974
975/// Fully resident runtime using the same lifecycle as bounded execution.
976pub struct ResidentRuntime<A, B, S>
977where
978    B: NeuralBackend,
979    S: RuntimeState<B>,
980    A: LayeredArchitecture<B, S>,
981{
982    architecture: A,
983    graph: ExecutionGraph,
984    units: Vec<Vec<A::Unit>>,
985    backend: std::marker::PhantomData<fn() -> (B, S)>,
986}
987
988impl<A, B, S> ResidentRuntime<A, B, S>
989where
990    B: NeuralBackend,
991    S: RuntimeState<B>,
992    A: LayeredArchitecture<B, S>,
993{
994    /// Builds every execution unit once and keeps it resident.
995    pub fn new(
996        architecture: A,
997        context: &<B::Tensor as eredu_nn::Tensor>::Context,
998    ) -> Result<Self, A::Error> {
999        let graph = architecture.execution_graph()?;
1000        let mut units = Vec::with_capacity(graph.groups().len());
1001        for group in 0..graph.groups().len() {
1002            let count = architecture.group_unit_count(group)?;
1003            units.push(
1004                (0..count)
1005                    .map(|index| architecture.build_unit(group, index, context))
1006                    .collect::<Result<Vec<_>, _>>()?,
1007            );
1008        }
1009        Ok(Self {
1010            architecture,
1011            graph,
1012            units,
1013            backend: std::marker::PhantomData,
1014        })
1015    }
1016
1017    /// Runs one complete prefill or decode pass without dynamic dispatch.
1018    pub fn forward<'a>(
1019        &mut self,
1020        input: A::Input<'a>,
1021        state: &mut S,
1022        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1023    ) -> Result<B::Tensor, A::Error> {
1024        self.forward_with_context(input, state, context)
1025            .map(|(output, _)| output)
1026    }
1027
1028    /// Runs one complete pass and returns its architecture-owned context.
1029    ///
1030    /// This is the resident counterpart of the bounded runtime's context
1031    /// result and lets callers retain target captures without storing
1032    /// request-local tensors on the model object.
1033    pub fn forward_with_context<'a>(
1034        &mut self,
1035        input: A::Input<'a>,
1036        state: &mut S,
1037        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1038    ) -> Result<(B::Tensor, A::ForwardContext), A::Error> {
1039        self.forward_with_traversal_hook(input, state, context, &mut NoopLayeredTraversalHook)
1040    }
1041
1042    /// Runs one resident pass through a statically dispatched traversal hook.
1043    pub fn forward_with_traversal_hook<'a, H>(
1044        &mut self,
1045        input: A::Input<'a>,
1046        state: &mut S,
1047        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1048        hook: &mut H,
1049    ) -> Result<(B::Tensor, A::ForwardContext), A::Error>
1050    where
1051        H: LayeredTraversalHook<B, A::ForwardContext, A::Error>,
1052    {
1053        let forward = self.architecture.begin_forward(input, state, context)?;
1054        let initial = forward.hidden;
1055        let mut forward_context = forward.context;
1056        let mut schedule = ExecutionGroupSchedule::new(&self.graph);
1057        let mut outputs: Vec<Option<B::Tensor>> = vec![None; self.graph.groups().len()];
1058        for &group in self.graph.execution_order() {
1059            let dependencies = schedule
1060                .dependencies(group)
1061                .expect("validated execution order contains a known group")
1062                .iter()
1063                .map(|&dependency| {
1064                    outputs[dependency]
1065                        .as_ref()
1066                        .expect("topological dependency has completed")
1067                        .clone()
1068                })
1069                .collect::<Vec<_>>();
1070            let dependency_refs = dependencies.iter().collect::<Vec<_>>();
1071            let mut hidden = self.architecture.begin_execution_group(
1072                group,
1073                &initial,
1074                &dependency_refs,
1075                state,
1076                &mut forward_context,
1077                context,
1078            )?;
1079            for dependency in schedule
1080                .started(group)
1081                .expect("topological execution starts only ready groups")
1082            {
1083                outputs[dependency] = None;
1084            }
1085            if self
1086                .architecture
1087                .should_execute_group(group, &forward_context)
1088            {
1089                let unit_count = self.units[group].len();
1090                for (index, unit) in self.units[group].iter_mut().enumerate() {
1091                    if hook.before_unit(
1092                        group,
1093                        index,
1094                        unit_count - index,
1095                        &hidden,
1096                        &mut forward_context,
1097                        context,
1098                    )? == LayeredUnitAction::SkipRemainingGroup
1099                    {
1100                        break;
1101                    }
1102                    hidden = self.architecture.forward_unit(
1103                        group,
1104                        index,
1105                        unit,
1106                        &hidden,
1107                        state,
1108                        &mut forward_context,
1109                        context,
1110                    )?;
1111                    hook.after_unit(group, index, &hidden, &mut forward_context, context)?;
1112                }
1113            }
1114            hidden = self.architecture.complete_execution_group(
1115                group,
1116                &hidden,
1117                state,
1118                &mut forward_context,
1119                context,
1120            )?;
1121            hook.after_group(group, &hidden, &mut forward_context, context)?;
1122            outputs[group] = Some(hidden);
1123            schedule
1124                .ordered(group)
1125                .expect("started group can be ordered exactly once");
1126        }
1127        let hidden = outputs[self.graph.output()]
1128            .take()
1129            .expect("validated graph output completed");
1130        let output = self
1131            .architecture
1132            .finish_forward(&hidden, state, &forward_context, context)?;
1133        Ok((output, forward_context))
1134    }
1135
1136    /// Borrows the architecture and its pinned parameter topology.
1137    pub const fn architecture(&self) -> &A {
1138        &self.architecture
1139    }
1140
1141    /// Mutably borrows the architecture.
1142    pub fn architecture_mut(&mut self) -> &mut A {
1143        &mut self.architecture
1144    }
1145
1146    /// Borrows resident execution units for loading or inspection.
1147    pub fn units(&self) -> &[Vec<A::Unit>] {
1148        &self.units
1149    }
1150
1151    /// Mutably borrows resident execution units for parameter binding.
1152    pub fn units_mut(&mut self) -> &mut [Vec<A::Unit>] {
1153        &mut self.units
1154    }
1155
1156    /// Decomposes the runtime without cloning backend-native values.
1157    pub fn into_parts(self) -> (A, Vec<A::Unit>) {
1158        (
1159            self.architecture,
1160            self.units.into_iter().flatten().collect(),
1161        )
1162    }
1163}
1164
1165/// Policy controlling acquisition and exact release of one execution unit.
1166pub trait LayerwisePolicy<B, U>
1167where
1168    B: NeuralBackend,
1169{
1170    /// Concrete lease owning one populated unit and all residency guards.
1171    type Lease: std::ops::DerefMut<Target = U>;
1172    /// Concrete acquisition or completion failure.
1173    type Error;
1174
1175    /// Starts one forward after architecture input preparation.
1176    fn begin(
1177        &mut self,
1178        initial: &B::Tensor,
1179        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1180    ) -> Result<(), Self::Error>;
1181
1182    /// Aborts an incomplete forward and releases all policy-owned state.
1183    ///
1184    /// `active` contains the unit lease when execution stopped after
1185    /// acquisition but before exact completion. Implementations with no
1186    /// forward-scoped state may rely on the default, which simply drops it.
1187    fn abort(
1188        &mut self,
1189        active: Option<(usize, crate::ExecutionUnitAddress, Self::Lease)>,
1190        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
1191    ) {
1192        drop(active);
1193    }
1194
1195    /// Acquires one populated unit for exclusive execution.
1196    ///
1197    /// The flat ordinal addresses storage while `address` preserves the
1198    /// architecture execution group and group-local unit index for scheduling.
1199    fn acquire<E, F>(
1200        &mut self,
1201        ordinal: usize,
1202        address: crate::ExecutionUnitAddress,
1203        build: F,
1204        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1205    ) -> Result<Self::Lease, LayerwiseAcquireError<E, Self::Error>>
1206    where
1207        F: FnOnce(&<B::Tensor as eredu_nn::Tensor>::Context) -> Result<U, E>;
1208
1209    /// Retains the unit and dependent native values through exact completion.
1210    fn complete<'a, StateValues, ContextValues>(
1211        &mut self,
1212        ordinal: usize,
1213        address: crate::ExecutionUnitAddress,
1214        lease: Self::Lease,
1215        output: &'a B::Tensor,
1216        state_values: StateValues,
1217        context_values: ContextValues,
1218        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1219    ) -> Result<(), Self::Error>
1220    where
1221        B::Tensor: 'a,
1222        StateValues: Iterator<Item = &'a B::Tensor>,
1223        ContextValues: Iterator<Item = &'a B::Tensor>;
1224
1225    /// Completes the final output and releases any remaining unit guards.
1226    fn finish(
1227        &mut self,
1228        output: &B::Tensor,
1229        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1230    ) -> Result<(), Self::Error>;
1231}
1232
1233/// Failure-safe ownership of one policy forward and its current unit lease.
1234struct LayerwisePolicyForward<'a, B, U, P>
1235where
1236    B: NeuralBackend,
1237    P: LayerwisePolicy<B, U>,
1238{
1239    policy: &'a mut P,
1240    context: &'a <B::Tensor as eredu_nn::Tensor>::Context,
1241    active: Option<(usize, crate::ExecutionUnitAddress, P::Lease)>,
1242    finished: bool,
1243    unit: std::marker::PhantomData<fn() -> U>,
1244}
1245
1246impl<'a, B, U, P> LayerwisePolicyForward<'a, B, U, P>
1247where
1248    B: NeuralBackend,
1249    P: LayerwisePolicy<B, U>,
1250{
1251    fn begin(
1252        policy: &'a mut P,
1253        initial: &B::Tensor,
1254        context: &'a <B::Tensor as eredu_nn::Tensor>::Context,
1255    ) -> Result<Self, P::Error> {
1256        if let Err(error) = policy.begin(initial, context) {
1257            policy.abort(None, context);
1258            return Err(error);
1259        }
1260        Ok(Self {
1261            policy,
1262            context,
1263            active: None,
1264            finished: false,
1265            unit: std::marker::PhantomData,
1266        })
1267    }
1268
1269    fn acquire<E, F>(
1270        &mut self,
1271        ordinal: usize,
1272        address: crate::ExecutionUnitAddress,
1273        build: F,
1274    ) -> Result<&mut P::Lease, LayerwiseAcquireError<E, P::Error>>
1275    where
1276        F: FnOnce(&<B::Tensor as eredu_nn::Tensor>::Context) -> Result<U, E>,
1277    {
1278        debug_assert!(self.active.is_none());
1279        let lease = self.policy.acquire(ordinal, address, build, self.context)?;
1280        self.active = Some((ordinal, address, lease));
1281        Ok(&mut self
1282            .active
1283            .as_mut()
1284            .expect("acquired policy lease is active")
1285            .2)
1286    }
1287
1288    fn complete<'value, StateValues, ContextValues>(
1289        &mut self,
1290        output: &'value B::Tensor,
1291        state_values: StateValues,
1292        context_values: ContextValues,
1293    ) -> Result<(), P::Error>
1294    where
1295        B::Tensor: 'value,
1296        StateValues: Iterator<Item = &'value B::Tensor>,
1297        ContextValues: Iterator<Item = &'value B::Tensor>,
1298    {
1299        let (ordinal, address, lease) = self
1300            .active
1301            .take()
1302            .expect("policy completion follows one acquisition");
1303        self.policy.complete(
1304            ordinal,
1305            address,
1306            lease,
1307            output,
1308            state_values,
1309            context_values,
1310            self.context,
1311        )
1312    }
1313
1314    fn finish(&mut self, output: &B::Tensor) -> Result<(), P::Error> {
1315        self.policy.finish(output, self.context)?;
1316        self.finished = true;
1317        Ok(())
1318    }
1319}
1320
1321impl<B, U, P> Drop for LayerwisePolicyForward<'_, B, U, P>
1322where
1323    B: NeuralBackend,
1324    P: LayerwisePolicy<B, U>,
1325{
1326    fn drop(&mut self) {
1327        if !self.finished {
1328            self.policy.abort(self.active.take(), self.context);
1329        }
1330    }
1331}
1332
1333/// Failure while a layerwise policy acquires or populates one architecture unit.
1334#[derive(Debug)]
1335pub enum LayerwiseAcquireError<A, P> {
1336    /// The neutral architecture could not construct its unloaded unit.
1337    Architecture(A),
1338    /// The execution policy could not acquire residency or populate the unit.
1339    Policy(P),
1340}
1341
1342/// Failure from architecture execution or layerwise residency policy.
1343#[derive(Debug, thiserror::Error)]
1344pub enum LayerwiseRuntimeError<A, P>
1345where
1346    A: std::fmt::Display,
1347    P: std::fmt::Display,
1348{
1349    /// Architecture construction or forward failure.
1350    #[error("layered architecture failed: {0}")]
1351    Architecture(A),
1352    /// Invalid access to architecture-declared mutable state.
1353    #[error(transparent)]
1354    State(#[from] crate::StateError),
1355    /// Architecture execution groups did not map to one stable residency-unit order.
1356    #[error(transparent)]
1357    Layout(#[from] crate::ExecutionUnitLayoutError),
1358    /// Unit acquisition or exact-completion failure.
1359    #[error("layerwise execution policy failed: {0}")]
1360    Policy(P),
1361    /// Backend-native graph submission or dependency ordering failed.
1362    #[error("layerwise backend submission failed: {0}")]
1363    Submission(String),
1364}
1365
1366/// Bounded-unit runtime invoking the same architecture lifecycle as resident execution.
1367pub struct LayerwiseRuntime<A, B, S, P>
1368where
1369    B: SubmissionBackend<Executor = <<B as NeuralBackend>::Tensor as eredu_nn::Tensor>::Context>,
1370    S: RuntimeState<B>,
1371    A: LayeredArchitecture<B, S>,
1372    P: LayerwisePolicy<B, A::Unit>,
1373{
1374    architecture: A,
1375    policy: P,
1376    executors: Option<Vec<B::OwnedExecutor>>,
1377    backend: std::marker::PhantomData<fn() -> (B, S)>,
1378}
1379
1380impl<A, B, S, P> LayerwiseRuntime<A, B, S, P>
1381where
1382    B: SubmissionBackend<Executor = <<B as NeuralBackend>::Tensor as eredu_nn::Tensor>::Context>,
1383    S: RuntimeState<B>,
1384    A: LayeredArchitecture<B, S>,
1385    P: LayerwisePolicy<B, A::Unit>,
1386    A::Error: std::fmt::Display,
1387    P::Error: std::fmt::Display,
1388{
1389    /// Creates a layerwise runtime from concrete architecture, state, and policy.
1390    pub const fn new(architecture: A, policy: P) -> Self {
1391        Self {
1392            architecture,
1393            policy,
1394            executors: None,
1395            backend: std::marker::PhantomData,
1396        }
1397    }
1398
1399    /// Creates a layerwise runtime while evaluating the policy before moving
1400    /// the architecture. This is useful when policy realization needs to
1401    /// borrow the architecture's canonical unit constructor first.
1402    pub const fn new_policy_first(policy: P, architecture: A) -> Self {
1403        Self::new(architecture, policy)
1404    }
1405
1406    /// Borrows the concrete architecture instance.
1407    pub const fn architecture(&self) -> &A {
1408        &self.architecture
1409    }
1410
1411    /// Mutably borrows the concrete architecture instance.
1412    pub fn architecture_mut(&mut self) -> &mut A {
1413        &mut self.architecture
1414    }
1415
1416    /// Borrows the concrete execution policy for cold-path diagnostics.
1417    pub const fn policy(&self) -> &P {
1418        &self.policy
1419    }
1420
1421    /// Mutably borrows the concrete execution policy.
1422    pub fn policy_mut(&mut self) -> &mut P {
1423        &mut self.policy
1424    }
1425
1426    /// Runs one complete prefill or decode pass with exact unit release points.
1427    pub fn forward<'a>(
1428        &mut self,
1429        input: A::Input<'a>,
1430        state: &mut S,
1431        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1432    ) -> Result<B::Tensor, LayerwiseRuntimeError<A::Error, P::Error>> {
1433        self.forward_with_context_hook(input, state, context, |_, _, _| Ok(()))
1434            .map(|(output, _)| output)
1435    }
1436
1437    /// Runs one pass and exposes mutable architecture context after each unit.
1438    pub fn forward_with_context_hook<'a, H>(
1439        &mut self,
1440        input: A::Input<'a>,
1441        state: &mut S,
1442        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1443        hook: H,
1444    ) -> Result<(B::Tensor, A::ForwardContext), LayerwiseRuntimeError<A::Error, P::Error>>
1445    where
1446        H: FnMut(usize, usize, &mut A::ForwardContext) -> Result<(), A::Error>,
1447    {
1448        self.forward_with_unit_executor_and_context_hook(
1449            input,
1450            state,
1451            context,
1452            |architecture, group, index, unit, hidden, state, forward, context| {
1453                architecture.forward_unit(group, index, unit, hidden, state, forward, context)
1454            },
1455            hook,
1456        )
1457    }
1458
1459    /// Runs one pass with a statically dispatched architecture-unit executor.
1460    ///
1461    /// Composition can use this cold API to inject routed expert execution or
1462    /// observation while the runtime retains graph traversal, residency, and
1463    /// exact completion ownership.
1464    pub fn forward_with_unit_executor<'a, E>(
1465        &mut self,
1466        input: A::Input<'a>,
1467        state: &mut S,
1468        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1469        execute: E,
1470    ) -> Result<B::Tensor, LayerwiseRuntimeError<A::Error, P::Error>>
1471    where
1472        E: FnMut(
1473            &mut A,
1474            usize,
1475            usize,
1476            &mut A::Unit,
1477            &B::Tensor,
1478            &mut S,
1479            &mut A::ForwardContext,
1480            &<B::Tensor as eredu_nn::Tensor>::Context,
1481        ) -> Result<B::Tensor, A::Error>,
1482    {
1483        self.forward_with_unit_executor_and_context_hook(
1484            input,
1485            state,
1486            context,
1487            execute,
1488            |_, _, _| Ok(()),
1489        )
1490        .map(|(output, _)| output)
1491    }
1492
1493    /// Runs the production sequential traversal with stable unit-boundary observation.
1494    pub fn forward_with_observer<'a, Observer>(
1495        &mut self,
1496        input: A::Input<'a>,
1497        state: &mut S,
1498        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1499        observer: &mut Observer,
1500    ) -> Result<B::Tensor, LayerwiseRuntimeError<A::Error, P::Error>>
1501    where
1502        Observer: ActivationObserver<B::Tensor, A::Error> + ?Sized,
1503    {
1504        self.forward_with_unit_executor_and_observer(
1505            input,
1506            state,
1507            context,
1508            |architecture, group, index, unit, hidden, state, forward, context| {
1509                architecture.forward_unit(group, index, unit, hidden, state, forward, context)
1510            },
1511            observer,
1512        )
1513    }
1514
1515    /// Runs a custom production unit executor with stable boundary observation.
1516    pub fn forward_with_unit_executor_and_observer<'a, E, Observer>(
1517        &mut self,
1518        input: A::Input<'a>,
1519        state: &mut S,
1520        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1521        mut execute: E,
1522        observer: &mut Observer,
1523    ) -> Result<B::Tensor, LayerwiseRuntimeError<A::Error, P::Error>>
1524    where
1525        E: FnMut(
1526            &mut A,
1527            usize,
1528            usize,
1529            &mut A::Unit,
1530            &B::Tensor,
1531            &mut S,
1532            &mut A::ForwardContext,
1533            &<B::Tensor as eredu_nn::Tensor>::Context,
1534        ) -> Result<B::Tensor, A::Error>,
1535        Observer: ActivationObserver<B::Tensor, A::Error> + ?Sized,
1536    {
1537        self.forward_with_unit_executor(
1538            input,
1539            state,
1540            context,
1541            |architecture, group, index, unit, hidden, state, forward, context| {
1542                let path = architecture.unit_path(group, index)?;
1543                let input = observe_and_intervene(observer, &format!("{path}.input"), hidden)?;
1544                let output = execute(
1545                    architecture,
1546                    group,
1547                    index,
1548                    unit,
1549                    &input,
1550                    state,
1551                    forward,
1552                    context,
1553                )?;
1554                observe_and_intervene(observer, &format!("{path}.output"), &output)
1555            },
1556        )
1557    }
1558
1559    /// Runs canonical provider-backed unit execution with unit-boundary and
1560    /// routed-expert observation.
1561    ///
1562    /// Observation wraps [`RoutedLayeredArchitecture::forward_unit_with_provider`]
1563    /// instead of replacing it. Architecture-owned validation, state lookup,
1564    /// shape handling, routing, and provider dispatch therefore remain shared
1565    /// with ordinary execution.
1566    #[allow(clippy::too_many_arguments)]
1567    pub fn forward_with_provider_and_observer<'a, Provider, Observer>(
1568        &mut self,
1569        input: A::Input<'a>,
1570        state: &mut S,
1571        pass: ExpertPass,
1572        provider: &mut Provider,
1573        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1574        observer: &mut Observer,
1575    ) -> Result<B::Tensor, LayerwiseRuntimeError<A::Error, P::Error>>
1576    where
1577        B: eredu_nn::GroupedNeuralBackend,
1578        A: RoutedLayeredArchitecture<B, S>,
1579        A::Error: std::fmt::Display,
1580        Provider: RoutedExpertProvider<B>,
1581        Provider::Error: std::fmt::Display,
1582        Observer: ActivationObserver<B::Tensor, A::Error> + ?Sized,
1583    {
1584        self.forward_with_unit_executor(
1585            input,
1586            state,
1587            context,
1588            |architecture, group, index, unit, hidden, state, forward, context| {
1589                let path = architecture.unit_path(group, index)?;
1590                let input = observe_and_intervene(observer, &format!("{path}.input"), hidden)?;
1591                let output = match architecture.routed_observation_point(group, index)? {
1592                    Some(point) => {
1593                        let mut observed = ObservedExpertProvider::new(provider, observer, point);
1594                        architecture.forward_unit_with_provider(
1595                            group,
1596                            index,
1597                            unit,
1598                            &input,
1599                            state,
1600                            forward,
1601                            pass,
1602                            &mut observed,
1603                            context,
1604                        )?
1605                    }
1606                    None => architecture.forward_unit_with_provider(
1607                        group, index, unit, &input, state, forward, pass, provider, context,
1608                    )?,
1609                };
1610                observe_and_intervene(observer, &format!("{path}.output"), &output)
1611            },
1612        )
1613    }
1614
1615    /// Runs one pass with both a custom unit executor and post-unit context hook.
1616    pub fn forward_with_unit_executor_and_context_hook<'a, E, H>(
1617        &mut self,
1618        input: A::Input<'a>,
1619        state: &mut S,
1620        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1621        execute: E,
1622        mut hook: H,
1623    ) -> Result<(B::Tensor, A::ForwardContext), LayerwiseRuntimeError<A::Error, P::Error>>
1624    where
1625        E: FnMut(
1626            &mut A,
1627            usize,
1628            usize,
1629            &mut A::Unit,
1630            &B::Tensor,
1631            &mut S,
1632            &mut A::ForwardContext,
1633            &<B::Tensor as eredu_nn::Tensor>::Context,
1634        ) -> Result<B::Tensor, A::Error>,
1635        H: FnMut(usize, usize, &mut A::ForwardContext) -> Result<(), A::Error>,
1636    {
1637        self.forward_with_unit_executor_and_activation_hook(
1638            input,
1639            state,
1640            context,
1641            execute,
1642            |group, index, _hidden, forward| hook(group, index, forward),
1643        )
1644    }
1645
1646    /// Runs one pass with a custom unit executor and exposes each post-unit
1647    /// activation together with the mutable architecture context.
1648    ///
1649    /// The activation is the ordinary output of the execution unit. Target
1650    /// state taps and inspection therefore observe the production forward
1651    /// without requiring a second family-specific model path.
1652    pub fn forward_with_unit_executor_and_activation_hook<'a, E, H>(
1653        &mut self,
1654        input: A::Input<'a>,
1655        state: &mut S,
1656        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1657        execute: E,
1658        hook: H,
1659    ) -> Result<(B::Tensor, A::ForwardContext), LayerwiseRuntimeError<A::Error, P::Error>>
1660    where
1661        E: FnMut(
1662            &mut A,
1663            usize,
1664            usize,
1665            &mut A::Unit,
1666            &B::Tensor,
1667            &mut S,
1668            &mut A::ForwardContext,
1669            &<B::Tensor as eredu_nn::Tensor>::Context,
1670        ) -> Result<B::Tensor, A::Error>,
1671        H: FnMut(usize, usize, &B::Tensor, &mut A::ForwardContext) -> Result<(), A::Error>,
1672    {
1673        self.forward_with_unit_executor_and_traversal_hook(
1674            input,
1675            state,
1676            context,
1677            execute,
1678            &mut AfterUnitTraversalHook { after_unit: hook },
1679        )
1680    }
1681
1682    /// Runs one bounded pass through a statically dispatched traversal hook.
1683    pub fn forward_with_traversal_hook<'a, H>(
1684        &mut self,
1685        input: A::Input<'a>,
1686        state: &mut S,
1687        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1688        hook: &mut H,
1689    ) -> Result<(B::Tensor, A::ForwardContext), LayerwiseRuntimeError<A::Error, P::Error>>
1690    where
1691        H: LayeredTraversalHook<B, A::ForwardContext, A::Error>,
1692    {
1693        self.forward_with_unit_executor_and_traversal_hook(
1694            input,
1695            state,
1696            context,
1697            |architecture, group, index, unit, hidden, state, forward, context| {
1698                architecture.forward_unit(group, index, unit, hidden, state, forward, context)
1699            },
1700            hook,
1701        )
1702    }
1703
1704    /// Runs one bounded pass with custom unit execution and a shared traversal hook.
1705    pub fn forward_with_unit_executor_and_traversal_hook<'a, E, H>(
1706        &mut self,
1707        input: A::Input<'a>,
1708        state: &mut S,
1709        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1710        mut execute: E,
1711        hook: &mut H,
1712    ) -> Result<(B::Tensor, A::ForwardContext), LayerwiseRuntimeError<A::Error, P::Error>>
1713    where
1714        E: FnMut(
1715            &mut A,
1716            usize,
1717            usize,
1718            &mut A::Unit,
1719            &B::Tensor,
1720            &mut S,
1721            &mut A::ForwardContext,
1722            &<B::Tensor as eredu_nn::Tensor>::Context,
1723        ) -> Result<B::Tensor, A::Error>,
1724        H: LayeredTraversalHook<B, A::ForwardContext, A::Error>,
1725    {
1726        let graph = self
1727            .architecture
1728            .execution_graph()
1729            .map_err(LayerwiseRuntimeError::Architecture)?;
1730        let counts = (0..graph.groups().len())
1731            .map(|group| {
1732                self.architecture
1733                    .group_unit_count(group)
1734                    .map_err(LayerwiseRuntimeError::Architecture)
1735            })
1736            .collect::<Result<Vec<_>, _>>()?;
1737        let layout = ExecutionUnitLayout::new(&graph, counts)?;
1738        if self.executors.as_ref().map(Vec::len) != Some(graph.groups().len()) {
1739            self.executors = Some(
1740                B::fork_executors(context, graph.groups().len())
1741                    .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?,
1742            );
1743        }
1744        let executors = self
1745            .executors
1746            .as_ref()
1747            .expect("layered runtime initialized its executor cache");
1748        let forward = self
1749            .architecture
1750            .begin_forward(input, state, context)
1751            .map_err(LayerwiseRuntimeError::Architecture)?;
1752        let initial_completion = (graph.groups().len() > 1)
1753            .then(|| B::submit(context, [&forward.hidden]))
1754            .transpose()
1755            .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?;
1756        let mut policy = LayerwisePolicyForward::begin(&mut self.policy, &forward.hidden, context)
1757            .map_err(LayerwiseRuntimeError::Policy)?;
1758        let initial = forward.hidden;
1759        let mut forward_context = forward.context;
1760        let mut schedule = ExecutionGroupSchedule::new(&graph);
1761        let mut outputs: Vec<Option<B::Tensor>> = vec![None; graph.groups().len()];
1762        let mut completions: Vec<Option<B::Completion>> =
1763            (0..graph.groups().len()).map(|_| None).collect();
1764        for &group in graph.execution_order() {
1765            let executor = std::borrow::Borrow::borrow(&executors[group]);
1766            let group_dependencies = schedule
1767                .dependencies(group)
1768                .expect("validated execution order contains a known group");
1769            if group_dependencies.is_empty() {
1770                if let Some(completion) = &initial_completion {
1771                    B::order_after(completion, executor)
1772                        .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?;
1773                }
1774            }
1775            for &dependency in group_dependencies {
1776                B::order_after(
1777                    completions[dependency]
1778                        .as_ref()
1779                        .expect("topological dependency has a completion"),
1780                    executor,
1781                )
1782                .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?;
1783            }
1784            let dependencies = schedule
1785                .dependencies(group)
1786                .expect("validated execution order contains a known group")
1787                .iter()
1788                .map(|&dependency| {
1789                    outputs[dependency]
1790                        .as_ref()
1791                        .expect("topological dependency has completed")
1792                        .clone()
1793                })
1794                .collect::<Vec<_>>();
1795            let dependency_refs = dependencies.iter().collect::<Vec<_>>();
1796            let mut hidden = self
1797                .architecture
1798                .begin_execution_group(
1799                    group,
1800                    &initial,
1801                    &dependency_refs,
1802                    state,
1803                    &mut forward_context,
1804                    executor,
1805                )
1806                .map_err(LayerwiseRuntimeError::Architecture)?;
1807            for dependency in schedule
1808                .started(group)
1809                .expect("topological execution starts only ready groups")
1810            {
1811                outputs[dependency] = None;
1812            }
1813            if self
1814                .architecture
1815                .should_execute_group(group, &forward_context)
1816            {
1817                let unit_count = layout
1818                    .group_range(group)
1819                    .expect("layout covers every graph group")
1820                    .len();
1821                for index in 0..unit_count {
1822                    if hook
1823                        .before_unit(
1824                            group,
1825                            index,
1826                            unit_count - index,
1827                            &hidden,
1828                            &mut forward_context,
1829                            executor,
1830                        )
1831                        .map_err(LayerwiseRuntimeError::Architecture)?
1832                        == LayeredUnitAction::SkipRemainingGroup
1833                    {
1834                        break;
1835                    }
1836                    let ordinal = layout
1837                        .ordinal(group, index)
1838                        .expect("group-local unit belongs to the layout");
1839                    let address = layout
1840                        .address(ordinal)
1841                        .expect("group-local unit has a stable policy address");
1842                    let lease = policy
1843                        .acquire(ordinal, address, |executor| {
1844                            self.architecture.build_unit(group, index, executor)
1845                        })
1846                        .map_err(|error| match error {
1847                            LayerwiseAcquireError::Architecture(error) => {
1848                                LayerwiseRuntimeError::Architecture(error)
1849                            }
1850                            LayerwiseAcquireError::Policy(error) => {
1851                                LayerwiseRuntimeError::Policy(error)
1852                            }
1853                        })?;
1854                    hidden = execute(
1855                        &mut self.architecture,
1856                        group,
1857                        index,
1858                        lease,
1859                        &hidden,
1860                        state,
1861                        &mut forward_context,
1862                        executor,
1863                    )
1864                    .map_err(LayerwiseRuntimeError::Architecture)?;
1865                    hook.after_unit(group, index, &hidden, &mut forward_context, executor)
1866                        .map_err(LayerwiseRuntimeError::Architecture)?;
1867                    let mut state_values = Vec::new();
1868                    for state_ordinal in self
1869                        .architecture
1870                        .retained_state_ordinals(group, index, ordinal)
1871                    {
1872                        state_values.extend(
1873                            state
1874                                .retained_values(state_ordinal, address.with_index(state_ordinal))
1875                                .map_err(LayerwiseRuntimeError::State)?,
1876                        );
1877                    }
1878                    let context_values =
1879                        self.architecture
1880                            .retained_context_values(&forward_context, group, index);
1881                    policy
1882                        .complete(&hidden, state_values.into_iter(), context_values)
1883                        .map_err(LayerwiseRuntimeError::Policy)?;
1884                }
1885            }
1886            hidden = self
1887                .architecture
1888                .complete_execution_group(group, &hidden, state, &mut forward_context, executor)
1889                .map_err(LayerwiseRuntimeError::Architecture)?;
1890            hook.after_group(group, &hidden, &mut forward_context, executor)
1891                .map_err(LayerwiseRuntimeError::Architecture)?;
1892            outputs[group] = Some(hidden);
1893            if graph.groups().len() > 1 {
1894                completions[group] = Some(
1895                    B::submit(
1896                        executor,
1897                        [outputs[group]
1898                            .as_ref()
1899                            .expect("group output was stored before submission")],
1900                    )
1901                    .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?,
1902                );
1903            }
1904            schedule
1905                .ordered(group)
1906                .expect("started group can be ordered exactly once");
1907        }
1908        let hidden = outputs[graph.output()]
1909            .take()
1910            .expect("validated graph output completed");
1911        if let Some(completion) = &completions[graph.output()] {
1912            B::order_after(completion, context)
1913                .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?;
1914        }
1915        let output = self
1916            .architecture
1917            .finish_forward(&hidden, state, &forward_context, context)
1918            .map_err(LayerwiseRuntimeError::Architecture)?;
1919        policy
1920            .finish(&output)
1921            .map_err(LayerwiseRuntimeError::Policy)?;
1922        Ok((output, forward_context))
1923    }
1924
1925    /// Runs one complete rank-local pass through the neutral parallel lifecycle.
1926    pub fn forward_parallel<'a>(
1927        &mut self,
1928        input: A::Input<'a>,
1929        state: &mut S,
1930        parallel: &B::ParallelContext,
1931        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1932    ) -> Result<B::Tensor, LayerwiseRuntimeError<A::Error, P::Error>>
1933    where
1934        A: ParallelLayeredArchitecture<B, S>,
1935    {
1936        self.forward_parallel_with_context_hook(input, state, parallel, context, |_, _, _| Ok(()))
1937            .map(|(output, _)| output)
1938    }
1939
1940    /// Runs one rank-local pass and exposes mutable context after each unit.
1941    pub fn forward_parallel_with_context_hook<'a, H>(
1942        &mut self,
1943        input: A::Input<'a>,
1944        state: &mut S,
1945        parallel: &B::ParallelContext,
1946        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1947        hook: H,
1948    ) -> Result<(B::Tensor, A::ForwardContext), LayerwiseRuntimeError<A::Error, P::Error>>
1949    where
1950        A: ParallelLayeredArchitecture<B, S>,
1951        H: FnMut(usize, usize, &mut A::ForwardContext) -> Result<(), A::Error>,
1952    {
1953        self.forward_parallel_with_unit_executor_and_traversal_hook(
1954            input,
1955            state,
1956            parallel,
1957            context,
1958            |architecture, group, index, unit, hidden, state, forward, parallel, context| {
1959                architecture.forward_unit_parallel(
1960                    group, index, unit, hidden, state, forward, parallel, context,
1961                )
1962            },
1963            &mut AfterUnitContextTraversalHook { after_unit: hook },
1964        )
1965    }
1966
1967    /// Runs one parallel pass with a custom statically dispatched unit executor.
1968    pub fn forward_parallel_with_unit_executor<'a, E>(
1969        &mut self,
1970        input: A::Input<'a>,
1971        state: &mut S,
1972        parallel: &B::ParallelContext,
1973        context: &<B::Tensor as eredu_nn::Tensor>::Context,
1974        execute: E,
1975    ) -> Result<B::Tensor, LayerwiseRuntimeError<A::Error, P::Error>>
1976    where
1977        A: ParallelLayeredArchitecture<B, S>,
1978        E: FnMut(
1979            &mut A,
1980            usize,
1981            usize,
1982            &mut A::Unit,
1983            &B::Tensor,
1984            &mut S,
1985            &mut A::ForwardContext,
1986            &B::ParallelContext,
1987            &<B::Tensor as eredu_nn::Tensor>::Context,
1988        ) -> Result<B::Tensor, A::Error>,
1989    {
1990        self.forward_parallel_with_unit_executor_and_context_hook(
1991            input,
1992            state,
1993            parallel,
1994            context,
1995            execute,
1996            |_, _, _| Ok(()),
1997        )
1998        .map(|(output, _)| output)
1999    }
2000
2001    /// Runs the production parallel traversal with stable unit-boundary observation.
2002    pub fn forward_parallel_with_observer<'a, Observer>(
2003        &mut self,
2004        input: A::Input<'a>,
2005        state: &mut S,
2006        parallel: &B::ParallelContext,
2007        context: &<B::Tensor as eredu_nn::Tensor>::Context,
2008        observer: &mut Observer,
2009    ) -> Result<B::Tensor, LayerwiseRuntimeError<A::Error, P::Error>>
2010    where
2011        A: ParallelLayeredArchitecture<B, S>,
2012        Observer: ActivationObserver<B::Tensor, A::Error> + ?Sized,
2013    {
2014        self.forward_parallel_with_unit_executor_and_observer(
2015            input,
2016            state,
2017            parallel,
2018            context,
2019            |architecture, group, index, unit, hidden, state, forward, parallel, context| {
2020                architecture.forward_unit_parallel(
2021                    group, index, unit, hidden, state, forward, parallel, context,
2022                )
2023            },
2024            observer,
2025        )
2026    }
2027
2028    /// Runs a custom parallel unit executor with stable boundary observation.
2029    pub fn forward_parallel_with_unit_executor_and_observer<'a, E, Observer>(
2030        &mut self,
2031        input: A::Input<'a>,
2032        state: &mut S,
2033        parallel: &B::ParallelContext,
2034        context: &<B::Tensor as eredu_nn::Tensor>::Context,
2035        mut execute: E,
2036        observer: &mut Observer,
2037    ) -> Result<B::Tensor, LayerwiseRuntimeError<A::Error, P::Error>>
2038    where
2039        A: ParallelLayeredArchitecture<B, S>,
2040        E: FnMut(
2041            &mut A,
2042            usize,
2043            usize,
2044            &mut A::Unit,
2045            &B::Tensor,
2046            &mut S,
2047            &mut A::ForwardContext,
2048            &B::ParallelContext,
2049            &<B::Tensor as eredu_nn::Tensor>::Context,
2050        ) -> Result<B::Tensor, A::Error>,
2051        Observer: ActivationObserver<B::Tensor, A::Error> + ?Sized,
2052    {
2053        self.forward_parallel_with_unit_executor(
2054            input,
2055            state,
2056            parallel,
2057            context,
2058            |architecture, group, index, unit, hidden, state, forward, parallel, context| {
2059                let path = architecture.unit_path(group, index)?;
2060                let input = observe_and_intervene(observer, &format!("{path}.input"), hidden)?;
2061                let output = execute(
2062                    architecture,
2063                    group,
2064                    index,
2065                    unit,
2066                    &input,
2067                    state,
2068                    forward,
2069                    parallel,
2070                    context,
2071                )?;
2072                observe_and_intervene(observer, &format!("{path}.output"), &output)
2073            },
2074        )
2075    }
2076
2077    /// Runs provider-backed parallel execution with boundary and routing observation.
2078    #[allow(clippy::too_many_arguments)]
2079    pub fn forward_parallel_with_provider_and_observer<'a, Provider, Observer>(
2080        &mut self,
2081        input: A::Input<'a>,
2082        state: &mut S,
2083        pass: ExpertPass,
2084        provider: &mut Provider,
2085        parallel: &B::ParallelContext,
2086        context: &<B::Tensor as eredu_nn::Tensor>::Context,
2087        observer: &mut Observer,
2088    ) -> Result<B::Tensor, LayerwiseRuntimeError<A::Error, P::Error>>
2089    where
2090        B: eredu_nn::GroupedNeuralBackend,
2091        A: ParallelRoutedLayeredArchitecture<B, S>,
2092        Provider: crate::TensorParallelRoutedExpertProvider<B>,
2093        Provider::Error: std::fmt::Display,
2094        Observer: ActivationObserver<B::Tensor, A::Error> + ?Sized,
2095    {
2096        self.forward_parallel_with_unit_executor(
2097            input,
2098            state,
2099            parallel,
2100            context,
2101            |architecture, group, index, unit, hidden, state, forward, parallel, context| {
2102                let path = architecture.unit_path(group, index)?;
2103                let input = observe_and_intervene(observer, &format!("{path}.input"), hidden)?;
2104                let output = match architecture.routed_observation_point(group, index)? {
2105                    Some(point) => {
2106                        let mut observed = ObservedExpertProvider::new(provider, observer, point);
2107                        architecture.forward_unit_parallel_with_provider(
2108                            group,
2109                            index,
2110                            unit,
2111                            &input,
2112                            state,
2113                            forward,
2114                            pass,
2115                            &mut observed,
2116                            parallel,
2117                            context,
2118                        )
2119                    }
2120                    None => architecture.forward_unit_parallel_with_provider(
2121                        group, index, unit, &input, state, forward, pass, provider, parallel,
2122                        context,
2123                    ),
2124                }?;
2125                observe_and_intervene(observer, &format!("{path}.output"), &output)
2126            },
2127        )
2128    }
2129
2130    /// Runs one parallel pass with custom unit execution and a post-unit hook.
2131    pub fn forward_parallel_with_unit_executor_and_context_hook<'a, E, H>(
2132        &mut self,
2133        input: A::Input<'a>,
2134        state: &mut S,
2135        parallel: &B::ParallelContext,
2136        context: &<B::Tensor as eredu_nn::Tensor>::Context,
2137        execute: E,
2138        hook: H,
2139    ) -> Result<(B::Tensor, A::ForwardContext), LayerwiseRuntimeError<A::Error, P::Error>>
2140    where
2141        A: ParallelLayeredArchitecture<B, S>,
2142        E: FnMut(
2143            &mut A,
2144            usize,
2145            usize,
2146            &mut A::Unit,
2147            &B::Tensor,
2148            &mut S,
2149            &mut A::ForwardContext,
2150            &B::ParallelContext,
2151            &<B::Tensor as eredu_nn::Tensor>::Context,
2152        ) -> Result<B::Tensor, A::Error>,
2153        H: FnMut(usize, usize, &mut A::ForwardContext) -> Result<(), A::Error>,
2154    {
2155        self.forward_parallel_with_unit_executor_and_traversal_hook(
2156            input,
2157            state,
2158            parallel,
2159            context,
2160            execute,
2161            &mut AfterUnitContextTraversalHook { after_unit: hook },
2162        )
2163    }
2164
2165    /// Runs one parallel pass through a statically dispatched traversal hook.
2166    pub fn forward_parallel_with_traversal_hook<'a, H>(
2167        &mut self,
2168        input: A::Input<'a>,
2169        state: &mut S,
2170        parallel: &B::ParallelContext,
2171        context: &<B::Tensor as eredu_nn::Tensor>::Context,
2172        hook: &mut H,
2173    ) -> Result<(B::Tensor, A::ForwardContext), LayerwiseRuntimeError<A::Error, P::Error>>
2174    where
2175        A: ParallelLayeredArchitecture<B, S>,
2176        H: LayeredTraversalHook<B, A::ForwardContext, A::Error>,
2177    {
2178        self.forward_parallel_with_unit_executor_and_traversal_hook(
2179            input,
2180            state,
2181            parallel,
2182            context,
2183            |architecture, group, index, unit, hidden, state, forward, parallel, context| {
2184                architecture.forward_unit_parallel(
2185                    group, index, unit, hidden, state, forward, parallel, context,
2186                )
2187            },
2188            hook,
2189        )
2190    }
2191
2192    /// Runs one parallel pass with custom unit execution and a shared traversal hook.
2193    pub fn forward_parallel_with_unit_executor_and_traversal_hook<'a, E, H>(
2194        &mut self,
2195        input: A::Input<'a>,
2196        state: &mut S,
2197        parallel: &B::ParallelContext,
2198        context: &<B::Tensor as eredu_nn::Tensor>::Context,
2199        mut execute: E,
2200        hook: &mut H,
2201    ) -> Result<(B::Tensor, A::ForwardContext), LayerwiseRuntimeError<A::Error, P::Error>>
2202    where
2203        A: ParallelLayeredArchitecture<B, S>,
2204        E: FnMut(
2205            &mut A,
2206            usize,
2207            usize,
2208            &mut A::Unit,
2209            &B::Tensor,
2210            &mut S,
2211            &mut A::ForwardContext,
2212            &B::ParallelContext,
2213            &<B::Tensor as eredu_nn::Tensor>::Context,
2214        ) -> Result<B::Tensor, A::Error>,
2215        H: LayeredTraversalHook<B, A::ForwardContext, A::Error>,
2216    {
2217        let graph = self
2218            .architecture
2219            .execution_graph()
2220            .map_err(LayerwiseRuntimeError::Architecture)?;
2221        let counts = (0..graph.groups().len())
2222            .map(|group| {
2223                self.architecture
2224                    .group_unit_count(group)
2225                    .map_err(LayerwiseRuntimeError::Architecture)
2226            })
2227            .collect::<Result<Vec<_>, _>>()?;
2228        let layout = ExecutionUnitLayout::new(&graph, counts)?;
2229        if self.executors.as_ref().map(Vec::len) != Some(graph.groups().len()) {
2230            self.executors = Some(
2231                B::fork_executors(context, graph.groups().len())
2232                    .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?,
2233            );
2234        }
2235        let executors = self
2236            .executors
2237            .as_ref()
2238            .expect("layered runtime initialized its executor cache");
2239        let forward = self
2240            .architecture
2241            .begin_forward_parallel(input, state, parallel, context)
2242            .map_err(LayerwiseRuntimeError::Architecture)?;
2243        let initial_completion = (graph.groups().len() > 1)
2244            .then(|| B::submit(context, [&forward.hidden]))
2245            .transpose()
2246            .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?;
2247        let mut policy = LayerwisePolicyForward::begin(&mut self.policy, &forward.hidden, context)
2248            .map_err(LayerwiseRuntimeError::Policy)?;
2249        let initial = forward.hidden;
2250        let mut forward_context = forward.context;
2251        let mut schedule = ExecutionGroupSchedule::new(&graph);
2252        let mut outputs: Vec<Option<B::Tensor>> = vec![None; graph.groups().len()];
2253        let mut completions: Vec<Option<B::Completion>> =
2254            (0..graph.groups().len()).map(|_| None).collect();
2255        for &group in graph.execution_order() {
2256            let executor = std::borrow::Borrow::borrow(&executors[group]);
2257            let group_dependencies = schedule
2258                .dependencies(group)
2259                .expect("validated execution order contains a known group");
2260            if group_dependencies.is_empty() {
2261                if let Some(completion) = &initial_completion {
2262                    B::order_after(completion, executor)
2263                        .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?;
2264                }
2265            }
2266            for &dependency in group_dependencies {
2267                B::order_after(
2268                    completions[dependency]
2269                        .as_ref()
2270                        .expect("topological dependency has a completion"),
2271                    executor,
2272                )
2273                .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?;
2274            }
2275            let dependencies = schedule
2276                .dependencies(group)
2277                .expect("validated execution order contains a known group")
2278                .iter()
2279                .map(|&dependency| {
2280                    outputs[dependency]
2281                        .as_ref()
2282                        .expect("topological dependency has completed")
2283                        .clone()
2284                })
2285                .collect::<Vec<_>>();
2286            let dependency_refs = dependencies.iter().collect::<Vec<_>>();
2287            let mut hidden = self
2288                .architecture
2289                .begin_execution_group_parallel(
2290                    group,
2291                    &initial,
2292                    &dependency_refs,
2293                    state,
2294                    &mut forward_context,
2295                    parallel,
2296                    executor,
2297                )
2298                .map_err(LayerwiseRuntimeError::Architecture)?;
2299            for dependency in schedule
2300                .started(group)
2301                .expect("topological execution starts only ready groups")
2302            {
2303                outputs[dependency] = None;
2304            }
2305            if self
2306                .architecture
2307                .should_execute_group(group, &forward_context)
2308            {
2309                let unit_count = layout
2310                    .group_range(group)
2311                    .expect("layout covers every graph group")
2312                    .len();
2313                for index in 0..unit_count {
2314                    if hook
2315                        .before_unit(
2316                            group,
2317                            index,
2318                            unit_count - index,
2319                            &hidden,
2320                            &mut forward_context,
2321                            executor,
2322                        )
2323                        .map_err(LayerwiseRuntimeError::Architecture)?
2324                        == LayeredUnitAction::SkipRemainingGroup
2325                    {
2326                        break;
2327                    }
2328                    let ordinal = layout
2329                        .ordinal(group, index)
2330                        .expect("group-local unit belongs to the layout");
2331                    let address = layout
2332                        .address(ordinal)
2333                        .expect("group-local unit has a stable policy address");
2334                    let lease = policy
2335                        .acquire(ordinal, address, |executor| {
2336                            self.architecture.build_unit(group, index, executor)
2337                        })
2338                        .map_err(|error| match error {
2339                            LayerwiseAcquireError::Architecture(error) => {
2340                                LayerwiseRuntimeError::Architecture(error)
2341                            }
2342                            LayerwiseAcquireError::Policy(error) => {
2343                                LayerwiseRuntimeError::Policy(error)
2344                            }
2345                        })?;
2346                    hidden = execute(
2347                        &mut self.architecture,
2348                        group,
2349                        index,
2350                        lease,
2351                        &hidden,
2352                        state,
2353                        &mut forward_context,
2354                        parallel,
2355                        executor,
2356                    )
2357                    .map_err(LayerwiseRuntimeError::Architecture)?;
2358                    hook.after_unit(group, index, &hidden, &mut forward_context, executor)
2359                        .map_err(LayerwiseRuntimeError::Architecture)?;
2360                    let mut state_values = Vec::new();
2361                    for state_ordinal in self
2362                        .architecture
2363                        .retained_state_ordinals(group, index, ordinal)
2364                    {
2365                        state_values.extend(
2366                            state
2367                                .retained_values(state_ordinal, address.with_index(state_ordinal))
2368                                .map_err(LayerwiseRuntimeError::State)?,
2369                        );
2370                    }
2371                    let context_values =
2372                        self.architecture
2373                            .retained_context_values(&forward_context, group, index);
2374                    policy
2375                        .complete(&hidden, state_values.into_iter(), context_values)
2376                        .map_err(LayerwiseRuntimeError::Policy)?;
2377                }
2378            }
2379            hidden = self
2380                .architecture
2381                .complete_execution_group_parallel(
2382                    group,
2383                    &hidden,
2384                    state,
2385                    &mut forward_context,
2386                    parallel,
2387                    executor,
2388                )
2389                .map_err(LayerwiseRuntimeError::Architecture)?;
2390            hook.after_group(group, &hidden, &mut forward_context, executor)
2391                .map_err(LayerwiseRuntimeError::Architecture)?;
2392            outputs[group] = Some(hidden);
2393            if graph.groups().len() > 1 {
2394                completions[group] = Some(
2395                    B::submit(
2396                        executor,
2397                        [outputs[group]
2398                            .as_ref()
2399                            .expect("group output was stored before submission")],
2400                    )
2401                    .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?,
2402                );
2403            }
2404            schedule
2405                .ordered(group)
2406                .expect("started group can be ordered exactly once");
2407        }
2408        let hidden = outputs[graph.output()]
2409            .take()
2410            .expect("validated graph output completed");
2411        if let Some(completion) = &completions[graph.output()] {
2412            B::order_after(completion, context)
2413                .map_err(|error| LayerwiseRuntimeError::Submission(error.to_string()))?;
2414        }
2415        let output = self
2416            .architecture
2417            .finish_forward_parallel(&hidden, state, &forward_context, parallel, context)
2418            .map_err(LayerwiseRuntimeError::Architecture)?;
2419        policy
2420            .finish(&output)
2421            .map_err(LayerwiseRuntimeError::Policy)?;
2422        Ok((output, forward_context))
2423    }
2424}
2425
2426/// Indexed owned unit used by [`ResidentUnitWindow`].
2427pub struct ResidentUnitLease<U> {
2428    index: usize,
2429    unit: U,
2430}
2431
2432impl<U> std::ops::Deref for ResidentUnitLease<U> {
2433    type Target = U;
2434
2435    fn deref(&self) -> &Self::Target {
2436        &self.unit
2437    }
2438}
2439
2440impl<U> std::ops::DerefMut for ResidentUnitLease<U> {
2441    fn deref_mut(&mut self) -> &mut Self::Target {
2442        &mut self.unit
2443    }
2444}
2445
2446/// Minimal one-at-a-time unit window useful for conformance and resident storage.
2447pub struct ResidentUnitWindow<U> {
2448    units: Vec<Option<U>>,
2449}
2450
2451impl<U> ResidentUnitWindow<U> {
2452    /// Creates a window over an ordered set of already populated units.
2453    pub fn new(units: Vec<U>) -> Self {
2454        Self {
2455            units: units.into_iter().map(Some).collect(),
2456        }
2457    }
2458}
2459
2460impl<B, U> LayerwisePolicy<B, U> for ResidentUnitWindow<U>
2461where
2462    B: NeuralBackend,
2463{
2464    type Lease = ResidentUnitLease<U>;
2465    type Error = ResidentUnitWindowError;
2466
2467    fn begin(
2468        &mut self,
2469        _initial: &B::Tensor,
2470        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
2471    ) -> Result<(), Self::Error> {
2472        Ok(())
2473    }
2474
2475    fn abort(
2476        &mut self,
2477        active: Option<(usize, crate::ExecutionUnitAddress, Self::Lease)>,
2478        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
2479    ) {
2480        let Some((ordinal, _, lease)) = active else {
2481            return;
2482        };
2483        debug_assert_eq!(lease.index, ordinal);
2484        if let Some(slot) = self.units.get_mut(lease.index) {
2485            debug_assert!(slot.is_none());
2486            if slot.is_none() {
2487                *slot = Some(lease.unit);
2488            }
2489        }
2490    }
2491
2492    fn acquire<E, F>(
2493        &mut self,
2494        index: usize,
2495        _address: crate::ExecutionUnitAddress,
2496        _build: F,
2497        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
2498    ) -> Result<Self::Lease, LayerwiseAcquireError<E, Self::Error>>
2499    where
2500        F: FnOnce(&<B::Tensor as eredu_nn::Tensor>::Context) -> Result<U, E>,
2501    {
2502        let count = self.units.len();
2503        let unit = self
2504            .units
2505            .get_mut(index)
2506            .ok_or(ResidentUnitWindowError::UnknownUnit { index, count })
2507            .map_err(LayerwiseAcquireError::Policy)?
2508            .take()
2509            .ok_or(ResidentUnitWindowError::AlreadyAcquired { index })
2510            .map_err(LayerwiseAcquireError::Policy)?;
2511        Ok(ResidentUnitLease { index, unit })
2512    }
2513
2514    fn complete<'a, StateValues, ContextValues>(
2515        &mut self,
2516        index: usize,
2517        _address: crate::ExecutionUnitAddress,
2518        lease: Self::Lease,
2519        _output: &'a B::Tensor,
2520        _state_values: StateValues,
2521        _context_values: ContextValues,
2522        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
2523    ) -> Result<(), Self::Error>
2524    where
2525        B::Tensor: 'a,
2526        StateValues: Iterator<Item = &'a B::Tensor>,
2527        ContextValues: Iterator<Item = &'a B::Tensor>,
2528    {
2529        if lease.index != index {
2530            return Err(ResidentUnitWindowError::MismatchedUnit {
2531                expected: index,
2532                actual: lease.index,
2533            });
2534        }
2535        let slot = self
2536            .units
2537            .get_mut(index)
2538            .expect("acquired unit index remains in the window");
2539        if slot.replace(lease.unit).is_some() {
2540            return Err(ResidentUnitWindowError::AlreadyResident { index });
2541        }
2542        Ok(())
2543    }
2544
2545    fn finish(
2546        &mut self,
2547        _output: &B::Tensor,
2548        _context: &<B::Tensor as eredu_nn::Tensor>::Context,
2549    ) -> Result<(), Self::Error> {
2550        Ok(())
2551    }
2552}
2553
2554/// Invalid access to an owned resident-unit window.
2555#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
2556pub enum ResidentUnitWindowError {
2557    /// The requested unit is outside the ordered window.
2558    #[error("unit {index} is outside the {count}-unit window")]
2559    UnknownUnit {
2560        /// Requested index.
2561        index: usize,
2562        /// Window size.
2563        count: usize,
2564    },
2565    /// A unit was acquired twice without an intervening completion.
2566    #[error("unit {index} is already acquired")]
2567    AlreadyAcquired {
2568        /// Requested index.
2569        index: usize,
2570    },
2571    /// Completion returned a lease for the wrong unit.
2572    #[error("unit completion expected {expected}, received {actual}")]
2573    MismatchedUnit {
2574        /// Expected unit.
2575        expected: usize,
2576        /// Lease unit.
2577        actual: usize,
2578    },
2579    /// A completion attempted to overwrite a resident unit.
2580    #[error("unit {index} is already resident")]
2581    AlreadyResident {
2582        /// Conflicting index.
2583        index: usize,
2584    },
2585}
2586
2587#[cfg(test)]
2588mod tests {
2589    use super::{ArchitectureGroupKind, LayeredPipelineSchedule, LayeredPipelineScheduleError};
2590    use crate::{ExecutionGraph, ExecutionGroupSpec, ExecutionScheduleError};
2591
2592    fn pipeline_graph() -> ExecutionGraph {
2593        ExecutionGraph::new(
2594            vec![
2595                ExecutionGroupSpec::root("vision"),
2596                ExecutionGroupSpec::root("audio"),
2597                ExecutionGroupSpec::with_dependencies("projector", ["vision"]),
2598                ExecutionGroupSpec::with_dependencies("merge", ["projector", "audio"]),
2599                ExecutionGroupSpec::with_dependencies("decoder", ["merge"]),
2600                ExecutionGroupSpec::with_dependencies("prediction", ["decoder"]),
2601            ],
2602            "prediction",
2603        )
2604        .unwrap()
2605    }
2606
2607    #[test]
2608    fn pipeline_schedule_owns_activity_propagation_and_ready_batches() {
2609        let graph = pipeline_graph();
2610        let contracts = [
2611            (ArchitectureGroupKind::VisionEncoder, true),
2612            (ArchitectureGroupKind::AudioEncoder, true),
2613            (ArchitectureGroupKind::Projector, false),
2614            (ArchitectureGroupKind::Merger, false),
2615            (ArchitectureGroupKind::Decoder, false),
2616            (ArchitectureGroupKind::Prediction, false),
2617        ];
2618        let mut queried = Vec::new();
2619        let mut schedule = LayeredPipelineSchedule::try_new(&graph, contracts, |group| {
2620            queried.push(group);
2621            Ok::<_, LayeredPipelineScheduleError>(group == 0)
2622        })
2623        .unwrap();
2624
2625        assert_eq!(queried, [0, 1]);
2626        assert_eq!(schedule.activity(), [true, false, true, true, true, false]);
2627        assert_eq!(schedule.compatible_batch(|_, _| true), [0, 1]);
2628        schedule.started(0).unwrap();
2629        schedule.started(1).unwrap();
2630        schedule.ordered(0).unwrap();
2631        schedule.ordered(1).unwrap();
2632        assert_eq!(schedule.ready_groups().collect::<Vec<_>>(), [2]);
2633        for group in [2, 3, 4, 5] {
2634            schedule.started(group).unwrap();
2635            schedule.ordered(group).unwrap();
2636        }
2637        assert!(schedule.is_complete());
2638    }
2639
2640    #[test]
2641    fn pipeline_schedule_rejects_kind_and_transition_drift() {
2642        let graph = pipeline_graph();
2643        let error = LayeredPipelineSchedule::try_new(
2644            &graph,
2645            [(ArchitectureGroupKind::Decoder, false)],
2646            |_| Ok::<_, LayeredPipelineScheduleError>(true),
2647        )
2648        .unwrap_err();
2649        assert_eq!(
2650            error,
2651            LayeredPipelineScheduleError::GroupContractCount {
2652                graph: 6,
2653                declared: 1,
2654            }
2655        );
2656
2657        let contracts = [
2658            (ArchitectureGroupKind::VisionEncoder, true),
2659            (ArchitectureGroupKind::AudioEncoder, true),
2660            (ArchitectureGroupKind::Projector, false),
2661            (ArchitectureGroupKind::Merger, false),
2662            (ArchitectureGroupKind::Decoder, false),
2663            (ArchitectureGroupKind::Prediction, false),
2664        ];
2665        let mut schedule = LayeredPipelineSchedule::try_new(&graph, contracts, |_| {
2666            Ok::<_, LayeredPipelineScheduleError>(true)
2667        })
2668        .unwrap();
2669        assert_eq!(
2670            schedule.started(2),
2671            Err(LayeredPipelineScheduleError::Transition(
2672                ExecutionScheduleError::DependenciesPending { group: 2 }
2673            ))
2674        );
2675    }
2676
2677    #[test]
2678    fn pipeline_schedule_consumes_declared_request_optionality() {
2679        let graph = ExecutionGraph::new(
2680            vec![
2681                ExecutionGroupSpec::root("mandatory_vision"),
2682                ExecutionGroupSpec::root("optional_audio"),
2683                ExecutionGroupSpec::with_dependencies(
2684                    "decoder",
2685                    ["mandatory_vision", "optional_audio"],
2686                ),
2687            ],
2688            "decoder",
2689        )
2690        .unwrap();
2691        let contracts = [
2692            (ArchitectureGroupKind::VisionEncoder, false),
2693            (ArchitectureGroupKind::AudioEncoder, true),
2694            (ArchitectureGroupKind::Decoder, false),
2695        ];
2696        let mut queried = Vec::new();
2697        let schedule = LayeredPipelineSchedule::try_new(&graph, contracts, |group| {
2698            queried.push(group);
2699            Ok::<_, LayeredPipelineScheduleError>(false)
2700        })
2701        .unwrap();
2702
2703        assert_eq!(queried, [1]);
2704        assert_eq!(schedule.activity(), [true, false, true]);
2705
2706        let invalid = [
2707            (ArchitectureGroupKind::VisionEncoder, false),
2708            (ArchitectureGroupKind::AudioEncoder, false),
2709            (ArchitectureGroupKind::Decoder, true),
2710        ];
2711        assert_eq!(
2712            LayeredPipelineSchedule::try_new(&graph, invalid, |_| {
2713                Ok::<_, LayeredPipelineScheduleError>(true)
2714            })
2715            .unwrap_err(),
2716            LayeredPipelineScheduleError::InvalidRequestOptionalGroup {
2717                group: 2,
2718                kind: ArchitectureGroupKind::Decoder,
2719            }
2720        );
2721    }
2722}