1use eredu_nn::{
4 DistributedNeuralBackend, GroupSelection, GroupedGatedProductOperator, GroupedNeuralBackend,
5 GroupedRelu2Operator, Tensor, TensorParallelGroupedOutput,
6};
7
8use crate::ExpertPass;
9
10mod route_intervention;
11use crate::{
12 observe_and_intervene, ActivationObserver, ParameterBankAccess, ParameterBankKey,
13 ReplicatedTextMaterializationTask, ReplicatedTextParameterOwner, RoutingObservation,
14 WeightLoweringKind,
15};
16pub use route_intervention::{select_routes_with_observer, select_routes_with_provider};
17
18#[derive(Debug, Clone, Eq, PartialEq)]
20pub struct AddressableBankParameter {
21 binding_name: String,
22 task: ReplicatedTextMaterializationTask,
23 recipe: eredu_checkpoint::recipe::DerivedWeightRecipe,
24 source_output: eredu_checkpoint::recipe::RecipeMetadata,
25 selected_bytes: u64,
26 quantization_companions: Option<crate::QuantizationCompanionBindings>,
27}
28
29impl AddressableBankParameter {
30 pub fn new(
32 binding_name: impl Into<String>,
33 task: ReplicatedTextMaterializationTask,
34 recipe: eredu_checkpoint::recipe::DerivedWeightRecipe,
35 source_output: eredu_checkpoint::recipe::RecipeMetadata,
36 selected_bytes: u64,
37 quantization_companions: Option<crate::QuantizationCompanionBindings>,
38 ) -> Result<Self, AddressableBankMemberError> {
39 let binding_name = binding_name.into();
40 if binding_name.trim().is_empty() {
41 return Err(AddressableBankMemberError::InvalidParameter {
42 parameter: task.name().to_owned(),
43 detail: "addressable binding name is empty".into(),
44 });
45 }
46 task.source_recipe()
47 .map_err(|error| AddressableBankMemberError::InvalidParameter {
48 parameter: task.name().to_owned(),
49 detail: error.to_string(),
50 })?;
51 let descriptor = task.lowering_descriptor();
52 if descriptor.source() != task.source_encoding()
53 || descriptor.executable() != task.executable()
54 || descriptor.physical_shape() != task.physical_shape()
55 || descriptor.logical_shape() != task.logical_shape()
56 {
57 return Err(AddressableBankMemberError::InvalidParameter {
58 parameter: task.name().to_owned(),
59 detail: "selected source, executable, or lowering descriptor drifted".into(),
60 });
61 }
62 let declared_sources = task
63 .sources()
64 .iter()
65 .map(String::as_str)
66 .collect::<std::collections::BTreeSet<_>>();
67 let recipe_sources = recipe
68 .source_keys()
69 .into_iter()
70 .collect::<std::collections::BTreeSet<_>>();
71 if recipe_sources.is_empty() || !recipe_sources.is_subset(&declared_sources) {
72 return Err(AddressableBankMemberError::InvalidParameter {
73 parameter: task.name().to_owned(),
74 detail: "member recipe consumes sources outside the selected task".into(),
75 });
76 }
77 if source_output.byte_len() == 0 {
78 return Err(AddressableBankMemberError::ZeroSourceBytes {
79 parameter: task.name().to_owned(),
80 });
81 }
82 if selected_bytes == 0 {
83 return Err(AddressableBankMemberError::ZeroSelectedBytes {
84 parameter: task.name().to_owned(),
85 });
86 }
87 let transforms = matches!(
88 task.lowering(),
89 WeightLoweringKind::Transform | WeightLoweringKind::DerivedTransform
90 );
91 if !transforms && quantization_companions.is_some() {
92 return Err(AddressableBankMemberError::InvalidParameter {
93 parameter: task.name().to_owned(),
94 detail: "non-transform lowering declared local transform companions".into(),
95 });
96 }
97 if transforms
98 && quantization_companions.is_none()
99 && source_output.dtype() != &eredu_checkpoint::recipe::RecipeDtype::F4
100 {
101 return Err(AddressableBankMemberError::InvalidParameter {
102 parameter: task.name().to_owned(),
103 detail: "floating transform omitted its local output companions".into(),
104 });
105 }
106 if transforms
107 && source_output.dtype() != &eredu_checkpoint::recipe::RecipeDtype::F4
108 && task.output_companions().is_empty()
109 {
110 return Err(AddressableBankMemberError::InvalidParameter {
111 parameter: task.name().to_owned(),
112 detail: "floating transform omitted its exact selected output companions".into(),
113 });
114 }
115 if let Some(companions) = quantization_companions.as_ref() {
116 let declared_roles = task
117 .output_companions()
118 .iter()
119 .map(|companion| companion.role())
120 .collect::<std::collections::BTreeSet<_>>();
121 let mut bound_roles =
122 std::collections::BTreeSet::from([eredu_nn::LinearCompanionRole::Scale]);
123 if companions.affine_bias().is_some() {
124 bound_roles.insert(eredu_nn::LinearCompanionRole::AffineBias);
125 }
126 if declared_roles != bound_roles {
127 return Err(AddressableBankMemberError::InvalidParameter {
128 parameter: task.name().to_owned(),
129 detail: "selected quantization companion roles differ from exact outputs"
130 .into(),
131 });
132 }
133 for companion in task.output_companions() {
134 let local = match companion.role() {
135 eredu_nn::LinearCompanionRole::Scale => companions.scale(),
136 eredu_nn::LinearCompanionRole::AffineBias => companions
137 .affine_bias()
138 .expect("validated affine-bias role has one binding"),
139 };
140 if companion.name() != local && !companion.name().ends_with(&format!(".{local}")) {
141 return Err(AddressableBankMemberError::InvalidParameter {
142 parameter: task.name().to_owned(),
143 detail: format!(
144 "local companion {local:?} differs from selected output {:?}",
145 companion.name()
146 ),
147 });
148 }
149 let owner_matches = match (task.owner(), companion.owner()) {
150 (
151 ReplicatedTextParameterOwner::ExecutionUnit { group, unit },
152 crate::ParameterGroupOwner::ExecutionUnit {
153 group: companion_group,
154 global_unit,
155 },
156 ) => group == companion_group.as_str() && unit == global_unit,
157 (
158 ReplicatedTextParameterOwner::StaticRole(role),
159 crate::ParameterGroupOwner::StaticRole(companion_role),
160 ) => role == companion_role,
161 _ => false,
162 };
163 if !owner_matches {
164 return Err(AddressableBankMemberError::InvalidParameter {
165 parameter: task.name().to_owned(),
166 detail: format!(
167 "selected companion {:?} has a different owner",
168 companion.name()
169 ),
170 });
171 }
172 }
173 }
174 let expected = selected_addressable_parameter_bytes(&task, &source_output)?;
175 if selected_bytes != expected {
176 return Err(AddressableBankMemberError::SelectedByteMismatch {
177 parameter: task.name().to_owned(),
178 expected,
179 actual: selected_bytes,
180 });
181 }
182 Ok(Self {
183 binding_name,
184 task,
185 recipe,
186 source_output,
187 selected_bytes,
188 quantization_companions,
189 })
190 }
191
192 pub fn binding_name(&self) -> &str {
194 &self.binding_name
195 }
196
197 pub const fn task(&self) -> &ReplicatedTextMaterializationTask {
199 &self.task
200 }
201
202 pub const fn recipe(&self) -> &eredu_checkpoint::recipe::DerivedWeightRecipe {
204 &self.recipe
205 }
206
207 pub const fn source_output(&self) -> &eredu_checkpoint::recipe::RecipeMetadata {
209 &self.source_output
210 }
211
212 pub const fn source_bytes(&self) -> u64 {
214 self.source_output.byte_len()
215 }
216
217 pub const fn selected_bytes(&self) -> u64 {
219 self.selected_bytes
220 }
221
222 pub const fn quantization_companions(&self) -> Option<&crate::QuantizationCompanionBindings> {
224 self.quantization_companions.as_ref()
225 }
226}
227
228#[derive(Debug, Clone, Eq, PartialEq)]
230pub struct AddressableBankMember {
231 key: ParameterBankKey,
232 placement: AddressableBankMemberPlacement,
233 parameters: Vec<AddressableBankParameter>,
234 source_bytes: u64,
235 selected_bytes: u64,
236}
237
238#[derive(Debug, Clone, Copy, Eq, PartialEq)]
240#[non_exhaustive]
241pub enum AddressableBankDistribution {
242 Replicated,
244 ExpertParallel,
246}
247
248#[derive(Debug, Clone, Eq, PartialEq)]
250pub struct AddressableBankMemberPlacement {
251 owner_group: crate::ExecutionGroupId,
252 owner_unit: usize,
253 unit_path: String,
254 distribution: AddressableBankDistribution,
255 owner_rank: Option<usize>,
256}
257
258impl AddressableBankMemberPlacement {
259 pub fn new(
261 owner_group: crate::ExecutionGroupId,
262 owner_unit: usize,
263 unit_path: impl Into<String>,
264 distribution: AddressableBankDistribution,
265 ) -> Result<Self, AddressableBankMemberError> {
266 let unit_path = unit_path.into();
267 if unit_path.trim().is_empty() {
268 return Err(AddressableBankMemberError::InvalidPlacement(
269 "addressable member unit path is empty".into(),
270 ));
271 }
272 Ok(Self {
273 owner_group,
274 owner_unit,
275 unit_path,
276 distribution,
277 owner_rank: None,
278 })
279 }
280
281 pub fn with_owner_rank(mut self, owner_rank: usize) -> Self {
283 self.owner_rank = Some(owner_rank);
284 self
285 }
286
287 pub const fn owner_group(&self) -> &crate::ExecutionGroupId {
289 &self.owner_group
290 }
291 pub const fn owner_unit(&self) -> usize {
293 self.owner_unit
294 }
295 pub fn unit_path(&self) -> &str {
297 &self.unit_path
298 }
299 pub const fn distribution(&self) -> AddressableBankDistribution {
301 self.distribution
302 }
303 pub const fn owner_rank(&self) -> Option<usize> {
305 self.owner_rank
306 }
307}
308
309impl AddressableBankMember {
310 pub fn new(
312 key: ParameterBankKey,
313 placement: AddressableBankMemberPlacement,
314 parameters: impl IntoIterator<Item = AddressableBankParameter>,
315 ) -> Result<Self, AddressableBankMemberError> {
316 let parameters = parameters.into_iter().collect::<Vec<_>>();
317 if parameters.is_empty() {
318 return Err(AddressableBankMemberError::EmptyMember { key });
319 }
320 if placement.owner_unit() != key.unit() {
321 return Err(AddressableBankMemberError::InvalidPlacement(format!(
322 "addressable member unit {} differs from placement unit {}",
323 key.unit(),
324 placement.owner_unit()
325 )));
326 }
327 let mut bindings = std::collections::BTreeSet::new();
328 let mut targets = std::collections::BTreeSet::new();
329 let mut source_bytes = 0u64;
330 let mut selected_bytes = 0u64;
331 for parameter in ¶meters {
332 if !bindings.insert(parameter.binding_name())
333 || !targets.insert(parameter.task().name())
334 {
335 return Err(AddressableBankMemberError::DuplicateParameter { key });
336 }
337 if !matches!(
338 parameter.task().owner(),
339 ReplicatedTextParameterOwner::ExecutionUnit { group, unit }
340 if *unit == placement.owner_unit()
341 && group == placement.owner_group().as_str()
342 ) {
343 return Err(AddressableBankMemberError::InvalidParameter {
344 parameter: parameter.task().name().to_owned(),
345 detail: "selected task has a non-bank owner".into(),
346 });
347 }
348 source_bytes = source_bytes
349 .checked_add(parameter.source_bytes())
350 .ok_or(AddressableBankMemberError::SourceByteOverflow { key })?;
351 selected_bytes = selected_bytes
352 .checked_add(parameter.selected_bytes())
353 .ok_or(AddressableBankMemberError::SelectedByteOverflow { key })?;
354 }
355 Ok(Self {
356 key,
357 placement,
358 parameters,
359 source_bytes,
360 selected_bytes,
361 })
362 }
363
364 pub const fn key(&self) -> ParameterBankKey {
366 self.key
367 }
368
369 pub const fn placement(&self) -> &AddressableBankMemberPlacement {
371 &self.placement
372 }
373
374 pub fn with_owner_rank(mut self, owner_rank: usize) -> Self {
376 self.placement = self.placement.with_owner_rank(owner_rank);
377 self
378 }
379
380 pub fn parameters(&self) -> &[AddressableBankParameter] {
382 &self.parameters
383 }
384
385 pub const fn source_bytes(&self) -> u64 {
387 self.source_bytes
388 }
389
390 pub const fn selected_bytes(&self) -> u64 {
392 self.selected_bytes
393 }
394}
395
396#[derive(Debug, Clone, Eq, PartialEq)]
398pub struct AddressableBindingTransform {
399 quantization: eredu_checkpoint::WeightQuantization,
400 companion_dtype: eredu_checkpoint::recipe::RecipeDtype,
401}
402
403impl AddressableBindingTransform {
404 pub const fn quantization(&self) -> eredu_checkpoint::WeightQuantization {
406 self.quantization
407 }
408 pub const fn companion_dtype(&self) -> &eredu_checkpoint::recipe::RecipeDtype {
410 &self.companion_dtype
411 }
412}
413
414#[derive(Debug, Clone)]
416pub struct AddressableBankBindingPlan {
417 key: ParameterBankKey,
418 bindings: Vec<crate::WeightBinding>,
419 transformations: std::collections::BTreeMap<String, AddressableBindingTransform>,
420 selected_bytes: u64,
421 placement: AddressableBankMemberPlacement,
422}
423
424impl AddressableBankBindingPlan {
425 pub const fn key(&self) -> ParameterBankKey {
427 self.key
428 }
429 pub fn bindings(&self) -> &[crate::WeightBinding] {
431 &self.bindings
432 }
433 pub const fn transformations(
435 &self,
436 ) -> &std::collections::BTreeMap<String, AddressableBindingTransform> {
437 &self.transformations
438 }
439 pub const fn selected_bytes(&self) -> u64 {
441 self.selected_bytes
442 }
443 pub const fn placement(&self) -> &AddressableBankMemberPlacement {
445 &self.placement
446 }
447 #[allow(clippy::type_complexity)]
449 pub fn into_parts(
450 self,
451 ) -> (
452 ParameterBankKey,
453 Vec<crate::WeightBinding>,
454 std::collections::BTreeMap<String, AddressableBindingTransform>,
455 u64,
456 AddressableBankMemberPlacement,
457 ) {
458 (
459 self.key,
460 self.bindings,
461 self.transformations,
462 self.selected_bytes,
463 self.placement,
464 )
465 }
466}
467
468pub fn plan_addressable_bank_bindings<L, E>(
470 members: &[AddressableBankMember],
471 source: &dyn eredu_checkpoint::store::CheckpointSource,
472 mut lower_mxfp4: L,
473) -> Result<Vec<AddressableBankBindingPlan>, AddressableBankMemberError>
474where
475 L: FnMut(
476 &ReplicatedTextMaterializationTask,
477 eredu_checkpoint::recipe::DerivedWeightRecipe,
478 &dyn eredu_checkpoint::store::CheckpointSource,
479 ) -> Result<eredu_checkpoint::recipe::DerivedWeightRecipe, E>,
480 E: std::fmt::Display,
481{
482 let mut plans = Vec::with_capacity(members.len());
483 for member in members {
484 let mut bindings = Vec::with_capacity(member.parameters().len());
485 let mut transformations = std::collections::BTreeMap::new();
486 for parameter in member.parameters() {
487 let task = parameter.task();
488 let declared = task
489 .sources()
490 .iter()
491 .map(String::as_str)
492 .collect::<std::collections::BTreeSet<_>>();
493 let physical = task
494 .physical_sources()
495 .iter()
496 .map(|item| item.catalog_key())
497 .collect::<std::collections::BTreeSet<_>>();
498 if declared != physical || physical.len() != task.physical_sources().len() {
499 return Err(AddressableBankMemberError::InvalidParameter {
500 parameter: task.name().to_owned(),
501 detail: "selected physical provenance does not exactly cover task sources"
502 .into(),
503 });
504 }
505 for admitted in task.physical_sources() {
506 let actual = source
507 .source_provenance(admitted.catalog_key())
508 .map_err(|error| AddressableBankMemberError::InvalidParameter {
509 parameter: task.name().to_owned(),
510 detail: error.to_string(),
511 })?;
512 let metadata = source
513 .source_metadata(admitted.catalog_key())
514 .map_err(|error| AddressableBankMemberError::InvalidParameter {
515 parameter: task.name().to_owned(),
516 detail: error.to_string(),
517 })?;
518 if actual.catalog_key != admitted.catalog_key()
519 || actual.physical_tensor != admitted.tensor()
520 || actual.output != admitted.output()
521 || actual.backing_shard.as_deref() != Some(admitted.shard())
522 || actual.source_encoding != *admitted.source_encoding()
523 || metadata.encoded_byte_len != admitted.encoded_byte_len()
524 {
525 return Err(AddressableBankMemberError::InvalidParameter {
526 parameter: task.name().to_owned(),
527 detail: format!(
528 "source {:?} differs from admitted provenance",
529 admitted.catalog_key()
530 ),
531 });
532 }
533 }
534 let mut recipe = parameter.recipe().clone();
535 let inferred = recipe.infer(source).map_err(|error| {
536 AddressableBankMemberError::InvalidParameter {
537 parameter: task.name().to_owned(),
538 detail: error.to_string(),
539 }
540 })?;
541 if &inferred != parameter.source_output() {
542 return Err(AddressableBankMemberError::InvalidParameter {
543 parameter: task.name().to_owned(),
544 detail: "member-local recipe output drifted".into(),
545 });
546 }
547 if task.executable() == eredu_checkpoint::LinearFormat::MxFp4
548 && inferred.dtype() == &eredu_checkpoint::recipe::RecipeDtype::F4
549 && parameter.quantization_companions().is_none()
550 {
551 recipe = lower_mxfp4(task, recipe, source).map_err(|error| {
552 AddressableBankMemberError::InvalidParameter {
553 parameter: task.name().to_owned(),
554 detail: error.to_string(),
555 }
556 })?;
557 }
558 let metadata = recipe.infer(source).map_err(|error| {
559 AddressableBankMemberError::InvalidParameter {
560 parameter: task.name().to_owned(),
561 detail: error.to_string(),
562 }
563 })?;
564 let mut binding = crate::WeightBinding::from_recipe(
565 parameter.binding_name(),
566 recipe,
567 metadata.byte_len(),
568 )
569 .and_then(|binding| binding.with_logical_target(task.name()))
570 .map_err(|error| AddressableBankMemberError::InvalidParameter {
571 parameter: task.name().to_owned(),
572 detail: error.to_string(),
573 })?;
574 if let Some(companions) = parameter.quantization_companions() {
575 let quantization = task.executable().weight_quantization().ok_or_else(|| {
576 AddressableBankMemberError::InvalidParameter {
577 parameter: task.name().to_owned(),
578 detail: "transformed task has no packed format".into(),
579 }
580 })?;
581 transformations.insert(
582 parameter.binding_name().to_owned(),
583 AddressableBindingTransform {
584 quantization,
585 companion_dtype: parameter.source_output().dtype().clone(),
586 },
587 );
588 binding = binding
589 .with_quantization_companions(
590 companions.scale(),
591 companions.affine_bias().map(str::to_owned),
592 )
593 .map_err(|error| AddressableBankMemberError::InvalidParameter {
594 parameter: task.name().to_owned(),
595 detail: error.to_string(),
596 })?;
597 }
598 bindings.push(binding);
599 }
600 crate::WeightBindingPlan::new(&bindings).map_err(|error| {
601 AddressableBankMemberError::InvalidParameter {
602 parameter: format!("{:?}", member.key()),
603 detail: error.to_string(),
604 }
605 })?;
606 plans.push(AddressableBankBindingPlan {
607 key: member.key(),
608 bindings,
609 transformations,
610 selected_bytes: member.selected_bytes(),
611 placement: member.placement().clone(),
612 });
613 }
614 Ok(plans)
615}
616
617pub fn selected_addressable_parameter_bytes(
625 task: &ReplicatedTextMaterializationTask,
626 metadata: &eredu_checkpoint::recipe::RecipeMetadata,
627) -> Result<u64, AddressableBankMemberError> {
628 if !matches!(
629 task.lowering(),
630 WeightLoweringKind::Transform | WeightLoweringKind::DerivedTransform
631 ) {
632 return Ok(metadata.byte_len());
633 }
634 let quantization = task.executable().weight_quantization().ok_or_else(|| {
635 AddressableBankMemberError::InvalidParameter {
636 parameter: task.name().to_owned(),
637 detail: "transform lowering has no packed executable format".into(),
638 }
639 })?;
640 if matches!(
641 quantization,
642 eredu_checkpoint::WeightQuantization::GgufIQuant { .. }
643 ) {
644 return Err(AddressableBankMemberError::InvalidParameter {
645 parameter: task.name().to_owned(),
646 detail: "load-time transform selected checkpoint-native GGUF encoding".into(),
647 });
648 }
649 if task.lowering_descriptor().packed_axis() != metadata.shape().len().checked_sub(1) {
650 return Err(AddressableBankMemberError::InvalidParameter {
651 parameter: task.name().to_owned(),
652 detail: "transform packed axis is not the final logical matrix axis".into(),
653 });
654 }
655 let shape = metadata.shape();
656 let (&columns, row_shape) =
657 shape
658 .split_last()
659 .ok_or_else(|| AddressableBankMemberError::InvalidParameter {
660 parameter: task.name().to_owned(),
661 detail: "transform target is not a matrix".into(),
662 })?;
663 let rows = row_shape
664 .iter()
665 .try_fold(1u64, |total, dimension| {
666 total.checked_mul(*dimension as u64)
667 })
668 .ok_or_else(|| AddressableBankMemberError::InvalidParameter {
669 parameter: task.name().to_owned(),
670 detail: "transform row geometry overflowed".into(),
671 })?;
672 let group = usize::try_from(quantization.group_size()).map_err(|_| {
673 AddressableBankMemberError::InvalidParameter {
674 parameter: task.name().to_owned(),
675 detail: "transform group size is invalid".into(),
676 }
677 })?;
678 if group == 0 || !columns.is_multiple_of(group) || !columns.is_multiple_of(32) {
679 return Err(AddressableBankMemberError::InvalidParameter {
680 parameter: task.name().to_owned(),
681 detail: "transform geometry is incompatible with its packed format".into(),
682 });
683 }
684 let groups = (columns / group) as u64;
685 let packed = (columns as u64)
686 .checked_mul(quantization.bits() as u64)
687 .and_then(|bits| bits.checked_div(8))
688 .ok_or_else(|| AddressableBankMemberError::InvalidParameter {
689 parameter: task.name().to_owned(),
690 detail: "packed row byte geometry overflowed".into(),
691 })?;
692 let scalar_bytes = metadata.dtype().bit_width().map_err(|error| {
693 AddressableBankMemberError::InvalidParameter {
694 parameter: task.name().to_owned(),
695 detail: error.to_string(),
696 }
697 })? / 8;
698 let companion = if matches!(quantization, eredu_checkpoint::WeightQuantization::MxFp4) {
699 groups
700 } else {
701 groups.checked_mul(scalar_bytes).ok_or_else(|| {
702 AddressableBankMemberError::InvalidParameter {
703 parameter: task.name().to_owned(),
704 detail: "scale byte geometry overflowed".into(),
705 }
706 })?
707 };
708 let bias = if quantization.has_biases() {
709 companion
710 } else {
711 0
712 };
713 rows.checked_mul(
714 packed
715 .checked_add(companion)
716 .and_then(|bytes| bytes.checked_add(bias))
717 .ok_or_else(|| AddressableBankMemberError::InvalidParameter {
718 parameter: task.name().to_owned(),
719 detail: "selected row byte geometry overflowed".into(),
720 })?,
721 )
722 .ok_or_else(|| AddressableBankMemberError::InvalidParameter {
723 parameter: task.name().to_owned(),
724 detail: "selected byte geometry overflowed".into(),
725 })
726}
727
728#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
730pub enum AddressableBankMemberError {
731 #[error("invalid addressable bank member placement: {0}")]
733 InvalidPlacement(String),
734 #[error("addressable bank member {key:?} is empty")]
736 EmptyMember {
737 key: ParameterBankKey,
739 },
740 #[error("addressable bank member {key:?} repeats a parameter")]
742 DuplicateParameter {
743 key: ParameterBankKey,
745 },
746 #[error("invalid addressable bank parameter {parameter:?}: {detail}")]
748 InvalidParameter {
749 parameter: String,
751 detail: String,
753 },
754 #[error("addressable bank parameter {parameter:?} source byte geometry is zero")]
756 ZeroSourceBytes {
757 parameter: String,
759 },
760 #[error("addressable bank member {key:?} source byte geometry overflowed")]
762 SourceByteOverflow {
763 key: ParameterBankKey,
765 },
766 #[error("addressable bank member {key:?} selected byte geometry overflowed")]
768 SelectedByteOverflow {
769 key: ParameterBankKey,
771 },
772 #[error("addressable bank parameter {parameter:?} selected byte geometry is zero")]
774 ZeroSelectedBytes {
775 parameter: String,
777 },
778 #[error("addressable bank parameter {parameter:?} selected bytes differ: expected {expected}, got {actual}")]
780 SelectedByteMismatch {
781 parameter: String,
783 expected: u64,
785 actual: u64,
787 },
788}
789
790pub trait IndexedMovement<B>
795where
796 B: GroupedNeuralBackend,
797{
798 type Error;
800
801 fn index_demands(
803 &mut self,
804 indices: &B::Tensor,
805 upper_bound: usize,
806 context: &<B::Tensor as Tensor>::Context,
807 ) -> Result<Vec<(usize, u64)>, Self::Error>;
808
809 fn remap_indices(
811 &mut self,
812 indices: &B::Tensor,
813 mapping: &[(usize, usize)],
814 context: &<B::Tensor as Tensor>::Context,
815 ) -> Result<B::Tensor, Self::Error>;
816
817 fn select_rows(
819 &mut self,
820 value: &B::Tensor,
821 start: usize,
822 end: usize,
823 context: &<B::Tensor as Tensor>::Context,
824 ) -> Result<B::Tensor, Self::Error>;
825
826 fn concatenate_rows(
828 &mut self,
829 values: &[B::Tensor],
830 context: &<B::Tensor as Tensor>::Context,
831 ) -> Result<B::Tensor, Self::Error>;
832}
833
834pub trait ExpertRouteTensorMovement<T> {
840 type Error;
842
843 fn shape(&self, value: &T) -> Vec<usize>;
845
846 fn gather_rows(&mut self, value: &T, rows: &[usize]) -> Result<T, Self::Error>;
848
849 fn gather_route_values(
851 &mut self,
852 value: &T,
853 flattened_routes: &[usize],
854 ) -> Result<T, Self::Error>;
855
856 fn scatter_add_rows(
861 &mut self,
862 value: T,
863 destination_rows: &[usize],
864 output_rows: usize,
865 ) -> Result<T, Self::Error>;
866}
867
868pub trait ExpertRouteExchange<T> {
874 type Error;
876
877 fn exchange_tensor(
879 &mut self,
880 counts: &crate::CommunicationPeerCounts,
881 value: T,
882 ) -> Result<T, Self::Error>;
883
884 fn exchange_indices(
886 &mut self,
887 counts: &crate::CommunicationPeerCounts,
888 values: Vec<usize>,
889 ) -> Result<Vec<usize>, Self::Error>;
890}
891
892#[derive(Debug, Clone, Copy, Eq, PartialEq)]
894#[non_exhaustive]
895pub enum ExpertRouteCombination {
896 CoefficientWeightedSum,
898}
899
900pub struct AddressableExpertRouteRequest<'a, T> {
902 pub unit: usize,
904 pub input: &'a T,
906 pub global_experts: &'a [usize],
912 pub owner_local_experts: &'a [usize],
914 pub selected_scores: &'a T,
916 pub coefficients: &'a T,
918 pub pass: ExpertPass,
920 pub access: ParameterBankAccess,
922 pub combination: ExpertRouteCombination,
924}
925
926impl<T> AddressableExpertRouteRequest<'_, T> {
927 pub fn addressable_bank_key(&self, row: usize) -> Option<ParameterBankKey> {
932 self.global_experts
933 .get(row)
934 .copied()
935 .map(|global| ParameterBankKey::new(self.unit, global))
936 }
937
938 pub fn owner_local_execution_id(&self, row: usize) -> Option<usize> {
940 self.owner_local_experts.get(row).copied()
941 }
942}
943
944pub trait AddressableExpertRouteProvider<T> {
951 type Error;
953
954 fn execute_addressable_routes(
956 &mut self,
957 request: AddressableExpertRouteRequest<'_, T>,
958 ) -> Result<T, Self::Error>;
959
960 fn execute_addressable_routes_tensor_parallel(
969 &mut self,
970 request: AddressableExpertRouteRequest<'_, T>,
971 ) -> Result<RoutedExpertTensorParallelOutput<T>, Self::Error> {
972 self.execute_addressable_routes(request)
973 .map(RoutedExpertTensorParallelOutput::Complete)
974 }
975}
976
977#[derive(Debug, Clone, Copy)]
979pub struct ParameterBankAcquisition<'a> {
980 entries: &'a [(ParameterBankKey, u64)],
981 access: ParameterBankAccess,
982}
983
984impl<'a> ParameterBankAcquisition<'a> {
985 pub const fn new(entries: &'a [(ParameterBankKey, u64)], access: ParameterBankAccess) -> Self {
987 Self { entries, access }
988 }
989
990 pub const fn entries(&self) -> &'a [(ParameterBankKey, u64)] {
992 self.entries
993 }
994
995 pub const fn access(&self) -> ParameterBankAccess {
997 self.access
998 }
999}
1000
1001pub trait AddressableGroupedBank<B>
1007where
1008 B: GroupedNeuralBackend,
1009{
1010 type Acquisition;
1012 type Report;
1014 type Error;
1016
1017 fn member_bytes(&self, key: ParameterBankKey) -> Option<u64>;
1019
1020 fn acquire(
1022 &mut self,
1023 request: ParameterBankAcquisition<'_>,
1024 context: &<B::Tensor as Tensor>::Context,
1025 ) -> Result<Self::Acquisition, Self::Error>;
1026
1027 fn gated_product_groups(
1029 &mut self,
1030 acquisition: &Self::Acquisition,
1031 spec: &eredu_nn::GroupedGatedProductSpec,
1032 context: &<B::Tensor as Tensor>::Context,
1033 ) -> Result<B::GatedProductGroups, Self::Error>;
1034
1035 fn relu2_groups(
1037 &mut self,
1038 acquisition: &Self::Acquisition,
1039 spec: &eredu_nn::GroupedRelu2Spec,
1040 context: &<B::Tensor as Tensor>::Context,
1041 ) -> Result<B::Relu2Groups, Self::Error>;
1042
1043 fn complete(
1045 &mut self,
1046 acquisition: Self::Acquisition,
1047 output: &B::Tensor,
1048 context: &<B::Tensor as Tensor>::Context,
1049 ) -> Result<(), Self::Error>;
1050
1051 fn report(&self) -> Result<Self::Report, Self::Error>;
1053}
1054
1055pub trait AddressableGatedProductBank<B>
1057where
1058 B: GroupedNeuralBackend,
1059{
1060 type Error;
1062
1063 fn acquire(
1065 &mut self,
1066 key: ParameterBankKey,
1067 spec: &eredu_nn::GroupedGatedProductSpec,
1068 context: &<B::Tensor as Tensor>::Context,
1069 ) -> Result<&mut B::GatedProductGroups, Self::Error>;
1070}
1071
1072pub struct RoutedExpertRequest<'a, T> {
1074 pub layer: usize,
1076 pub input: &'a T,
1078 pub routes: &'a GroupSelection<T>,
1080 pub pass: ExpertPass,
1082}
1083
1084impl<T> RoutedExpertRequest<'_, T> {
1085 pub const fn parameter_bank_access(&self) -> ParameterBankAccess {
1088 self.pass.parameter_bank_access()
1089 }
1090}
1091
1092pub enum RoutedExpertTensorParallelOutput<T> {
1094 Complete(T),
1096 Partial(TensorParallelGroupedOutput<T>),
1098}
1099
1100pub fn reduce_tensor_parallel_expert_output<B>(
1102 output: TensorParallelGroupedOutput<B::Tensor>,
1103 parallel: &B::ParallelContext,
1104 context: &<B::Tensor as Tensor>::Context,
1105) -> Result<B::Tensor, eredu_nn::Error>
1106where
1107 B: GroupedNeuralBackend + DistributedNeuralBackend,
1108{
1109 let reduced = B::sum_parallel(output.reducible().clone(), parallel, context)?;
1110 match output.post_reduce().cloned() {
1111 Some(bias) => reduced.add(&bias, context),
1112 None => Ok(reduced),
1113 }
1114}
1115
1116pub fn combine_tensor_parallel_expert_outputs<B>(
1118 left: TensorParallelGroupedOutput<B::Tensor>,
1119 right: TensorParallelGroupedOutput<B::Tensor>,
1120 context: &<B::Tensor as Tensor>::Context,
1121) -> Result<TensorParallelGroupedOutput<B::Tensor>, eredu_nn::Error>
1122where
1123 B: GroupedNeuralBackend,
1124{
1125 let post_reduce = match (left.post_reduce().cloned(), right.post_reduce().cloned()) {
1126 (Some(left), Some(right)) => Some(left.add(&right, context)?),
1127 (Some(bias), None) | (None, Some(bias)) => Some(bias),
1128 (None, None) => None,
1129 };
1130 Ok(TensorParallelGroupedOutput::new(
1131 left.reducible().add(right.reducible(), context)?,
1132 post_reduce,
1133 ))
1134}
1135
1136pub fn combine_routed_expert_tensor_parallel<B>(
1138 left: RoutedExpertTensorParallelOutput<B::Tensor>,
1139 right: RoutedExpertTensorParallelOutput<B::Tensor>,
1140 context: &<B::Tensor as Tensor>::Context,
1141) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, eredu_nn::Error>
1142where
1143 B: GroupedNeuralBackend,
1144{
1145 match (left, right) {
1146 (
1147 RoutedExpertTensorParallelOutput::Complete(left),
1148 RoutedExpertTensorParallelOutput::Complete(right),
1149 ) => Ok(RoutedExpertTensorParallelOutput::Complete(
1150 left.add(&right, context)?,
1151 )),
1152 (
1153 RoutedExpertTensorParallelOutput::Partial(left),
1154 RoutedExpertTensorParallelOutput::Partial(right),
1155 ) => combine_tensor_parallel_expert_outputs::<B>(left, right, context)
1156 .map(RoutedExpertTensorParallelOutput::Partial),
1157 _ => Err(eredu_nn::Error::backend(
1158 "provider mixed complete and rank-local expert outputs in one block",
1159 )),
1160 }
1161}
1162
1163pub fn reduce_routed_expert_tensor_parallel<B>(
1165 output: RoutedExpertTensorParallelOutput<B::Tensor>,
1166 parallel: &B::ParallelContext,
1167 context: &<B::Tensor as Tensor>::Context,
1168) -> Result<B::Tensor, eredu_nn::Error>
1169where
1170 B: GroupedNeuralBackend + DistributedNeuralBackend,
1171{
1172 match output {
1173 RoutedExpertTensorParallelOutput::Complete(output) => Ok(output),
1174 RoutedExpertTensorParallelOutput::Partial(output) => {
1175 reduce_tensor_parallel_expert_output::<B>(output, parallel, context)
1176 }
1177 }
1178}
1179
1180pub trait RoutedExpertProvider<B>
1187where
1188 B: GroupedNeuralBackend,
1189{
1190 type Error;
1192
1193 fn routing_control(
1195 &mut self,
1196 _token_rows: u64,
1197 ) -> Result<Option<eredu_nn::routing_intervention::GroupSelectionControl>, Self::Error> {
1198 Ok(None)
1199 }
1200
1201 fn routing_applied(
1203 &mut self,
1204 _original: Option<crate::RoutingDecision<'_, B::Tensor>>,
1205 _effective: crate::RoutingDecision<'_, B::Tensor>,
1206 ) -> Result<(), Self::Error> {
1207 Ok(())
1208 }
1209
1210 fn routing_failed(&mut self, _message: &str) {}
1212
1213 fn forward_grouped(
1215 &mut self,
1216 resident_bank: &mut B::GatedProductGroups,
1217 request: RoutedExpertRequest<'_, B::Tensor>,
1218 context: &<B::Tensor as Tensor>::Context,
1219 ) -> Result<B::Tensor, Self::Error>;
1220
1221 fn forward_compact_grouped(
1227 &mut self,
1228 resident_bank: &mut B::GatedProductGroups,
1229 request: RoutedExpertRequest<'_, B::Tensor>,
1230 context: &<B::Tensor as Tensor>::Context,
1231 ) -> Result<B::Tensor, Self::Error> {
1232 self.forward_grouped(resident_bank, request, context)
1233 }
1234
1235 fn forward_relu2_routed(
1237 &mut self,
1238 resident_bank: &mut B::Relu2Groups,
1239 request: RoutedExpertRequest<'_, B::Tensor>,
1240 context: &<B::Tensor as Tensor>::Context,
1241 ) -> Result<B::Tensor, Self::Error>;
1242}
1243
1244pub trait TensorParallelRoutedExpertProvider<B>: RoutedExpertProvider<B>
1246where
1247 B: GroupedNeuralBackend,
1248{
1249 fn forward_grouped_tensor_parallel(
1251 &mut self,
1252 resident_bank: &mut B::GatedProductGroups,
1253 request: RoutedExpertRequest<'_, B::Tensor>,
1254 partitions: usize,
1255 context: &<B::Tensor as Tensor>::Context,
1256 ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error>;
1257
1258 fn forward_compact_grouped_tensor_parallel(
1261 &mut self,
1262 resident_bank: &mut B::GatedProductGroups,
1263 request: RoutedExpertRequest<'_, B::Tensor>,
1264 partitions: usize,
1265 context: &<B::Tensor as Tensor>::Context,
1266 ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error> {
1267 self.forward_grouped_tensor_parallel(resident_bank, request, partitions, context)
1268 }
1269
1270 fn forward_relu2_routed_tensor_parallel(
1272 &mut self,
1273 resident_bank: &mut B::Relu2Groups,
1274 request: RoutedExpertRequest<'_, B::Tensor>,
1275 partitions: usize,
1276 context: &<B::Tensor as Tensor>::Context,
1277 ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error>;
1278}
1279
1280#[derive(Debug, Clone, Eq, PartialEq)]
1283pub struct RoutedObservationPoint {
1284 path: String,
1285 expert_count: i32,
1286}
1287
1288impl RoutedObservationPoint {
1289 pub fn new(path: impl Into<String>, expert_count: i32) -> Self {
1291 Self {
1292 path: path.into(),
1293 expert_count,
1294 }
1295 }
1296
1297 pub fn path(&self) -> &str {
1299 &self.path
1300 }
1301
1302 pub const fn expert_count(&self) -> i32 {
1304 self.expert_count
1305 }
1306}
1307
1308#[derive(Debug)]
1310pub enum ObservedExpertProviderError<P, O> {
1311 Provider(P),
1313 Observer(O),
1315}
1316
1317impl<P, O> std::fmt::Display for ObservedExpertProviderError<P, O>
1318where
1319 P: std::fmt::Display,
1320 O: std::fmt::Display,
1321{
1322 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1323 match self {
1324 Self::Provider(error) => write!(formatter, "routed expert provider failed: {error}"),
1325 Self::Observer(error) => write!(formatter, "routed expert observer failed: {error}"),
1326 }
1327 }
1328}
1329
1330impl<P, O> std::error::Error for ObservedExpertProviderError<P, O>
1331where
1332 P: std::error::Error + 'static,
1333 O: std::error::Error + 'static,
1334{
1335}
1336
1337pub struct ObservedExpertProvider<'a, P, O: ?Sized, E> {
1345 provider: &'a mut P,
1346 observer: &'a mut O,
1347 point: RoutedObservationPoint,
1348 error: std::marker::PhantomData<fn() -> E>,
1349}
1350
1351impl<'a, P, O: ?Sized, E> ObservedExpertProvider<'a, P, O, E> {
1352 pub fn new(provider: &'a mut P, observer: &'a mut O, point: RoutedObservationPoint) -> Self {
1354 Self {
1355 provider,
1356 observer,
1357 point,
1358 error: std::marker::PhantomData,
1359 }
1360 }
1361
1362 fn observe<T, ObservationError>(
1363 &mut self,
1364 routes: &eredu_nn::GroupSelection<T>,
1365 output: &T,
1366 ) -> Result<T, ObservationError>
1367 where
1368 T: Clone,
1369 O: ActivationObserver<T, ObservationError>,
1370 {
1371 self.observer.observe_routing(RoutingObservation {
1372 path: self.point.path(),
1373 selected_experts: routes.group_indices(),
1374 selected_scores: routes.selected_scores(),
1375 coefficients: routes.coefficients(),
1376 routed_output: output,
1377 local_routed_output: None,
1378 reduced_routed_output: None,
1379 shared_output: None,
1380 combined_output: None,
1381 expert_count: self.point.expert_count(),
1382 })?;
1383 observe_and_intervene(
1384 self.observer,
1385 &format!("{}.output", self.point.path()),
1386 output,
1387 )
1388 }
1389}
1390
1391impl<B, P, O, E> RoutedExpertProvider<B> for ObservedExpertProvider<'_, P, O, E>
1392where
1393 B: GroupedNeuralBackend,
1394 P: RoutedExpertProvider<B>,
1395 O: ActivationObserver<B::Tensor, E> + ?Sized,
1396{
1397 type Error = ObservedExpertProviderError<P::Error, E>;
1398
1399 fn routing_control(
1400 &mut self,
1401 token_rows: u64,
1402 ) -> Result<Option<eredu_nn::routing_intervention::GroupSelectionControl>, Self::Error> {
1403 self.observer
1404 .routing_control(self.point.path(), token_rows)
1405 .map_err(ObservedExpertProviderError::Observer)
1406 }
1407
1408 fn routing_applied(
1409 &mut self,
1410 original: Option<crate::RoutingDecision<'_, B::Tensor>>,
1411 effective: crate::RoutingDecision<'_, B::Tensor>,
1412 ) -> Result<(), Self::Error> {
1413 self.observer
1414 .routing_applied(self.point.path(), original, effective)
1415 .map_err(ObservedExpertProviderError::Observer)
1416 }
1417
1418 fn routing_failed(&mut self, message: &str) {
1419 self.observer.routing_failed(self.point.path(), message);
1420 }
1421
1422 fn forward_grouped(
1423 &mut self,
1424 resident_bank: &mut B::GatedProductGroups,
1425 request: RoutedExpertRequest<'_, B::Tensor>,
1426 context: &<B::Tensor as Tensor>::Context,
1427 ) -> Result<B::Tensor, Self::Error> {
1428 let routes = request.routes;
1429 let output = self
1430 .provider
1431 .forward_grouped(resident_bank, request, context)
1432 .map_err(ObservedExpertProviderError::Provider)?;
1433 self.observe(routes, &output)
1434 .map_err(ObservedExpertProviderError::Observer)
1435 }
1436
1437 fn forward_relu2_routed(
1438 &mut self,
1439 resident_bank: &mut B::Relu2Groups,
1440 request: RoutedExpertRequest<'_, B::Tensor>,
1441 context: &<B::Tensor as Tensor>::Context,
1442 ) -> Result<B::Tensor, Self::Error> {
1443 let routes = request.routes;
1444 let output = self
1445 .provider
1446 .forward_relu2_routed(resident_bank, request, context)
1447 .map_err(ObservedExpertProviderError::Provider)?;
1448 self.observe(routes, &output)
1449 .map_err(ObservedExpertProviderError::Observer)
1450 }
1451}
1452
1453impl<B, P, O, E> TensorParallelRoutedExpertProvider<B> for ObservedExpertProvider<'_, P, O, E>
1454where
1455 B: GroupedNeuralBackend,
1456 P: TensorParallelRoutedExpertProvider<B>,
1457 O: ActivationObserver<B::Tensor, E> + ?Sized,
1458{
1459 fn forward_grouped_tensor_parallel(
1460 &mut self,
1461 resident_bank: &mut B::GatedProductGroups,
1462 request: RoutedExpertRequest<'_, B::Tensor>,
1463 partitions: usize,
1464 context: &<B::Tensor as Tensor>::Context,
1465 ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error> {
1466 self.provider
1467 .forward_grouped_tensor_parallel(resident_bank, request, partitions, context)
1468 .map_err(ObservedExpertProviderError::Provider)
1469 }
1470
1471 fn forward_relu2_routed_tensor_parallel(
1472 &mut self,
1473 resident_bank: &mut B::Relu2Groups,
1474 request: RoutedExpertRequest<'_, B::Tensor>,
1475 partitions: usize,
1476 context: &<B::Tensor as Tensor>::Context,
1477 ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error> {
1478 self.provider
1479 .forward_relu2_routed_tensor_parallel(resident_bank, request, partitions, context)
1480 .map_err(ObservedExpertProviderError::Provider)
1481 }
1482}
1483
1484#[derive(Debug, Default, Clone, Copy)]
1486pub struct ResidentExpertProvider;
1487
1488impl<B> RoutedExpertProvider<B> for ResidentExpertProvider
1489where
1490 B: GroupedNeuralBackend,
1491{
1492 type Error = eredu_nn::Error;
1493
1494 fn forward_grouped(
1495 &mut self,
1496 resident_bank: &mut B::GatedProductGroups,
1497 request: RoutedExpertRequest<'_, B::Tensor>,
1498 context: &<B::Tensor as Tensor>::Context,
1499 ) -> Result<B::Tensor, Self::Error> {
1500 resident_bank.forward_grouped(request.input, request.routes, context)
1501 }
1502
1503 fn forward_relu2_routed(
1504 &mut self,
1505 resident_bank: &mut B::Relu2Groups,
1506 request: RoutedExpertRequest<'_, B::Tensor>,
1507 context: &<B::Tensor as Tensor>::Context,
1508 ) -> Result<B::Tensor, Self::Error> {
1509 resident_bank.forward_grouped(request.input, request.routes, context)
1510 }
1511}
1512
1513impl<B> TensorParallelRoutedExpertProvider<B> for ResidentExpertProvider
1514where
1515 B: eredu_nn::TensorParallelGroupedNeuralBackend,
1516{
1517 fn forward_grouped_tensor_parallel(
1518 &mut self,
1519 resident_bank: &mut B::GatedProductGroups,
1520 request: RoutedExpertRequest<'_, B::Tensor>,
1521 partitions: usize,
1522 context: &<B::Tensor as Tensor>::Context,
1523 ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error> {
1524 B::gated_product_groups_tensor_parallel(
1525 resident_bank,
1526 request.input,
1527 request.routes,
1528 partitions,
1529 context,
1530 )
1531 .map(RoutedExpertTensorParallelOutput::Partial)
1532 }
1533
1534 fn forward_relu2_routed_tensor_parallel(
1535 &mut self,
1536 resident_bank: &mut B::Relu2Groups,
1537 request: RoutedExpertRequest<'_, B::Tensor>,
1538 partitions: usize,
1539 context: &<B::Tensor as Tensor>::Context,
1540 ) -> Result<RoutedExpertTensorParallelOutput<B::Tensor>, Self::Error> {
1541 B::relu2_groups_tensor_parallel(
1542 resident_bank,
1543 request.input,
1544 request.routes,
1545 partitions,
1546 context,
1547 )
1548 .map(RoutedExpertTensorParallelOutput::Partial)
1549 }
1550}