1use std::collections::{BTreeMap, BTreeSet};
4
5use crate::store::{CheckpointSource, StoreError, TensorMetadata, TensorSelection, WeightStore};
6use crate::StoredDtype;
7
8pub trait RecipeCatalog {
10 fn tensor_metadata(&self, key: &str) -> Result<TensorMetadata, StoreError>;
12}
13
14pub trait BoundedRecipeSource: RecipeCatalog {
17 fn verify_bounded_source(
19 &self,
20 key: &str,
21 selection: TensorSelection,
22 ) -> Result<(), StoreError>;
23}
24
25impl<T: WeightStore> BoundedRecipeSource for T {
26 fn verify_bounded_source(
27 &self,
28 key: &str,
29 selection: TensorSelection,
30 ) -> Result<(), StoreError> {
31 drop(self.acquire(crate::store::TensorReadRequest {
32 key: key.to_owned(),
33 selection,
34 policy: crate::store::ReadPolicy::RequireBounded,
35 })?);
36 Ok(())
37 }
38}
39
40impl<T: WeightStore> RecipeCatalog for T {
41 fn tensor_metadata(&self, key: &str) -> Result<TensorMetadata, StoreError> {
42 self.metadata(key)
43 }
44}
45
46impl RecipeCatalog for dyn CheckpointSource + '_ {
47 fn tensor_metadata(&self, key: &str) -> Result<TensorMetadata, StoreError> {
48 self.source_metadata(key)
49 }
50}
51
52impl BoundedRecipeSource for dyn CheckpointSource + '_ {
53 fn verify_bounded_source(
54 &self,
55 key: &str,
56 selection: TensorSelection,
57 ) -> Result<(), StoreError> {
58 drop(self.acquire_lease(crate::store::TensorReadRequest {
59 key: key.to_owned(),
60 selection,
61 policy: crate::store::ReadPolicy::RequireBounded,
62 })?);
63 Ok(())
64 }
65}
66
67#[derive(Debug, Clone, Eq, PartialEq)]
69#[non_exhaustive]
70#[allow(missing_docs)]
71pub enum RecipeDtype {
72 Bool,
73 U8,
74 I8,
75 I16,
76 U16,
77 F16,
78 BF16,
79 I32,
80 U32,
81 F32,
82 F64,
83 I64,
84 U64,
85 C64,
86 F8E4M3,
87 F8E5M2,
88 F4,
89 F8E8M0,
90 Other(String),
91}
92
93impl RecipeDtype {
94 pub fn bit_width(&self) -> Result<u64, RecipeError> {
96 match self {
97 Self::F4 => Ok(4),
98 Self::Bool | Self::U8 | Self::I8 | Self::F8E4M3 | Self::F8E5M2 | Self::F8E8M0 => Ok(8),
99 Self::I16 | Self::U16 | Self::F16 | Self::BF16 => Ok(16),
100 Self::I32 | Self::U32 | Self::F32 => Ok(32),
101 Self::F64 | Self::I64 | Self::U64 | Self::C64 => Ok(64),
102 Self::Other(dtype) => Err(RecipeError::UnsupportedDtype {
103 dtype: dtype.clone(),
104 }),
105 }
106 }
107}
108
109impl From<StoredDtype> for RecipeDtype {
110 fn from(value: StoredDtype) -> Self {
111 match value {
112 StoredDtype::Bool => Self::Bool,
113 StoredDtype::U8 => Self::U8,
114 StoredDtype::I8 => Self::I8,
115 StoredDtype::I16 => Self::I16,
116 StoredDtype::U16 => Self::U16,
117 StoredDtype::F16 => Self::F16,
118 StoredDtype::BF16 => Self::BF16,
119 StoredDtype::I32 => Self::I32,
120 StoredDtype::U32 => Self::U32,
121 StoredDtype::F32 => Self::F32,
122 StoredDtype::F64 => Self::F64,
123 StoredDtype::I64 => Self::I64,
124 StoredDtype::U64 => Self::U64,
125 StoredDtype::C64 => Self::C64,
126 StoredDtype::F8E4M3 => Self::F8E4M3,
127 StoredDtype::F8E5M2 => Self::F8E5M2,
128 StoredDtype::F4 => Self::F4,
129 StoredDtype::F8E8M0 => Self::F8E8M0,
130 StoredDtype::Other(dtype) => Self::Other(dtype),
131 }
132 }
133}
134
135#[derive(Debug, Clone, Eq, PartialEq)]
137pub struct RecipeMetadata {
138 pub shape: Vec<usize>,
140 pub dtype: RecipeDtype,
142 pub byte_len: u64,
144}
145
146#[derive(Debug, Clone, Eq, PartialEq)]
150pub struct AtomicRecipeSet {
151 outputs: BTreeMap<String, DerivedWeightRecipe>,
152 aliases: BTreeMap<String, String>,
153}
154
155impl AtomicRecipeSet {
156 pub fn new<C: RecipeCatalog + ?Sized>(
159 catalog: &C,
160 outputs: impl IntoIterator<Item = (String, DerivedWeightRecipe)>,
161 ) -> Result<Self, RecipeError> {
162 Self::new_with_aliases(catalog, outputs, std::iter::empty())
163 }
164
165 pub fn new_with_aliases<C: RecipeCatalog + ?Sized>(
171 catalog: &C,
172 outputs: impl IntoIterator<Item = (String, DerivedWeightRecipe)>,
173 aliases: impl IntoIterator<Item = RecipeAlias>,
174 ) -> Result<Self, RecipeError> {
175 let mut validated = BTreeMap::new();
176 for (target, recipe) in outputs {
177 if target.trim().is_empty() {
178 return Err(RecipeError::EmptyOutputName);
179 }
180 if validated.insert(target.clone(), recipe).is_some() {
181 return Err(RecipeError::DuplicateOutput { target });
182 }
183 }
184 if validated.is_empty() {
185 return Err(RecipeError::EmptyOutputs);
186 }
187 for recipe in validated.values() {
188 recipe.infer(catalog)?;
189 }
190 let aliases = validate_recipe_aliases(validated.keys(), aliases)?;
191 Ok(Self {
192 outputs: validated,
193 aliases,
194 })
195 }
196
197 pub fn get(&self, target: &str) -> Option<&DerivedWeightRecipe> {
199 self.outputs.get(target)
200 }
201
202 pub fn get_resolved(&self, target: &str) -> Option<(&str, &DerivedWeightRecipe)> {
204 let owner = self
205 .aliases
206 .get(target)
207 .map(String::as_str)
208 .unwrap_or(target);
209 self.outputs
210 .get_key_value(owner)
211 .map(|(owner, recipe)| (owner.as_str(), recipe))
212 }
213
214 pub fn iter(&self) -> impl Iterator<Item = (&str, &DerivedWeightRecipe)> {
216 self.outputs
217 .iter()
218 .map(|(target, recipe)| (target.as_str(), recipe))
219 }
220
221 pub fn aliases(&self) -> impl Iterator<Item = (&str, &str)> {
223 self.aliases
224 .iter()
225 .map(|(alias, owner)| (alias.as_str(), owner.as_str()))
226 }
227
228 pub fn into_outputs(self) -> BTreeMap<String, DerivedWeightRecipe> {
230 self.outputs
231 }
232
233 pub fn into_parts(
235 self,
236 ) -> (
237 BTreeMap<String, DerivedWeightRecipe>,
238 BTreeMap<String, String>,
239 ) {
240 (self.outputs, self.aliases)
241 }
242}
243
244#[derive(Debug, Clone, Eq, PartialEq)]
246pub struct RecipeAlias {
247 pub alias: String,
249 pub destination: String,
251}
252
253impl RecipeAlias {
254 pub fn new(alias: impl Into<String>, destination: impl Into<String>) -> Self {
256 Self {
257 alias: alias.into(),
258 destination: destination.into(),
259 }
260 }
261}
262
263fn validate_recipe_aliases<'a>(
264 outputs: impl IntoIterator<Item = &'a String>,
265 aliases: impl IntoIterator<Item = RecipeAlias>,
266) -> Result<BTreeMap<String, String>, RecipeError> {
267 let outputs = outputs.into_iter().cloned().collect::<BTreeSet<_>>();
268 let mut declarations = BTreeMap::new();
269 for declaration in aliases {
270 if declaration.alias.trim().is_empty() {
271 return Err(RecipeError::EmptyAliasName);
272 }
273 if declaration.destination.trim().is_empty() {
274 return Err(RecipeError::InvalidAliasDestination {
275 alias: declaration.alias,
276 destination: declaration.destination,
277 });
278 }
279 if outputs.contains(&declaration.alias) {
280 return Err(RecipeError::AliasOutputCollision {
281 alias: declaration.alias,
282 });
283 }
284 if declarations
285 .insert(declaration.alias.clone(), declaration.destination)
286 .is_some()
287 {
288 return Err(RecipeError::DuplicateAlias {
289 alias: declaration.alias,
290 });
291 }
292 }
293
294 fn resolve(
295 alias: &str,
296 outputs: &BTreeSet<String>,
297 declarations: &BTreeMap<String, String>,
298 resolved: &mut BTreeMap<String, String>,
299 visiting: &mut BTreeSet<String>,
300 ) -> Result<String, RecipeError> {
301 if let Some(owner) = resolved.get(alias) {
302 return Ok(owner.clone());
303 }
304 if !visiting.insert(alias.to_owned()) {
305 return Err(RecipeError::AliasCycle {
306 alias: alias.to_owned(),
307 });
308 }
309 let destination = declarations
310 .get(alias)
311 .expect("resolver receives a declared alias");
312 let owner = if outputs.contains(destination) {
313 destination.clone()
314 } else if declarations.contains_key(destination) {
315 resolve(destination, outputs, declarations, resolved, visiting)?
316 } else {
317 return Err(RecipeError::InvalidAliasDestination {
318 alias: alias.to_owned(),
319 destination: destination.clone(),
320 });
321 };
322 visiting.remove(alias);
323 resolved.insert(alias.to_owned(), owner.clone());
324 Ok(owner)
325 }
326
327 let mut resolved = BTreeMap::new();
328 for alias in declarations.keys() {
329 resolve(
330 alias,
331 &outputs,
332 &declarations,
333 &mut resolved,
334 &mut BTreeSet::new(),
335 )?;
336 }
337 Ok(resolved)
338}
339
340#[derive(Debug, Clone, Eq, PartialEq)]
342pub struct MatrixRecipeMember {
343 pub target: String,
345 pub recipe: DerivedWeightRecipe,
347}
348
349impl MatrixRecipeMember {
350 pub fn new(target: impl Into<String>, recipe: DerivedWeightRecipe) -> Self {
352 Self {
353 target: target.into(),
354 recipe,
355 }
356 }
357}
358
359#[derive(Debug, Clone, Eq, PartialEq)]
365pub struct AtomicMatrixRecipeFamily {
366 weight: MatrixRecipeMember,
367 scales: Option<MatrixRecipeMember>,
368 biases: Option<MatrixRecipeMember>,
369}
370
371impl AtomicMatrixRecipeFamily {
372 pub fn new<C: RecipeCatalog + ?Sized>(
374 catalog: &C,
375 weight: MatrixRecipeMember,
376 scales: Option<MatrixRecipeMember>,
377 biases: Option<MatrixRecipeMember>,
378 ) -> Result<Self, RecipeError> {
379 if biases.is_some() && scales.is_none() {
380 return Err(RecipeError::MatrixBiasWithoutScales);
381 }
382 let family = Self {
383 weight,
384 scales,
385 biases,
386 };
387 family.validate(catalog)?;
388 Ok(family)
389 }
390
391 pub const fn weight(&self) -> &MatrixRecipeMember {
393 &self.weight
394 }
395
396 pub const fn scales(&self) -> Option<&MatrixRecipeMember> {
398 self.scales.as_ref()
399 }
400
401 pub const fn biases(&self) -> Option<&MatrixRecipeMember> {
403 self.biases.as_ref()
404 }
405
406 pub fn select_leading_axis<C: RecipeCatalog + ?Sized>(
411 &self,
412 catalog: &C,
413 selection: TensorSelection,
414 ) -> Result<Self, RecipeError> {
415 match &selection {
416 TensorSelection::Full
417 | TensorSelection::Range { axis: 0, .. }
418 | TensorSelection::Indices { axis: 0, .. } => {}
419 TensorSelection::Range { axis, .. } | TensorSelection::Indices { axis, .. } => {
420 return Err(RecipeError::MatrixFamilySelectionAxis { axis: *axis });
421 }
422 TensorSelection::Contiguous { .. } => {
423 return Err(RecipeError::MatrixFamilyContiguousSelection);
424 }
425 }
426 let select = |member: &MatrixRecipeMember| -> Result<MatrixRecipeMember, RecipeError> {
427 Ok(MatrixRecipeMember {
428 target: member.target.clone(),
429 recipe: member.recipe.select_bounded(catalog, selection.clone())?,
430 })
431 };
432 Self::new(
433 catalog,
434 select(&self.weight)?,
435 self.scales.as_ref().map(select).transpose()?,
436 self.biases.as_ref().map(select).transpose()?,
437 )
438 }
439
440 pub fn publish<C: RecipeCatalog + ?Sized>(
442 &self,
443 catalog: &C,
444 aliases: impl IntoIterator<Item = RecipeAlias>,
445 ) -> Result<AtomicRecipeSet, RecipeError> {
446 self.validate(catalog)?;
447 AtomicRecipeSet::new_with_aliases(
448 catalog,
449 std::iter::once(&self.weight)
450 .chain(self.scales.iter())
451 .chain(self.biases.iter())
452 .map(|member| (member.target.clone(), member.recipe.clone())),
453 aliases,
454 )
455 }
456
457 fn validate<C: RecipeCatalog + ?Sized>(&self, catalog: &C) -> Result<(), RecipeError> {
458 let members = std::iter::once(&self.weight)
459 .chain(self.scales.iter())
460 .chain(self.biases.iter())
461 .collect::<Vec<_>>();
462 let mut targets = BTreeSet::new();
463 for member in &members {
464 if member.target.trim().is_empty() {
465 return Err(RecipeError::EmptyOutputName);
466 }
467 if !targets.insert(member.target.clone()) {
468 return Err(RecipeError::DuplicateOutput {
469 target: member.target.clone(),
470 });
471 }
472 }
473 let weight = self.weight.recipe.infer(catalog)?;
474 if weight.shape.len() < 2 {
475 return Err(RecipeError::InvalidMatrixFamilyWeight {
476 shape: weight.shape,
477 });
478 }
479 let leading = &weight.shape[..weight.shape.len() - 1];
480 let mut scale_shape = None;
481 for (kind, member) in [
482 ("scales", self.scales.as_ref()),
483 ("biases", self.biases.as_ref()),
484 ] {
485 let Some(member) = member else { continue };
486 let metadata = member.recipe.infer(catalog)?;
487 if metadata.shape.len() != weight.shape.len()
488 || metadata.shape[..metadata.shape.len() - 1] != *leading
489 {
490 return Err(RecipeError::MatrixCompanionGeometry {
491 member: kind,
492 weight: weight.shape.clone(),
493 companion: metadata.shape,
494 });
495 }
496 if kind == "scales" {
497 scale_shape = Some(metadata.shape);
498 } else if scale_shape.as_ref() != Some(&metadata.shape) {
499 return Err(RecipeError::MatrixScaleBiasGeometry {
500 scales: scale_shape.unwrap_or_default(),
501 biases: metadata.shape,
502 });
503 }
504 }
505 Ok(())
506 }
507}
508
509#[derive(Debug, Clone, Eq, PartialEq)]
511pub struct FusedSplitOutput {
512 pub target: String,
514 pub width: usize,
516}
517
518#[derive(Debug, Clone, Eq, PartialEq)]
523pub struct FusedSplitMember {
524 pub source: String,
526 pub axis: usize,
528 pub outputs: Vec<FusedSplitOutput>,
530}
531
532pub fn atomic_fused_split_recipes<C: RecipeCatalog + ?Sized>(
534 catalog: &C,
535 members: impl IntoIterator<Item = FusedSplitMember>,
536) -> Result<AtomicRecipeSet, RecipeError> {
537 let mut recipes = Vec::new();
538 for member in members {
539 if member.source.trim().is_empty() {
540 return Err(RecipeError::EmptySourceKey);
541 }
542 if member.outputs.is_empty()
543 || member
544 .outputs
545 .iter()
546 .any(|output| output.target.trim().is_empty() || output.width == 0)
547 {
548 return Err(RecipeError::InvalidFusedSplit {
549 tensor: member.source,
550 });
551 }
552 let metadata = catalog.tensor_metadata(&member.source)?;
553 let dimension = metadata.logical_shape.get(member.axis).copied().ok_or(
554 RecipeError::InvalidSelectionAxis {
555 axis: member.axis,
556 rank: metadata.logical_shape.len(),
557 },
558 )?;
559 let total = member.outputs.iter().try_fold(0usize, |total, output| {
560 total
561 .checked_add(output.width)
562 .ok_or(RecipeError::ArithmeticOverflow("fused split widths"))
563 })?;
564 if total != dimension {
565 return Err(RecipeError::FusedSplitWidthMismatch {
566 tensor: member.source,
567 axis: member.axis,
568 dimension,
569 outputs: total,
570 });
571 }
572 let mut start = 0usize;
573 for output in member.outputs {
574 let end = start + output.width;
575 recipes.push((
576 output.target,
577 DerivedWeightRecipe::source(
578 &member.source,
579 TensorSelection::Range {
580 axis: member.axis,
581 start,
582 end,
583 },
584 ),
585 ));
586 start = end;
587 }
588 }
589 AtomicRecipeSet::new(catalog, recipes)
590}
591
592pub fn ordered_axis_selection<C: RecipeCatalog + ?Sized>(
596 catalog: &C,
597 source: impl Into<String>,
598 axis: usize,
599 indices: Vec<usize>,
600) -> Result<DerivedWeightRecipe, RecipeError> {
601 let recipe = DerivedWeightRecipe::source(source, TensorSelection::Indices { axis, indices });
602 recipe.infer(catalog)?;
603 Ok(recipe)
604}
605
606impl RecipeMetadata {
607 pub fn shape(&self) -> &[usize] {
609 &self.shape
610 }
611
612 pub const fn dtype(&self) -> &RecipeDtype {
614 &self.dtype
615 }
616
617 pub const fn byte_len(&self) -> u64 {
619 self.byte_len
620 }
621}
622
623#[derive(Debug, Clone, Eq, PartialEq)]
625#[allow(missing_docs)]
626pub enum DerivedWeightRecipe {
627 Source {
628 key: String,
629 selection: TensorSelection,
630 },
631 Select {
632 input: Box<Self>,
633 selection: TensorSelection,
634 },
635 Concatenate {
636 axis: usize,
637 inputs: Vec<Self>,
638 },
639 Stack {
640 axis: usize,
641 inputs: Vec<Self>,
642 },
643 Reshape {
644 input: Box<Self>,
645 shape: Vec<usize>,
646 },
647 Transpose {
648 input: Box<Self>,
649 axes: Vec<usize>,
650 },
651 Cast {
652 input: Box<Self>,
653 dtype: RecipeDtype,
654 },
655 View {
656 input: Box<Self>,
657 dtype: RecipeDtype,
658 shape: Vec<usize>,
659 },
660 NegLog {
661 input: Box<Self>,
662 },
663 SubtractOne {
664 input: Box<Self>,
665 },
666}
667
668impl DerivedWeightRecipe {
669 pub fn source(key: impl Into<String>, selection: TensorSelection) -> Self {
671 Self::Source {
672 key: key.into(),
673 selection,
674 }
675 }
676
677 pub fn preflight_bounded<S: BoundedRecipeSource + ?Sized>(
679 &self,
680 source: &S,
681 ) -> Result<(), RecipeError> {
682 match self {
683 Self::Source { key, selection } => {
684 source.verify_bounded_source(key, selection.clone())?;
685 }
686 Self::Concatenate { inputs, .. } | Self::Stack { inputs, .. } => {
687 for input in inputs {
688 input.preflight_bounded(source)?;
689 }
690 }
691 Self::Select { input, .. }
692 | Self::Reshape { input, .. }
693 | Self::Transpose { input, .. }
694 | Self::Cast { input, .. }
695 | Self::View { input, .. }
696 | Self::NegLog { input }
697 | Self::SubtractOne { input } => input.preflight_bounded(source)?,
698 }
699 Ok(())
700 }
701
702 pub fn select_bounded<C: RecipeCatalog + ?Sized>(
704 &self,
705 catalog: &C,
706 selection: TensorSelection,
707 ) -> Result<Self, RecipeError> {
708 let metadata = self.infer(catalog)?;
709 let selection = normalize_selection(selection, &metadata.shape)?;
710 let expected_shape = selected_shape(metadata.shape.clone(), &selection)?;
711 let expanded = expand_indexed_sources(self.clone());
712 let rewritten = normalize_bounded_source_ranges(
713 expand_indexed_sources(push_selection(&expanded, catalog, selection)?),
714 catalog,
715 )?;
716 let actual = rewritten.infer(catalog)?;
717 if actual.shape != expected_shape || actual.dtype != metadata.dtype {
718 return Err(RecipeError::SelectionPushdownUnsupported {
719 operation: "recipe",
720 reason: format!(
721 "rewrite produced shape {:?} and dtype {:?}, expected {:?} and {:?}",
722 actual.shape, actual.dtype, expected_shape, metadata.dtype
723 ),
724 });
725 }
726 Ok(rewritten)
727 }
728
729 pub fn select_bounded_matrix_rows<C: RecipeCatalog + ?Sized>(
731 &self,
732 catalog: &C,
733 leading_index: usize,
734 start: usize,
735 end: usize,
736 ) -> Result<Self, RecipeError> {
737 let metadata = self.infer(catalog)?;
738 if metadata.shape.len() < 2 {
739 return Err(RecipeError::SelectionPushdownUnsupported {
740 operation: "matrix row selection",
741 reason: format!("rank {} has no matrix row axis", metadata.shape.len()),
742 });
743 }
744 let row_axis = metadata.shape.len() - 2;
745 let leading = usize::try_from(element_count(
746 &metadata.shape[..row_axis],
747 "leading matrix dimensions",
748 )?)
749 .map_err(|_| RecipeError::ArithmeticOverflow("leading matrix dimensions"))?;
750 if leading_index >= leading {
751 return Err(RecipeError::InvalidIndices {
752 axis: 0,
753 dimension: leading,
754 });
755 }
756 let mut coordinates = vec![0usize; row_axis];
757 let mut remainder = leading_index;
758 for axis in (0..row_axis).rev() {
759 let dimension = metadata.shape[axis];
760 coordinates[axis] = remainder % dimension;
761 remainder /= dimension;
762 }
763 let mut selected = self.clone();
764 for (axis, coordinate) in coordinates.into_iter().enumerate() {
765 selected = selected.select_bounded(
766 catalog,
767 TensorSelection::Range {
768 axis,
769 start: coordinate,
770 end: coordinate + 1,
771 },
772 )?;
773 }
774 selected.select_bounded(
775 catalog,
776 TensorSelection::Range {
777 axis: row_axis,
778 start,
779 end,
780 },
781 )
782 }
783
784 pub fn peak_materialization_bytes<C: RecipeCatalog + ?Sized>(
786 &self,
787 catalog: &C,
788 ) -> Result<u64, RecipeError> {
789 let output_bytes = self.infer(catalog)?.byte_len();
790 match self {
791 Self::Source { .. } => Ok(output_bytes),
792 Self::Select { input, .. }
793 | Self::Reshape { input, .. }
794 | Self::Transpose { input, .. }
795 | Self::Cast { input, .. }
796 | Self::View { input, .. }
797 | Self::NegLog { input }
798 | Self::SubtractOne { input } => {
799 let input_bytes = input.infer(catalog)?.byte_len();
800 let child_peak = input.peak_materialization_bytes(catalog)?;
801 Ok(child_peak.max(input_bytes.checked_add(output_bytes).ok_or(
802 RecipeError::ArithmeticOverflow("unary recipe peak materialization bytes"),
803 )?))
804 }
805 Self::Concatenate { inputs, .. } | Self::Stack { inputs, .. } => {
806 let mut retained = 0u64;
807 let mut peak = 0u64;
808 for input in inputs {
809 let child_peak = input.peak_materialization_bytes(catalog)?;
810 peak = peak.max(retained.checked_add(child_peak).ok_or(
811 RecipeError::ArithmeticOverflow("joined recipe child peak bytes"),
812 )?);
813 retained = retained
814 .checked_add(input.infer(catalog)?.byte_len())
815 .ok_or(RecipeError::ArithmeticOverflow(
816 "joined recipe retained input bytes",
817 ))?;
818 }
819 Ok(peak.max(retained.checked_add(output_bytes).ok_or(
820 RecipeError::ArithmeticOverflow("joined recipe output peak bytes"),
821 )?))
822 }
823 }
824 }
825
826 pub fn source_keys(&self) -> Vec<&str> {
828 let mut keys = BTreeSet::new();
829 self.collect_source_keys(&mut keys);
830 keys.into_iter().collect()
831 }
832
833 pub fn source_occurrences(&self) -> Vec<&str> {
839 let mut occurrences = Vec::new();
840 self.collect_source_occurrences(&mut occurrences);
841 occurrences
842 }
843
844 fn collect_source_keys<'a>(&'a self, keys: &mut BTreeSet<&'a str>) {
845 match self {
846 Self::Source { key, .. } => {
847 keys.insert(key);
848 }
849 Self::Concatenate { inputs, .. } | Self::Stack { inputs, .. } => {
850 for input in inputs {
851 input.collect_source_keys(keys);
852 }
853 }
854 Self::Select { input, .. }
855 | Self::Reshape { input, .. }
856 | Self::Transpose { input, .. }
857 | Self::Cast { input, .. }
858 | Self::View { input, .. }
859 | Self::NegLog { input }
860 | Self::SubtractOne { input } => input.collect_source_keys(keys),
861 }
862 }
863
864 fn collect_source_occurrences<'a>(&'a self, occurrences: &mut Vec<&'a str>) {
865 match self {
866 Self::Source { key, .. } => occurrences.push(key),
867 Self::Concatenate { inputs, .. } | Self::Stack { inputs, .. } => {
868 for input in inputs {
869 input.collect_source_occurrences(occurrences);
870 }
871 }
872 Self::Select { input, .. }
873 | Self::Reshape { input, .. }
874 | Self::Transpose { input, .. }
875 | Self::Cast { input, .. }
876 | Self::View { input, .. }
877 | Self::NegLog { input }
878 | Self::SubtractOne { input } => input.collect_source_occurrences(occurrences),
879 }
880 }
881
882 pub fn infer<C: RecipeCatalog + ?Sized>(
884 &self,
885 catalog: &C,
886 ) -> Result<RecipeMetadata, RecipeError> {
887 match self {
888 Self::Source { key, selection } => {
889 if key.trim().is_empty() {
890 return Err(RecipeError::EmptySourceKey);
891 }
892 let metadata = catalog.tensor_metadata(key)?;
893 metadata_for(
894 selected_shape(metadata.logical_shape, selection)?,
895 metadata.stored_dtype.into(),
896 )
897 }
898 Self::Select { input, selection } => {
899 let metadata = input.infer(catalog)?;
900 metadata_for(selected_shape(metadata.shape, selection)?, metadata.dtype)
901 }
902 Self::Concatenate { axis, inputs } => infer_join(catalog, *axis, inputs, false),
903 Self::Stack { axis, inputs } => infer_join(catalog, *axis, inputs, true),
904 Self::Reshape { input, shape } => {
905 let metadata = input.infer(catalog)?;
906 let old_count = element_count(&metadata.shape, "reshape input")?;
907 let new_count = element_count(shape, "reshape output")?;
908 if old_count != new_count {
909 return Err(RecipeError::ElementCountMismatch {
910 input: old_count,
911 output: new_count,
912 });
913 }
914 metadata_for(shape.clone(), metadata.dtype)
915 }
916 Self::Transpose { input, axes } => {
917 let metadata = input.infer(catalog)?;
918 let unique = axes.iter().copied().collect::<BTreeSet<_>>();
919 if axes.len() != metadata.shape.len()
920 || unique.len() != axes.len()
921 || axes.iter().any(|axis| *axis >= axes.len())
922 {
923 return Err(RecipeError::InvalidPermutation {
924 axes: axes.clone(),
925 rank: metadata.shape.len(),
926 });
927 }
928 metadata_for(
929 axes.iter().map(|axis| metadata.shape[*axis]).collect(),
930 metadata.dtype,
931 )
932 }
933 Self::Cast { input, dtype } => metadata_for(input.infer(catalog)?.shape, dtype.clone()),
934 Self::View {
935 input,
936 dtype,
937 shape,
938 } => {
939 let input = input.infer(catalog)?;
940 let output = metadata_for(shape.clone(), dtype.clone())?;
941 if input.byte_len != output.byte_len {
942 return Err(RecipeError::ByteCountMismatch {
943 input: input.byte_len,
944 output: output.byte_len,
945 });
946 }
947 Ok(output)
948 }
949 Self::NegLog { input } | Self::SubtractOne { input } => input.infer(catalog),
950 }
951 }
952}
953
954fn selected_shape(
955 mut shape: Vec<usize>,
956 selection: &TensorSelection,
957) -> Result<Vec<usize>, RecipeError> {
958 match selection {
959 TensorSelection::Full => {}
960 TensorSelection::Range { axis, start, end } => {
961 let rank = shape.len();
962 let dimension = shape
963 .get_mut(*axis)
964 .ok_or(RecipeError::InvalidSelectionAxis { axis: *axis, rank })?;
965 if start >= end || *end > *dimension {
966 return Err(RecipeError::InvalidRange {
967 axis: *axis,
968 start: *start,
969 end: *end,
970 dimension: *dimension,
971 });
972 }
973 *dimension = end - start;
974 }
975 TensorSelection::Indices { axis, indices } => {
976 let rank = shape.len();
977 let dimension = shape
978 .get_mut(*axis)
979 .ok_or(RecipeError::InvalidSelectionAxis { axis: *axis, rank })?;
980 if indices.is_empty() || indices.iter().any(|index| *index >= *dimension) {
981 return Err(RecipeError::InvalidIndices {
982 axis: *axis,
983 dimension: *dimension,
984 });
985 }
986 *dimension = indices.len();
987 }
988 TensorSelection::Contiguous {
989 offset_elements,
990 shape: selected,
991 } => {
992 if selected.is_empty() || selected.contains(&0) {
993 return Err(RecipeError::InvalidContiguousSelection);
994 }
995 let full = element_count(&shape, "contiguous source")?;
996 let count = element_count(selected, "contiguous selection")?;
997 let end = u64::try_from(*offset_elements)
998 .map_err(|_| RecipeError::ArithmeticOverflow("contiguous offset"))?
999 .checked_add(count)
1000 .ok_or(RecipeError::ArithmeticOverflow("contiguous end"))?;
1001 if end > full {
1002 return Err(RecipeError::InvalidContiguousSelection);
1003 }
1004 shape = selected.clone();
1005 }
1006 }
1007 Ok(shape)
1008}
1009
1010fn expand_indexed_sources(recipe: DerivedWeightRecipe) -> DerivedWeightRecipe {
1011 match recipe {
1012 DerivedWeightRecipe::Source {
1013 key,
1014 selection: TensorSelection::Indices { axis, indices },
1015 } => {
1016 let mut runs = Vec::<(usize, usize)>::new();
1017 for index in indices {
1018 if let Some((_, end)) = runs.last_mut() {
1019 if *end == index {
1020 *end += 1;
1021 continue;
1022 }
1023 }
1024 runs.push((index, index + 1));
1025 }
1026 let mut inputs = runs
1027 .into_iter()
1028 .map(|(start, end)| {
1029 DerivedWeightRecipe::source(
1030 key.clone(),
1031 TensorSelection::Range { axis, start, end },
1032 )
1033 })
1034 .collect::<Vec<_>>();
1035 if inputs.len() == 1 {
1036 inputs.pop().unwrap()
1037 } else {
1038 DerivedWeightRecipe::Concatenate { axis, inputs }
1039 }
1040 }
1041 DerivedWeightRecipe::Source { .. } => recipe,
1042 DerivedWeightRecipe::Select { input, selection } => DerivedWeightRecipe::Select {
1043 input: Box::new(expand_indexed_sources(*input)),
1044 selection,
1045 },
1046 DerivedWeightRecipe::Concatenate { axis, inputs } => DerivedWeightRecipe::Concatenate {
1047 axis,
1048 inputs: inputs.into_iter().map(expand_indexed_sources).collect(),
1049 },
1050 DerivedWeightRecipe::Stack { axis, inputs } => DerivedWeightRecipe::Stack {
1051 axis,
1052 inputs: inputs.into_iter().map(expand_indexed_sources).collect(),
1053 },
1054 DerivedWeightRecipe::Reshape { input, shape } => DerivedWeightRecipe::Reshape {
1055 input: Box::new(expand_indexed_sources(*input)),
1056 shape,
1057 },
1058 DerivedWeightRecipe::Transpose { input, axes } => DerivedWeightRecipe::Transpose {
1059 input: Box::new(expand_indexed_sources(*input)),
1060 axes,
1061 },
1062 DerivedWeightRecipe::Cast { input, dtype } => DerivedWeightRecipe::Cast {
1063 input: Box::new(expand_indexed_sources(*input)),
1064 dtype,
1065 },
1066 DerivedWeightRecipe::View {
1067 input,
1068 dtype,
1069 shape,
1070 } => DerivedWeightRecipe::View {
1071 input: Box::new(expand_indexed_sources(*input)),
1072 dtype,
1073 shape,
1074 },
1075 DerivedWeightRecipe::NegLog { input } => DerivedWeightRecipe::NegLog {
1076 input: Box::new(expand_indexed_sources(*input)),
1077 },
1078 DerivedWeightRecipe::SubtractOne { input } => DerivedWeightRecipe::SubtractOne {
1079 input: Box::new(expand_indexed_sources(*input)),
1080 },
1081 }
1082}
1083
1084fn normalize_bounded_source_ranges<C: RecipeCatalog + ?Sized>(
1085 recipe: DerivedWeightRecipe,
1086 store: &C,
1087) -> Result<DerivedWeightRecipe, RecipeError> {
1088 Ok(match recipe {
1089 DerivedWeightRecipe::Source {
1090 key,
1091 selection: TensorSelection::Range { axis, start, end },
1092 } if axis > 0 => {
1093 let shape = store.tensor_metadata(&key)?.logical_shape;
1094 if shape[..axis].iter().product::<usize>() == 1 {
1095 let trailing = shape[axis + 1..]
1096 .iter()
1097 .try_fold(1usize, |count, dimension| {
1098 count
1099 .checked_mul(*dimension)
1100 .ok_or(RecipeError::ArithmeticOverflow(
1101 "bounded source range trailing span",
1102 ))
1103 })?;
1104 let offset_elements =
1105 start
1106 .checked_mul(trailing)
1107 .ok_or(RecipeError::ArithmeticOverflow(
1108 "bounded source range offset",
1109 ))?;
1110 let mut selected_shape = shape;
1111 selected_shape[axis] = end - start;
1112 DerivedWeightRecipe::source(
1113 key,
1114 TensorSelection::Contiguous {
1115 offset_elements,
1116 shape: selected_shape,
1117 },
1118 )
1119 } else {
1120 DerivedWeightRecipe::source(key, TensorSelection::Range { axis, start, end })
1121 }
1122 }
1123 DerivedWeightRecipe::Source { .. } => recipe,
1124 DerivedWeightRecipe::Select { input, selection } => DerivedWeightRecipe::Select {
1125 input: Box::new(normalize_bounded_source_ranges(*input, store)?),
1126 selection,
1127 },
1128 DerivedWeightRecipe::Concatenate { axis, inputs } => DerivedWeightRecipe::Concatenate {
1129 axis,
1130 inputs: inputs
1131 .into_iter()
1132 .map(|input| normalize_bounded_source_ranges(input, store))
1133 .collect::<Result<Vec<_>, _>>()?,
1134 },
1135 DerivedWeightRecipe::Stack { axis, inputs } => DerivedWeightRecipe::Stack {
1136 axis,
1137 inputs: inputs
1138 .into_iter()
1139 .map(|input| normalize_bounded_source_ranges(input, store))
1140 .collect::<Result<Vec<_>, _>>()?,
1141 },
1142 DerivedWeightRecipe::Reshape { input, shape } => DerivedWeightRecipe::Reshape {
1143 input: Box::new(normalize_bounded_source_ranges(*input, store)?),
1144 shape,
1145 },
1146 DerivedWeightRecipe::Transpose { input, axes } => DerivedWeightRecipe::Transpose {
1147 input: Box::new(normalize_bounded_source_ranges(*input, store)?),
1148 axes,
1149 },
1150 DerivedWeightRecipe::Cast { input, dtype } => DerivedWeightRecipe::Cast {
1151 input: Box::new(normalize_bounded_source_ranges(*input, store)?),
1152 dtype,
1153 },
1154 DerivedWeightRecipe::View {
1155 input,
1156 dtype,
1157 shape,
1158 } => DerivedWeightRecipe::View {
1159 input: Box::new(normalize_bounded_source_ranges(*input, store)?),
1160 dtype,
1161 shape,
1162 },
1163 DerivedWeightRecipe::NegLog { input } => DerivedWeightRecipe::NegLog {
1164 input: Box::new(normalize_bounded_source_ranges(*input, store)?),
1165 },
1166 DerivedWeightRecipe::SubtractOne { input } => DerivedWeightRecipe::SubtractOne {
1167 input: Box::new(normalize_bounded_source_ranges(*input, store)?),
1168 },
1169 })
1170}
1171
1172fn push_selection<C: RecipeCatalog + ?Sized>(
1173 recipe: &DerivedWeightRecipe,
1174 store: &C,
1175 selection: TensorSelection,
1176) -> Result<DerivedWeightRecipe, RecipeError> {
1177 if matches!(selection, TensorSelection::Full) {
1178 return Ok(recipe.clone());
1179 }
1180 match recipe {
1181 DerivedWeightRecipe::Source {
1182 key,
1183 selection: source_selection,
1184 } => {
1185 let source_shape = store.tensor_metadata(key)?.logical_shape;
1186 let source_selection = normalize_selection(source_selection.clone(), &source_shape)?;
1187 let selected_source_shape = selected_shape(source_shape.clone(), &source_selection)?;
1188 let selection = normalize_selection(selection, &selected_source_shape)?;
1189 if matches!(selection, TensorSelection::Full) {
1190 return Ok(DerivedWeightRecipe::source(key.clone(), source_selection));
1191 }
1192 if matches!(source_selection, TensorSelection::Full) {
1193 return Ok(DerivedWeightRecipe::source(key.clone(), selection));
1194 }
1195 if let Some(selection) = select_from_contiguous_span(&source_selection, &selection)? {
1196 return Ok(DerivedWeightRecipe::source(key.clone(), selection));
1197 }
1198 if selection_axis(&source_selection) == selection_axis(&selection) {
1199 return Ok(DerivedWeightRecipe::source(
1200 key.clone(),
1201 compose_same_axis_selection(&source_selection, &selection)?,
1202 ));
1203 }
1204 if let Some(contiguous) =
1205 combine_independent_ranges(&source_shape, &source_selection, &selection)?
1206 {
1207 return Ok(DerivedWeightRecipe::source(key.clone(), contiguous));
1208 }
1209 Ok(DerivedWeightRecipe::Select {
1210 input: Box::new(DerivedWeightRecipe::source(key.clone(), source_selection)),
1211 selection,
1212 })
1213 }
1214 DerivedWeightRecipe::Select {
1215 input,
1216 selection: existing,
1217 } => {
1218 if matches!(existing, TensorSelection::Full) {
1219 return push_selection(input, store, selection);
1220 }
1221 if selection_axis(existing) == selection_axis(&selection) {
1222 return push_selection(
1223 input,
1224 store,
1225 compose_same_axis_selection(existing, &selection)?,
1226 );
1227 }
1228 let selected_input = push_selection(input, store, selection)?;
1233 push_selection(&selected_input, store, existing.clone())
1234 }
1235 DerivedWeightRecipe::Concatenate { axis, inputs } => {
1236 push_concatenate_selection(*axis, inputs, store, selection)
1237 }
1238 DerivedWeightRecipe::Stack { axis, inputs } => {
1239 push_stack_selection(*axis, inputs, store, selection)
1240 }
1241 DerivedWeightRecipe::Reshape { input, shape } => {
1242 let input_metadata = input.infer(store)?;
1243 let output_metadata = recipe.infer(store)?;
1244 let input_selection = map_reinterpret_selection(
1245 &input_metadata,
1246 &output_metadata,
1247 &selection,
1248 "reshape",
1249 )?;
1250 Ok(DerivedWeightRecipe::Reshape {
1251 input: Box::new(push_selection(input, store, input_selection)?),
1252 shape: selected_shape(shape.clone(), &selection)?,
1253 })
1254 }
1255 DerivedWeightRecipe::Transpose { input, axes } => {
1256 let output_axis = selection_axis(&selection).expect("non-full selection");
1257 let input_axis = *axes
1258 .get(output_axis)
1259 .ok_or(RecipeError::InvalidSelectionAxis {
1260 axis: output_axis,
1261 rank: axes.len(),
1262 })?;
1263 Ok(DerivedWeightRecipe::Transpose {
1264 input: Box::new(push_selection(
1265 input,
1266 store,
1267 selection_with_axis(selection, input_axis),
1268 )?),
1269 axes: axes.clone(),
1270 })
1271 }
1272 DerivedWeightRecipe::Cast { input, dtype } => Ok(DerivedWeightRecipe::Cast {
1273 input: Box::new(push_selection(input, store, selection)?),
1274 dtype: dtype.clone(),
1275 }),
1276 DerivedWeightRecipe::View {
1277 input,
1278 dtype,
1279 shape,
1280 } => {
1281 let input_metadata = input.infer(store)?;
1282 let output_metadata = recipe.infer(store)?;
1283 let input_selection =
1284 map_reinterpret_selection(&input_metadata, &output_metadata, &selection, "view")?;
1285 Ok(DerivedWeightRecipe::View {
1286 input: Box::new(push_selection(input, store, input_selection)?),
1287 dtype: dtype.clone(),
1288 shape: selected_shape(shape.clone(), &selection)?,
1289 })
1290 }
1291 DerivedWeightRecipe::NegLog { input } => Ok(DerivedWeightRecipe::NegLog {
1292 input: Box::new(push_selection(input, store, selection)?),
1293 }),
1294 DerivedWeightRecipe::SubtractOne { input } => Ok(DerivedWeightRecipe::SubtractOne {
1295 input: Box::new(push_selection(input, store, selection)?),
1296 }),
1297 }
1298}
1299
1300fn push_concatenate_selection<C: RecipeCatalog + ?Sized>(
1301 axis: usize,
1302 inputs: &[DerivedWeightRecipe],
1303 store: &C,
1304 selection: TensorSelection,
1305) -> Result<DerivedWeightRecipe, RecipeError> {
1306 let selected_axis = selection_axis(&selection).expect("non-full selection");
1307 if selected_axis != axis {
1308 let inputs = inputs
1309 .iter()
1310 .map(|input| push_selection(input, store, selection.clone()))
1311 .collect::<Result<Vec<_>, _>>()?;
1312 return Ok(DerivedWeightRecipe::Concatenate { axis, inputs });
1313 }
1314 let metadata = inputs
1315 .iter()
1316 .map(|input| input.infer(store))
1317 .collect::<Result<Vec<_>, _>>()?;
1318 let dimensions = metadata
1319 .iter()
1320 .map(|item| item.shape[axis])
1321 .collect::<Vec<_>>();
1322 let mut rewritten = Vec::new();
1323 match selection {
1324 TensorSelection::Range { start, end, .. } => {
1325 let mut offset = 0usize;
1326 for (input, dimension) in inputs.iter().zip(dimensions) {
1327 let child_end =
1328 offset
1329 .checked_add(dimension)
1330 .ok_or(RecipeError::ArithmeticOverflow(
1331 "concatenate selection offset",
1332 ))?;
1333 let overlap_start = start.max(offset);
1334 let overlap_end = end.min(child_end);
1335 if overlap_start < overlap_end {
1336 let child_selection = normalize_selection(
1337 TensorSelection::Range {
1338 axis,
1339 start: overlap_start - offset,
1340 end: overlap_end - offset,
1341 },
1342 &input.infer(store)?.shape,
1343 )?;
1344 rewritten.push(push_selection(input, store, child_selection)?);
1345 }
1346 offset = child_end;
1347 }
1348 }
1349 TensorSelection::Indices { indices, .. } => {
1350 let mut offsets = Vec::with_capacity(dimensions.len() + 1);
1351 offsets.push(0usize);
1352 for dimension in dimensions {
1353 let next = offsets
1354 .last()
1355 .copied()
1356 .unwrap()
1357 .checked_add(dimension)
1358 .ok_or(RecipeError::ArithmeticOverflow(
1359 "concatenate selection offset",
1360 ))?;
1361 offsets.push(next);
1362 }
1363 let mut runs = Vec::<(usize, Vec<usize>)>::new();
1364 for index in indices {
1365 let child = offsets
1366 .windows(2)
1367 .position(|bounds| index >= bounds[0] && index < bounds[1])
1368 .ok_or(RecipeError::InvalidIndices {
1369 axis,
1370 dimension: *offsets.last().unwrap(),
1371 })?;
1372 let local = index - offsets[child];
1373 if let Some((last_child, local_indices)) = runs.last_mut() {
1374 if *last_child == child {
1375 local_indices.push(local);
1376 continue;
1377 }
1378 }
1379 runs.push((child, vec![local]));
1380 }
1381 for (child, indices) in runs {
1382 rewritten.push(push_selection(
1383 &inputs[child],
1384 store,
1385 TensorSelection::Indices { axis, indices },
1386 )?);
1387 }
1388 }
1389 TensorSelection::Full => unreachable!(),
1390 TensorSelection::Contiguous { .. } => {
1391 return Err(RecipeError::SelectionPushdownUnsupported {
1392 operation: "concatenate",
1393 reason: "a storage-contiguous span has no concatenate-axis semantics".into(),
1394 })
1395 }
1396 }
1397 match rewritten.len() {
1398 0 => Err(RecipeError::SelectionPushdownUnsupported {
1399 operation: "concatenate",
1400 reason: "selection did not intersect any child".into(),
1401 }),
1402 1 => Ok(rewritten.pop().unwrap()),
1403 _ => Ok(DerivedWeightRecipe::Concatenate {
1404 axis,
1405 inputs: rewritten,
1406 }),
1407 }
1408}
1409
1410fn push_stack_selection<C: RecipeCatalog + ?Sized>(
1411 axis: usize,
1412 inputs: &[DerivedWeightRecipe],
1413 store: &C,
1414 selection: TensorSelection,
1415) -> Result<DerivedWeightRecipe, RecipeError> {
1416 let selected_axis = selection_axis(&selection).expect("non-full selection");
1417 if selected_axis == axis {
1418 let selected = match selection {
1419 TensorSelection::Range { start, end, .. } => inputs[start..end].to_vec(),
1420 TensorSelection::Indices { indices, .. } => indices
1421 .into_iter()
1422 .map(|index| inputs[index].clone())
1423 .collect(),
1424 TensorSelection::Full => unreachable!(),
1425 TensorSelection::Contiguous { .. } => {
1426 return Err(RecipeError::SelectionPushdownUnsupported {
1427 operation: "stack",
1428 reason: "a storage-contiguous span has no stack-axis semantics".into(),
1429 })
1430 }
1431 };
1432 return Ok(DerivedWeightRecipe::Stack {
1433 axis,
1434 inputs: selected,
1435 });
1436 }
1437 let input_axis = if selected_axis < axis {
1438 selected_axis
1439 } else {
1440 selected_axis - 1
1441 };
1442 let input_selection = selection_with_axis(selection, input_axis);
1443 let inputs = inputs
1444 .iter()
1445 .map(|input| push_selection(input, store, input_selection.clone()))
1446 .collect::<Result<Vec<_>, _>>()?;
1447 Ok(DerivedWeightRecipe::Stack { axis, inputs })
1448}
1449
1450fn map_reinterpret_selection(
1451 input: &RecipeMetadata,
1452 output: &RecipeMetadata,
1453 selection: &TensorSelection,
1454 operation: &'static str,
1455) -> Result<TensorSelection, RecipeError> {
1456 let output_axis = selection_axis(selection).expect("non-full selection");
1457 let output_unit = axis_unit_bytes(&output.shape, output.dtype.bit_width()?, output_axis)?;
1458 let output_cycle = output_unit
1459 .checked_mul(output.shape[output_axis] as u64)
1460 .ok_or(RecipeError::ArithmeticOverflow("selection output cycle"))?;
1461 for input_axis in 0..input.shape.len() {
1462 let input_unit = axis_unit_bytes(&input.shape, input.dtype.bit_width()?, input_axis)?;
1463 let input_cycle = input_unit
1464 .checked_mul(input.shape[input_axis] as u64)
1465 .ok_or(RecipeError::ArithmeticOverflow("selection input cycle"))?;
1466 if input_cycle != output_cycle {
1467 continue;
1468 }
1469 if let Some(mapped) = map_selection_units(
1470 selection,
1471 input_axis,
1472 input.shape[input_axis],
1473 output_unit,
1474 input_unit,
1475 )? {
1476 return normalize_selection(mapped, &input.shape);
1477 }
1478 }
1479 Err(RecipeError::SelectionPushdownUnsupported {
1480 operation,
1481 reason: format!(
1482 "axis {output_axis} selection cannot be expressed as a single-axis bounded selection from shape {:?} to {:?}",
1483 input.shape, output.shape
1484 ),
1485 })
1486}
1487
1488fn map_selection_units(
1489 selection: &TensorSelection,
1490 input_axis: usize,
1491 input_dimension: usize,
1492 output_unit: u64,
1493 input_unit: u64,
1494) -> Result<Option<TensorSelection>, RecipeError> {
1495 let map_interval = |start: usize, end: usize| -> Result<Option<(usize, usize)>, RecipeError> {
1496 let start_bytes = (start as u64)
1497 .checked_mul(output_unit)
1498 .ok_or(RecipeError::ArithmeticOverflow("selection interval start"))?;
1499 let end_bytes = (end as u64)
1500 .checked_mul(output_unit)
1501 .ok_or(RecipeError::ArithmeticOverflow("selection interval end"))?;
1502 if start_bytes % input_unit != 0 || end_bytes % input_unit != 0 {
1503 return Ok(None);
1504 }
1505 let start = usize::try_from(start_bytes / input_unit)
1506 .map_err(|_| RecipeError::ArithmeticOverflow("mapped selection start"))?;
1507 let end = usize::try_from(end_bytes / input_unit)
1508 .map_err(|_| RecipeError::ArithmeticOverflow("mapped selection end"))?;
1509 Ok((end <= input_dimension).then_some((start, end)))
1510 };
1511 match selection {
1512 TensorSelection::Range { start, end, .. } => Ok(map_interval(*start, *end)?.map(
1513 |(start, end)| TensorSelection::Range {
1514 axis: input_axis,
1515 start,
1516 end,
1517 },
1518 )),
1519 TensorSelection::Indices { indices, .. } => {
1520 let mut mapped = Vec::new();
1521 let mut run_start = indices[0];
1522 let mut run_end = run_start + 1;
1523 for index in indices.iter().copied().skip(1) {
1524 if index == run_end {
1525 run_end += 1;
1526 continue;
1527 }
1528 let Some((start, end)) = map_interval(run_start, run_end)? else {
1529 return Ok(None);
1530 };
1531 mapped.extend(start..end);
1532 run_start = index;
1533 run_end = index + 1;
1534 }
1535 let Some((start, end)) = map_interval(run_start, run_end)? else {
1536 return Ok(None);
1537 };
1538 mapped.extend(start..end);
1539 Ok(Some(TensorSelection::Indices {
1540 axis: input_axis,
1541 indices: mapped,
1542 }))
1543 }
1544 TensorSelection::Full => Ok(Some(TensorSelection::Full)),
1545 TensorSelection::Contiguous { .. } => Ok(None),
1546 }
1547}
1548
1549fn axis_unit_bytes(shape: &[usize], dtype_width: u64, axis: usize) -> Result<u64, RecipeError> {
1550 shape[axis + 1..]
1551 .iter()
1552 .try_fold(dtype_width, |bytes, dimension| {
1553 bytes
1554 .checked_mul(*dimension as u64)
1555 .ok_or(RecipeError::ArithmeticOverflow("selection axis unit"))
1556 })
1557}
1558
1559fn selection_axis(selection: &TensorSelection) -> Option<usize> {
1560 match selection {
1561 TensorSelection::Full => None,
1562 TensorSelection::Range { axis, .. } | TensorSelection::Indices { axis, .. } => Some(*axis),
1563 TensorSelection::Contiguous { .. } => None,
1564 }
1565}
1566
1567fn selection_with_axis(selection: TensorSelection, axis: usize) -> TensorSelection {
1568 match selection {
1569 TensorSelection::Full => TensorSelection::Full,
1570 TensorSelection::Range { start, end, .. } => TensorSelection::Range { axis, start, end },
1571 TensorSelection::Indices { indices, .. } => TensorSelection::Indices { axis, indices },
1572 selection @ TensorSelection::Contiguous { .. } => selection,
1573 }
1574}
1575
1576fn normalize_selection(
1577 selection: TensorSelection,
1578 shape: &[usize],
1579) -> Result<TensorSelection, RecipeError> {
1580 selected_shape(shape.to_vec(), &selection)?;
1581 match selection {
1582 TensorSelection::Range {
1583 axis,
1584 start: 0,
1585 end,
1586 } if end == shape[axis] => Ok(TensorSelection::Full),
1587 TensorSelection::Indices { axis, indices }
1588 if indices.windows(2).all(|pair| pair[1] == pair[0] + 1) =>
1589 {
1590 let start = indices[0];
1591 let end = indices[indices.len() - 1] + 1;
1592 if start == 0 && end == shape[axis] {
1593 Ok(TensorSelection::Full)
1594 } else {
1595 Ok(TensorSelection::Range { axis, start, end })
1596 }
1597 }
1598 selection => Ok(selection),
1599 }
1600}
1601
1602fn compose_same_axis_selection(
1603 existing: &TensorSelection,
1604 requested: &TensorSelection,
1605) -> Result<TensorSelection, RecipeError> {
1606 debug_assert_eq!(selection_axis(existing), selection_axis(requested));
1607 let axis = selection_axis(existing).expect("non-full selections");
1608 match (existing, requested) {
1609 (
1610 TensorSelection::Range { start, .. },
1611 TensorSelection::Range {
1612 start: requested_start,
1613 end: requested_end,
1614 ..
1615 },
1616 ) => Ok(TensorSelection::Range {
1617 axis,
1618 start: start + requested_start,
1619 end: start + requested_end,
1620 }),
1621 (TensorSelection::Range { start, .. }, TensorSelection::Indices { indices, .. }) => {
1622 Ok(TensorSelection::Indices {
1623 axis,
1624 indices: indices.iter().map(|index| start + index).collect(),
1625 })
1626 }
1627 (TensorSelection::Indices { indices, .. }, TensorSelection::Range { start, end, .. }) => {
1628 Ok(TensorSelection::Indices {
1629 axis,
1630 indices: indices[*start..*end].to_vec(),
1631 })
1632 }
1633 (
1634 TensorSelection::Indices {
1635 indices: existing, ..
1636 },
1637 TensorSelection::Indices { indices, .. },
1638 ) => Ok(TensorSelection::Indices {
1639 axis,
1640 indices: indices.iter().map(|index| existing[*index]).collect(),
1641 }),
1642 _ => Err(RecipeError::SelectionPushdownUnsupported {
1643 operation: "selection composition",
1644 reason: "full selections must be normalized before composition".into(),
1645 }),
1646 }
1647}
1648
1649fn combine_independent_ranges(
1650 source_shape: &[usize],
1651 existing: &TensorSelection,
1652 requested: &TensorSelection,
1653) -> Result<Option<TensorSelection>, RecipeError> {
1654 let (
1655 TensorSelection::Range {
1656 axis: existing_axis,
1657 start: existing_start,
1658 end: existing_end,
1659 },
1660 TensorSelection::Range {
1661 axis: requested_axis,
1662 start: requested_start,
1663 end: requested_end,
1664 },
1665 ) = (existing, requested)
1666 else {
1667 return Ok(None);
1668 };
1669 if existing_axis == requested_axis {
1670 return Ok(None);
1671 }
1672 let mut starts = vec![0usize; source_shape.len()];
1673 let mut ends = source_shape.to_vec();
1674 starts[*existing_axis] = *existing_start;
1675 ends[*existing_axis] = *existing_end;
1676 starts[*requested_axis] = *requested_start;
1677 ends[*requested_axis] = *requested_end;
1678 let selected_shape = starts
1679 .iter()
1680 .zip(&ends)
1681 .map(|(start, end)| end - start)
1682 .collect::<Vec<_>>();
1683 let Some(last_partial) = (0..source_shape.len())
1684 .rev()
1685 .find(|axis| starts[*axis] != 0 || ends[*axis] != source_shape[*axis])
1686 else {
1687 return Ok(Some(TensorSelection::Full));
1688 };
1689 if selected_shape[..last_partial]
1690 .iter()
1691 .any(|dimension| *dimension != 1)
1692 || (last_partial + 1..source_shape.len())
1693 .any(|axis| starts[axis] != 0 || ends[axis] != source_shape[axis])
1694 {
1695 return Ok(None);
1696 }
1697 let mut offset_elements = 0usize;
1698 let mut stride = 1usize;
1699 for axis in (0..source_shape.len()).rev() {
1700 offset_elements = offset_elements
1701 .checked_add(starts[axis].checked_mul(stride).ok_or(
1702 RecipeError::ArithmeticOverflow("contiguous selection offset"),
1703 )?)
1704 .ok_or(RecipeError::ArithmeticOverflow(
1705 "contiguous selection offset",
1706 ))?;
1707 stride = stride
1708 .checked_mul(source_shape[axis])
1709 .ok_or(RecipeError::ArithmeticOverflow(
1710 "contiguous selection stride",
1711 ))?;
1712 }
1713 Ok(Some(TensorSelection::Contiguous {
1714 offset_elements,
1715 shape: selected_shape,
1716 }))
1717}
1718
1719fn select_from_contiguous_span(
1720 existing: &TensorSelection,
1721 requested: &TensorSelection,
1722) -> Result<Option<TensorSelection>, RecipeError> {
1723 let TensorSelection::Contiguous {
1724 offset_elements,
1725 shape,
1726 } = existing
1727 else {
1728 return Ok(None);
1729 };
1730 let (axis, start, end) = match requested {
1731 TensorSelection::Range { axis, start, end } => (*axis, *start, *end),
1732 TensorSelection::Indices { axis, indices }
1733 if indices.windows(2).all(|pair| pair[1] == pair[0] + 1) =>
1734 {
1735 (*axis, indices[0], indices[indices.len() - 1] + 1)
1736 }
1737 _ => return Ok(None),
1738 };
1739 if shape[..axis].iter().product::<usize>() != 1 {
1740 return Ok(None);
1741 }
1742 let trailing = shape[axis + 1..]
1743 .iter()
1744 .try_fold(1usize, |count, dimension| {
1745 count
1746 .checked_mul(*dimension)
1747 .ok_or(RecipeError::ArithmeticOverflow(
1748 "contiguous selection trailing span",
1749 ))
1750 })?;
1751 let offset_elements = offset_elements
1752 .checked_add(
1753 start
1754 .checked_mul(trailing)
1755 .ok_or(RecipeError::ArithmeticOverflow(
1756 "contiguous selection offset",
1757 ))?,
1758 )
1759 .ok_or(RecipeError::ArithmeticOverflow(
1760 "contiguous selection offset",
1761 ))?;
1762 let mut selected_shape = shape.clone();
1763 selected_shape[axis] = end - start;
1764 Ok(Some(TensorSelection::Contiguous {
1765 offset_elements,
1766 shape: selected_shape,
1767 }))
1768}
1769
1770fn infer_join<C: RecipeCatalog + ?Sized>(
1771 catalog: &C,
1772 axis: usize,
1773 inputs: &[DerivedWeightRecipe],
1774 stack: bool,
1775) -> Result<RecipeMetadata, RecipeError> {
1776 if inputs.is_empty() {
1777 return Err(RecipeError::EmptyInputs);
1778 }
1779 let metadata = inputs
1780 .iter()
1781 .map(|input| input.infer(catalog))
1782 .collect::<Result<Vec<_>, _>>()?;
1783 let first = &metadata[0];
1784 if metadata.iter().any(|item| item.dtype != first.dtype) {
1785 return Err(RecipeError::DtypeMismatch);
1786 }
1787 let rank = first.shape.len();
1788 if axis > rank || (!stack && axis == rank) {
1789 return Err(RecipeError::InvalidJoinAxis { axis, rank, stack });
1790 }
1791 if stack {
1792 if metadata.iter().any(|item| item.shape != first.shape) {
1793 return Err(RecipeError::ShapeMismatch);
1794 }
1795 let mut shape = first.shape.clone();
1796 shape.insert(axis, metadata.len());
1797 metadata_for(shape, first.dtype.clone())
1798 } else {
1799 let mut shape = first.shape.clone();
1800 shape[axis] = 0;
1801 for item in &metadata {
1802 if item.shape.len() != rank
1803 || item
1804 .shape
1805 .iter()
1806 .enumerate()
1807 .any(|(index, dimension)| index != axis && *dimension != first.shape[index])
1808 {
1809 return Err(RecipeError::ShapeMismatch);
1810 }
1811 shape[axis] = shape[axis]
1812 .checked_add(item.shape[axis])
1813 .ok_or(RecipeError::ArithmeticOverflow("concatenate dimension"))?;
1814 }
1815 metadata_for(shape, first.dtype.clone())
1816 }
1817}
1818
1819fn metadata_for(shape: Vec<usize>, dtype: RecipeDtype) -> Result<RecipeMetadata, RecipeError> {
1820 let bits = element_count(&shape, "recipe output")?
1821 .checked_mul(dtype.bit_width()?)
1822 .ok_or(RecipeError::ArithmeticOverflow("recipe output bits"))?;
1823 let byte_len = bits
1824 .checked_add(7)
1825 .ok_or(RecipeError::ArithmeticOverflow("recipe output bytes"))?
1826 / 8;
1827 if byte_len == 0 {
1828 return Err(RecipeError::ZeroSizedOutput);
1829 }
1830 Ok(RecipeMetadata {
1831 shape,
1832 dtype,
1833 byte_len,
1834 })
1835}
1836
1837fn element_count(shape: &[usize], context: &'static str) -> Result<u64, RecipeError> {
1838 shape.iter().try_fold(1u64, |count, dimension| {
1839 count
1840 .checked_mul(
1841 u64::try_from(*dimension).map_err(|_| RecipeError::ArithmeticOverflow(context))?,
1842 )
1843 .ok_or(RecipeError::ArithmeticOverflow(context))
1844 })
1845}
1846
1847#[derive(Debug, thiserror::Error)]
1849#[allow(missing_docs)]
1850pub enum RecipeError {
1851 #[error("derived-weight source key must not be empty")]
1852 EmptySourceKey,
1853 #[error("derived-weight output name must not be empty")]
1854 EmptyOutputName,
1855 #[error("derived-weight recipe family requires at least one output")]
1856 EmptyOutputs,
1857 #[error("derived-weight output {target:?} is declared more than once")]
1858 DuplicateOutput { target: String },
1859 #[error("derived-weight alias name must not be empty")]
1860 EmptyAliasName,
1861 #[error("derived-weight alias {alias:?} is declared more than once")]
1862 DuplicateAlias { alias: String },
1863 #[error("derived-weight alias {alias:?} collides with a canonical output")]
1864 AliasOutputCollision { alias: String },
1865 #[error("derived-weight alias {alias:?} has unknown destination {destination:?}")]
1866 InvalidAliasDestination { alias: String, destination: String },
1867 #[error("derived-weight alias cycle contains {alias:?}")]
1868 AliasCycle { alias: String },
1869 #[error("matrix-family affine biases require a scale companion")]
1870 MatrixBiasWithoutScales,
1871 #[error("matrix-family weight must have rank at least two, got {shape:?}")]
1872 InvalidMatrixFamilyWeight { shape: Vec<usize> },
1873 #[error(
1874 "matrix-family {member} geometry {companion:?} is incompatible with weight {weight:?}"
1875 )]
1876 MatrixCompanionGeometry {
1877 member: &'static str,
1878 weight: Vec<usize>,
1879 companion: Vec<usize>,
1880 },
1881 #[error("matrix-family scale geometry {scales:?} differs from affine biases {biases:?}")]
1882 MatrixScaleBiasGeometry {
1883 scales: Vec<usize>,
1884 biases: Vec<usize>,
1885 },
1886 #[error("matrix-family leading selection must use axis 0, got axis {axis}")]
1887 MatrixFamilySelectionAxis { axis: usize },
1888 #[error("matrix-family leading selection does not accept a scalar contiguous span")]
1889 MatrixFamilyContiguousSelection,
1890 #[error("fused source {tensor:?} requires positive, named output segments")]
1891 InvalidFusedSplit { tensor: String },
1892 #[error(
1893 "fused source {tensor:?} axis {axis} has dimension {dimension}, but output widths sum to {outputs}"
1894 )]
1895 FusedSplitWidthMismatch {
1896 tensor: String,
1897 axis: usize,
1898 dimension: usize,
1899 outputs: usize,
1900 },
1901 #[error("selection axis {axis} is outside rank {rank}")]
1902 InvalidSelectionAxis { axis: usize, rank: usize },
1903 #[error("range {start}..{end} is invalid for axis {axis} dimension {dimension}")]
1904 InvalidRange {
1905 axis: usize,
1906 start: usize,
1907 end: usize,
1908 dimension: usize,
1909 },
1910 #[error("ordered indices are empty or outside axis {axis} dimension {dimension}")]
1911 InvalidIndices { axis: usize, dimension: usize },
1912 #[error("contiguous selection is empty or outside its source tensor")]
1913 InvalidContiguousSelection,
1914 #[error("concatenate and stack recipes require at least one input")]
1915 EmptyInputs,
1916 #[error("derived-weight inputs have different dtypes")]
1917 DtypeMismatch,
1918 #[error("derived-weight inputs have incompatible shapes")]
1919 ShapeMismatch,
1920 #[error("axis {axis} is invalid for rank {rank} (stack={stack})")]
1921 InvalidJoinAxis {
1922 axis: usize,
1923 rank: usize,
1924 stack: bool,
1925 },
1926 #[error("reshape changes element count from {input} to {output}")]
1927 ElementCountMismatch { input: u64, output: u64 },
1928 #[error("bitwise view changes byte count from {input} to {output}")]
1929 ByteCountMismatch { input: u64, output: u64 },
1930 #[error("axes {axes:?} are not a permutation of rank {rank}")]
1931 InvalidPermutation { axes: Vec<usize>, rank: usize },
1932 #[error("derived-weight output must contain at least one byte")]
1933 ZeroSizedOutput,
1934 #[error("derived-weight dtype {dtype} is unsupported")]
1935 UnsupportedDtype { dtype: String },
1936 #[error("derived-weight arithmetic overflow: {0}")]
1937 ArithmeticOverflow(&'static str),
1938 #[error("cannot push selection through {operation}: {reason}")]
1939 SelectionPushdownUnsupported {
1940 operation: &'static str,
1941 reason: String,
1942 },
1943 #[error(transparent)]
1944 Store(#[from] StoreError),
1945}
1946
1947#[cfg(test)]
1948mod tests {
1949 use super::*;
1950 use crate::store::{EncodedTensorLease, TensorReadRequest, WeightStoreDiagnostics};
1951 use std::path::Path;
1952 use std::sync::Mutex;
1953
1954 struct Catalog;
1955 struct Lease;
1956
1957 #[derive(Default)]
1958 struct BoundedCatalog {
1959 requests: Mutex<Vec<(String, TensorSelection)>>,
1960 }
1961
1962 impl RecipeCatalog for BoundedCatalog {
1963 fn tensor_metadata(&self, key: &str) -> Result<TensorMetadata, StoreError> {
1964 Catalog.metadata(key)
1965 }
1966 }
1967
1968 impl BoundedRecipeSource for BoundedCatalog {
1969 fn verify_bounded_source(
1970 &self,
1971 key: &str,
1972 selection: TensorSelection,
1973 ) -> Result<(), StoreError> {
1974 self.requests
1975 .lock()
1976 .unwrap()
1977 .push((key.to_owned(), selection));
1978 Ok(())
1979 }
1980 }
1981
1982 impl EncodedTensorLease for Lease {
1983 fn metadata(&self) -> &TensorMetadata {
1984 unreachable!()
1985 }
1986 fn selection(&self) -> &TensorSelection {
1987 unreachable!()
1988 }
1989 fn output_shape(&self) -> &[usize] {
1990 unreachable!()
1991 }
1992 fn bounded_read_proof(&self) -> &crate::store::BoundedReadProof {
1993 unreachable!()
1994 }
1995 fn backing_path(&self) -> Option<&Path> {
1996 None
1997 }
1998 fn encoded_bytes(&self) -> Option<&[u8]> {
1999 None
2000 }
2001 }
2002
2003 impl WeightStore for Catalog {
2004 type Lease = Lease;
2005
2006 fn keys(&self) -> Vec<String> {
2007 vec!["left".into(), "right".into()]
2008 }
2009 fn metadata(&self, key: &str) -> Result<TensorMetadata, StoreError> {
2010 if !self.keys().iter().any(|candidate| candidate == key) {
2011 return Err(StoreError::UnknownTensor { key: key.into() });
2012 }
2013 Ok(TensorMetadata {
2014 name: key.into(),
2015 logical_shape: vec![2, 3],
2016 physical_shape: vec![2, 3],
2017 stored_dtype: StoredDtype::F16,
2018 encoded_byte_len: 12,
2019 backing_shard: None,
2020 })
2021 }
2022 fn acquire(&self, _: TensorReadRequest) -> Result<Self::Lease, StoreError> {
2023 unreachable!()
2024 }
2025 fn diagnostics(&self) -> Result<WeightStoreDiagnostics, StoreError> {
2026 unreachable!()
2027 }
2028 }
2029
2030 #[test]
2031 fn nested_recipe_inference_is_backend_independent() {
2032 let recipe = DerivedWeightRecipe::Transpose {
2033 input: Box::new(DerivedWeightRecipe::Concatenate {
2034 axis: 0,
2035 inputs: vec![
2036 DerivedWeightRecipe::source("left", TensorSelection::Full),
2037 DerivedWeightRecipe::source(
2038 "right",
2039 TensorSelection::Range {
2040 axis: 0,
2041 start: 0,
2042 end: 1,
2043 },
2044 ),
2045 ],
2046 }),
2047 axes: vec![1, 0],
2048 };
2049 let metadata = recipe.infer(&Catalog).unwrap();
2050 assert_eq!(metadata.shape(), &[3, 3]);
2051 assert_eq!(metadata.dtype(), &RecipeDtype::F16);
2052 assert_eq!(metadata.byte_len(), 18);
2053 assert_eq!(recipe.source_keys(), ["left", "right"]);
2054
2055 let ordered = DerivedWeightRecipe::Stack {
2056 axis: 0,
2057 inputs: vec![
2058 DerivedWeightRecipe::source("right", TensorSelection::Full),
2059 DerivedWeightRecipe::source("left", TensorSelection::Full),
2060 DerivedWeightRecipe::source("right", TensorSelection::Full),
2061 ],
2062 };
2063 assert_eq!(ordered.source_keys(), ["left", "right"]);
2064 assert_eq!(ordered.source_occurrences(), ["right", "left", "right"]);
2065 }
2066
2067 #[test]
2068 fn bounded_preflight_walks_exact_physical_source_selections() {
2069 let catalog = BoundedCatalog::default();
2070 let recipe = DerivedWeightRecipe::Concatenate {
2071 axis: 0,
2072 inputs: vec![
2073 DerivedWeightRecipe::source(
2074 "left",
2075 TensorSelection::Range {
2076 axis: 0,
2077 start: 0,
2078 end: 1,
2079 },
2080 ),
2081 DerivedWeightRecipe::Reshape {
2082 input: Box::new(DerivedWeightRecipe::source("right", TensorSelection::Full)),
2083 shape: vec![2, 3],
2084 },
2085 ],
2086 };
2087
2088 recipe.preflight_bounded(&catalog).unwrap();
2089 assert_eq!(
2090 *catalog.requests.lock().unwrap(),
2091 vec![
2092 (
2093 "left".into(),
2094 TensorSelection::Range {
2095 axis: 0,
2096 start: 0,
2097 end: 1,
2098 },
2099 ),
2100 ("right".into(), TensorSelection::Full),
2101 ]
2102 );
2103 }
2104
2105 #[test]
2106 fn bounded_selection_pushdown_is_backend_independent() {
2107 let recipe = DerivedWeightRecipe::Concatenate {
2108 axis: 0,
2109 inputs: vec![
2110 DerivedWeightRecipe::source("left", TensorSelection::Full),
2111 DerivedWeightRecipe::source("right", TensorSelection::Full),
2112 ],
2113 };
2114 let selected = recipe
2115 .select_bounded(
2116 &Catalog,
2117 TensorSelection::Range {
2118 axis: 0,
2119 start: 1,
2120 end: 3,
2121 },
2122 )
2123 .unwrap();
2124 assert_eq!(selected.infer(&Catalog).unwrap().shape(), &[2, 3]);
2125 assert_eq!(
2126 selected,
2127 DerivedWeightRecipe::Concatenate {
2128 axis: 0,
2129 inputs: vec![
2130 DerivedWeightRecipe::source(
2131 "left",
2132 TensorSelection::Range {
2133 axis: 0,
2134 start: 1,
2135 end: 2,
2136 },
2137 ),
2138 DerivedWeightRecipe::source(
2139 "right",
2140 TensorSelection::Range {
2141 axis: 0,
2142 start: 0,
2143 end: 1,
2144 },
2145 ),
2146 ],
2147 }
2148 );
2149 }
2150
2151 #[test]
2152 fn fused_members_and_companions_validate_as_one_atomic_recipe_set() {
2153 let split = atomic_fused_split_recipes(
2154 &Catalog,
2155 [
2156 FusedSplitMember {
2157 source: "left".into(),
2158 axis: 1,
2159 outputs: vec![
2160 FusedSplitOutput {
2161 target: "weight.query".into(),
2162 width: 1,
2163 },
2164 FusedSplitOutput {
2165 target: "weight.key_value".into(),
2166 width: 2,
2167 },
2168 ],
2169 },
2170 FusedSplitMember {
2171 source: "right".into(),
2172 axis: 1,
2173 outputs: vec![
2174 FusedSplitOutput {
2175 target: "scale.query".into(),
2176 width: 1,
2177 },
2178 FusedSplitOutput {
2179 target: "scale.key_value".into(),
2180 width: 2,
2181 },
2182 ],
2183 },
2184 ],
2185 )
2186 .unwrap();
2187 assert_eq!(split.iter().count(), 4);
2188 assert_eq!(
2189 split
2190 .get("weight.key_value")
2191 .unwrap()
2192 .infer(&Catalog)
2193 .unwrap()
2194 .shape(),
2195 &[2, 2]
2196 );
2197
2198 let duplicate = atomic_fused_split_recipes(
2199 &Catalog,
2200 [FusedSplitMember {
2201 source: "left".into(),
2202 axis: 1,
2203 outputs: vec![
2204 FusedSplitOutput {
2205 target: "same".into(),
2206 width: 1,
2207 },
2208 FusedSplitOutput {
2209 target: "same".into(),
2210 width: 2,
2211 },
2212 ],
2213 }],
2214 );
2215 assert!(matches!(
2216 duplicate,
2217 Err(RecipeError::DuplicateOutput { .. })
2218 ));
2219
2220 let mismatch = atomic_fused_split_recipes(
2221 &Catalog,
2222 [FusedSplitMember {
2223 source: "left".into(),
2224 axis: 1,
2225 outputs: vec![FusedSplitOutput {
2226 target: "short".into(),
2227 width: 2,
2228 }],
2229 }],
2230 );
2231 assert!(matches!(
2232 mismatch,
2233 Err(RecipeError::FusedSplitWidthMismatch { .. })
2234 ));
2235 }
2236
2237 #[test]
2238 fn ordered_axis_selection_validates_value_head_layout_without_payload_reads() {
2239 let recipe = ordered_axis_selection(&Catalog, "left", 1, vec![2, 0, 1]).unwrap();
2240 assert_eq!(recipe.infer(&Catalog).unwrap().shape(), &[2, 3]);
2241 assert!(ordered_axis_selection(&Catalog, "left", 1, vec![3]).is_err());
2242 }
2243
2244 struct FamilyCatalog(BTreeMap<String, TensorMetadata>);
2245
2246 impl RecipeCatalog for FamilyCatalog {
2247 fn tensor_metadata(&self, key: &str) -> Result<TensorMetadata, StoreError> {
2248 self.0
2249 .get(key)
2250 .cloned()
2251 .ok_or_else(|| StoreError::UnknownTensor { key: key.into() })
2252 }
2253 }
2254
2255 fn family_catalog(entries: &[(&str, &[usize], StoredDtype)]) -> FamilyCatalog {
2256 FamilyCatalog(
2257 entries
2258 .iter()
2259 .map(|(name, shape, dtype)| {
2260 (
2261 (*name).to_owned(),
2262 TensorMetadata {
2263 name: (*name).to_owned(),
2264 logical_shape: shape.to_vec(),
2265 physical_shape: shape.to_vec(),
2266 stored_dtype: dtype.clone(),
2267 encoded_byte_len: 1,
2268 backing_shard: Some("synthetic.safetensors".into()),
2269 },
2270 )
2271 })
2272 .collect(),
2273 )
2274 }
2275
2276 fn member(target: &str, source: &str) -> MatrixRecipeMember {
2277 MatrixRecipeMember::new(
2278 target,
2279 DerivedWeightRecipe::source(source, TensorSelection::Full),
2280 )
2281 }
2282
2283 #[test]
2284 fn dense_matrix_family_selects_leading_rows_atomically() {
2285 let catalog = family_catalog(&[("dense.weight", &[4, 6], StoredDtype::F16)]);
2286 let family = AtomicMatrixRecipeFamily::new(
2287 &catalog,
2288 member("model.weight", "dense.weight"),
2289 None,
2290 None,
2291 )
2292 .unwrap();
2293 let selected = family
2294 .select_leading_axis(
2295 &catalog,
2296 TensorSelection::Range {
2297 axis: 0,
2298 start: 1,
2299 end: 3,
2300 },
2301 )
2302 .unwrap();
2303 assert_eq!(
2304 selected.weight().recipe.infer(&catalog).unwrap().shape(),
2305 &[2, 6]
2306 );
2307 let published = selected.publish(&catalog, []).unwrap();
2308 assert_eq!(published.iter().count(), 1);
2309 assert_eq!(published.aliases().count(), 0);
2310 assert_eq!(
2311 published.get("model.weight").unwrap(),
2312 &DerivedWeightRecipe::source(
2313 "dense.weight",
2314 TensorSelection::Range {
2315 axis: 0,
2316 start: 1,
2317 end: 3,
2318 }
2319 )
2320 );
2321 }
2322
2323 #[test]
2324 fn affine_matrix_family_selects_weight_scales_and_biases_coherently() {
2325 let catalog = family_catalog(&[
2326 ("affine.weight", &[4, 3], StoredDtype::U32),
2327 ("affine.scales", &[4, 2], StoredDtype::F16),
2328 ("affine.biases", &[4, 2], StoredDtype::F16),
2329 ]);
2330 let family = AtomicMatrixRecipeFamily::new(
2331 &catalog,
2332 member("model.weight", "affine.weight"),
2333 Some(member("model.scales", "affine.scales")),
2334 Some(member("model.biases", "affine.biases")),
2335 )
2336 .unwrap();
2337 let selected = family
2338 .select_leading_axis(
2339 &catalog,
2340 TensorSelection::Indices {
2341 axis: 0,
2342 indices: vec![3, 1],
2343 },
2344 )
2345 .unwrap();
2346 let published = selected.publish(&catalog, []).unwrap();
2347 assert_eq!(published.iter().count(), 3);
2348 assert_eq!(
2349 published
2350 .get("model.weight")
2351 .unwrap()
2352 .infer(&catalog)
2353 .unwrap()
2354 .shape(),
2355 &[2, 3]
2356 );
2357 for companion in ["model.scales", "model.biases"] {
2358 assert_eq!(
2359 published
2360 .get(companion)
2361 .unwrap()
2362 .infer(&catalog)
2363 .unwrap()
2364 .shape(),
2365 &[2, 2]
2366 );
2367 }
2368 }
2369
2370 #[test]
2371 fn mxfp4_matrix_family_preserves_scale_companion_without_biases() {
2372 let catalog = family_catalog(&[
2373 ("mxfp4.weight", &[4, 32], StoredDtype::F4),
2374 ("mxfp4.scales", &[4, 1], StoredDtype::F8E8M0),
2375 ]);
2376 let family = AtomicMatrixRecipeFamily::new(
2377 &catalog,
2378 member("model.weight", "mxfp4.weight"),
2379 Some(member("model.scales", "mxfp4.scales")),
2380 None,
2381 )
2382 .unwrap();
2383 let selected = family
2384 .select_leading_axis(
2385 &catalog,
2386 TensorSelection::Range {
2387 axis: 0,
2388 start: 0,
2389 end: 1,
2390 },
2391 )
2392 .unwrap();
2393 assert_eq!(
2394 selected.weight().recipe.infer(&catalog).unwrap().dtype(),
2395 &RecipeDtype::F4
2396 );
2397 assert_eq!(
2398 selected
2399 .scales()
2400 .unwrap()
2401 .recipe
2402 .infer(&catalog)
2403 .unwrap()
2404 .shape(),
2405 &[1, 1]
2406 );
2407 assert!(selected.biases().is_none());
2408 }
2409
2410 #[test]
2411 fn matrix_family_rejects_malformed_companions_before_publication() {
2412 let catalog = family_catalog(&[
2413 ("weight", &[4, 8], StoredDtype::U32),
2414 ("bad.scales", &[3, 2], StoredDtype::F16),
2415 ("scales", &[4, 2], StoredDtype::F16),
2416 ("bad.biases", &[4, 1], StoredDtype::F16),
2417 ]);
2418 assert!(matches!(
2419 AtomicMatrixRecipeFamily::new(
2420 &catalog,
2421 member("model.weight", "weight"),
2422 Some(member("model.scales", "bad.scales")),
2423 None,
2424 ),
2425 Err(RecipeError::MatrixCompanionGeometry { .. })
2426 ));
2427 assert!(matches!(
2428 AtomicMatrixRecipeFamily::new(
2429 &catalog,
2430 member("model.weight", "weight"),
2431 None,
2432 Some(member("model.biases", "bad.biases")),
2433 ),
2434 Err(RecipeError::MatrixBiasWithoutScales)
2435 ));
2436 assert!(matches!(
2437 AtomicMatrixRecipeFamily::new(
2438 &catalog,
2439 member("model.weight", "weight"),
2440 Some(member("model.scales", "scales")),
2441 Some(member("model.biases", "bad.biases")),
2442 ),
2443 Err(RecipeError::MatrixScaleBiasGeometry { .. })
2444 ));
2445
2446 let valid = AtomicMatrixRecipeFamily::new(
2447 &catalog,
2448 member("model.weight", "weight"),
2449 Some(member("model.scales", "scales")),
2450 None,
2451 )
2452 .unwrap();
2453 assert!(matches!(
2454 valid.select_leading_axis(
2455 &catalog,
2456 TensorSelection::Range {
2457 axis: 1,
2458 start: 0,
2459 end: 1,
2460 }
2461 ),
2462 Err(RecipeError::MatrixFamilySelectionAxis { axis: 1 })
2463 ));
2464 }
2465
2466 #[test]
2467 fn aliases_resolve_to_one_canonical_owner_without_recipe_duplication() {
2468 let catalog = family_catalog(&[("shared", &[2, 3], StoredDtype::F16)]);
2469 let family = AtomicMatrixRecipeFamily::new(
2470 &catalog,
2471 member("canonical.weight", "shared"),
2472 None,
2473 None,
2474 )
2475 .unwrap();
2476 let published = family
2477 .publish(
2478 &catalog,
2479 [
2480 RecipeAlias::new("slice.1.weight", "canonical.weight"),
2481 RecipeAlias::new("slice.2.weight", "slice.1.weight"),
2482 ],
2483 )
2484 .unwrap();
2485 assert_eq!(
2486 published.aliases().collect::<Vec<_>>(),
2487 vec![
2488 ("slice.1.weight", "canonical.weight"),
2489 ("slice.2.weight", "canonical.weight"),
2490 ]
2491 );
2492 let (_, canonical) = published.get_resolved("canonical.weight").unwrap();
2493 let (owner, aliased) = published.get_resolved("slice.2.weight").unwrap();
2494 assert_eq!(owner, "canonical.weight");
2495 assert!(std::ptr::eq(canonical, aliased));
2496 let (outputs, aliases) = published.into_parts();
2497 assert_eq!(outputs.len(), 1);
2498 assert_eq!(aliases.len(), 2);
2499 }
2500
2501 #[test]
2502 fn alias_validation_rejects_collision_cycle_and_unknown_destination_atomically() {
2503 let catalog = family_catalog(&[("shared", &[2, 3], StoredDtype::F16)]);
2504 let outputs = || {
2505 [(
2506 "canonical.weight".to_owned(),
2507 DerivedWeightRecipe::source("shared", TensorSelection::Full),
2508 )]
2509 };
2510 assert!(matches!(
2511 AtomicRecipeSet::new_with_aliases(
2512 &catalog,
2513 outputs(),
2514 [RecipeAlias::new("canonical.weight", "canonical.weight")],
2515 ),
2516 Err(RecipeError::AliasOutputCollision { .. })
2517 ));
2518 assert!(matches!(
2519 AtomicRecipeSet::new_with_aliases(
2520 &catalog,
2521 outputs(),
2522 [
2523 RecipeAlias::new("first", "second"),
2524 RecipeAlias::new("second", "first"),
2525 ],
2526 ),
2527 Err(RecipeError::AliasCycle { .. })
2528 ));
2529 assert!(matches!(
2530 AtomicRecipeSet::new_with_aliases(
2531 &catalog,
2532 outputs(),
2533 [RecipeAlias::new("orphan", "missing.weight")],
2534 ),
2535 Err(RecipeError::InvalidAliasDestination { .. })
2536 ));
2537 assert!(matches!(
2538 AtomicRecipeSet::new_with_aliases(
2539 &catalog,
2540 outputs(),
2541 [
2542 RecipeAlias::new("duplicate", "canonical.weight"),
2543 RecipeAlias::new("duplicate", "canonical.weight"),
2544 ],
2545 ),
2546 Err(RecipeError::DuplicateAlias { .. })
2547 ));
2548 }
2549}