Skip to main content

eredu_checkpoint/
schema.rs

1//! Declarative physical checkpoint schemas.
2
3#![allow(missing_docs)]
4
5use std::collections::BTreeSet;
6
7use eredu_gguf::GgmlType as GgufType;
8
9use crate::{BlockFp8ScaleEncoding, LinearFormat, StoredDtype};
10
11/// One named output segment in a fused projection.
12#[derive(Debug, Clone, Eq, PartialEq)]
13pub struct FusedProjectionSegment {
14    pub semantic: String,
15    pub width: usize,
16}
17
18impl FusedProjectionSegment {
19    pub fn new(semantic: impl Into<String>, width: usize) -> Result<Self, String> {
20        let semantic = semantic.into();
21        if semantic.trim().is_empty() || width == 0 {
22            return Err("fused projection segments require a name and positive width".into());
23        }
24        Ok(Self { semantic, width })
25    }
26}
27
28/// Family-neutral geometry for a projection whose output contains ordered
29/// semantic segments sharing one input and partition domain.
30#[derive(Debug, Clone, Eq, PartialEq)]
31pub struct FusedSegmentedProjectionSchema {
32    input_width: usize,
33    segments: Vec<FusedProjectionSegment>,
34    output_width: usize,
35}
36
37impl FusedSegmentedProjectionSchema {
38    pub fn new(
39        input_width: usize,
40        segments: impl IntoIterator<Item = FusedProjectionSegment>,
41    ) -> Result<Self, String> {
42        if input_width == 0 {
43            return Err("fused projection input width must be positive".into());
44        }
45        let segments = segments.into_iter().collect::<Vec<_>>();
46        if segments.is_empty() {
47            return Err("fused projection requires at least one segment".into());
48        }
49        let mut names = BTreeSet::new();
50        let mut output_width = 0usize;
51        for segment in &segments {
52            if segment.semantic.trim().is_empty()
53                || segment.width == 0
54                || !names.insert(segment.semantic.clone())
55            {
56                return Err("fused projection segments must be positive and uniquely named".into());
57            }
58            output_width = output_width
59                .checked_add(segment.width)
60                .ok_or_else(|| "fused projection output width overflows".to_string())?;
61        }
62        Ok(Self {
63            input_width,
64            segments,
65            output_width,
66        })
67    }
68
69    pub const fn input_width(&self) -> usize {
70        self.input_width
71    }
72
73    pub const fn output_width(&self) -> usize {
74        self.output_width
75    }
76
77    pub fn matrix_shape(&self) -> Vec<usize> {
78        vec![self.output_width, self.input_width]
79    }
80
81    pub fn bias_shape(&self) -> Vec<usize> {
82        vec![self.output_width]
83    }
84
85    pub fn segment_ranges(&self) -> Vec<std::ops::Range<usize>> {
86        let mut start = 0;
87        self.segments
88            .iter()
89            .map(|segment| {
90                let range = start..start + segment.width;
91                start = range.end;
92                range
93            })
94            .collect()
95    }
96}
97
98/// Physical axis convention for a depthwise causal-convolution kernel.
99#[derive(Debug, Clone, Copy, Eq, PartialEq)]
100pub enum DepthwiseKernelAxes {
101    /// `[channels, 1, kernel]`.
102    ChannelsSingletonKernel,
103    /// `[channels, kernel, 1]`.
104    ChannelsKernelSingleton,
105}
106
107/// Explicit storage and execution geometry for a depthwise convolution.
108#[derive(Debug, Clone, Copy, Eq, PartialEq)]
109pub struct DepthwiseConvolutionSchema {
110    channels: usize,
111    kernel: usize,
112    storage_axes: DepthwiseKernelAxes,
113    execution_axes: DepthwiseKernelAxes,
114    bias: bool,
115}
116
117impl DepthwiseConvolutionSchema {
118    pub fn new(channels: usize, kernel: usize, bias: bool) -> Result<Self, String> {
119        Self::with_axes(
120            channels,
121            kernel,
122            DepthwiseKernelAxes::ChannelsSingletonKernel,
123            DepthwiseKernelAxes::ChannelsSingletonKernel,
124            bias,
125        )
126    }
127
128    pub fn with_axes(
129        channels: usize,
130        kernel: usize,
131        storage_axes: DepthwiseKernelAxes,
132        execution_axes: DepthwiseKernelAxes,
133        bias: bool,
134    ) -> Result<Self, String> {
135        if channels == 0 || kernel == 0 {
136            return Err("depthwise convolution channels and kernel must be positive".into());
137        }
138        Ok(Self {
139            channels,
140            kernel,
141            storage_axes,
142            execution_axes,
143            bias,
144        })
145    }
146
147    pub fn storage_shape(self) -> Vec<usize> {
148        self.shape(self.storage_axes)
149    }
150
151    pub fn execution_shape(self) -> Vec<usize> {
152        self.shape(self.execution_axes)
153    }
154
155    pub fn bias_shape(self) -> Option<Vec<usize>> {
156        self.bias.then(|| vec![self.channels])
157    }
158
159    pub const fn element_count(self) -> usize {
160        self.channels * self.kernel
161    }
162
163    fn shape(self, axes: DepthwiseKernelAxes) -> Vec<usize> {
164        match axes {
165            DepthwiseKernelAxes::ChannelsSingletonKernel => vec![self.channels, 1, self.kernel],
166            DepthwiseKernelAxes::ChannelsKernelSingleton => vec![self.channels, self.kernel, 1],
167        }
168    }
169}
170
171/// Reusable head/group geometry for recurrent state-space parameter groups.
172#[derive(Debug, Clone, Copy, Eq, PartialEq)]
173pub struct RecurrentParameterGroupSchema {
174    pub heads: usize,
175    pub groups: usize,
176    pub head_width: usize,
177    pub state_width: usize,
178}
179
180impl RecurrentParameterGroupSchema {
181    pub fn new(
182        heads: usize,
183        groups: usize,
184        head_width: usize,
185        state_width: usize,
186    ) -> Result<Self, String> {
187        if heads == 0
188            || groups == 0
189            || head_width == 0
190            || state_width == 0
191            || !heads.is_multiple_of(groups)
192        {
193            return Err(
194                "recurrent parameter groups require positive widths and whole groups".into(),
195            );
196        }
197        Ok(Self {
198            heads,
199            groups,
200            head_width,
201            state_width,
202        })
203    }
204
205    pub const fn per_head_shape(self) -> [usize; 1] {
206        [self.heads]
207    }
208
209    pub const fn grouped_state_width(self) -> usize {
210        self.groups * self.state_width
211    }
212
213    pub const fn recurrent_state_shape(self) -> [usize; 3] {
214        [self.heads, self.head_width, self.state_width]
215    }
216}
217
218/// Architecture-supplied physical names for a block-FP8 scale companion.
219#[derive(Debug, Clone, Eq, PartialEq)]
220pub struct MatrixScaleNames {
221    /// Canonical physical scale tensor name.
222    pub key: String,
223    /// Accepted alternative physical scale tensor names.
224    pub aliases: Vec<String>,
225}
226
227/// Invalid matrix-plus-quantization-companion geometry.
228#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
229pub enum MatrixConstraintError {
230    /// Matrix shape has no input dimension.
231    #[error("quantized matrix {name:?} has scalar shape")]
232    Scalar { name: String },
233    /// Packing or companion geometry is incompatible.
234    #[error("{detail}")]
235    Invalid { detail: String },
236    /// A block-FP8 matrix did not declare its architecture-owned scale name.
237    #[error("block-FP8 matrix {name:?} requires an explicit scale companion name")]
238    MissingBlockScaleName { name: String },
239}
240
241/// Builds a matrix constraint and every companion required by its complete
242/// physical format.
243///
244/// This helper is family-neutral: callers provide canonical names, aliases,
245/// logical shape, and the selected physical encoding.
246pub fn matrix_for_linear_format(
247    name: impl Into<String>,
248    aliases: impl IntoIterator<Item = impl Into<String>>,
249    shape: Vec<usize>,
250    format: LinearFormat,
251    scale_companion: Option<MatrixScaleNames>,
252) -> Result<Vec<SafetensorsTensorConstraint>, MatrixConstraintError> {
253    let name = name.into();
254    let aliases = aliases.into_iter().map(Into::into).collect::<Vec<String>>();
255    if format == LinearFormat::Dense {
256        return Ok(vec![SafetensorsTensorConstraint::required(
257            name,
258            shape,
259            StoredDtypeConstraint::Floating,
260        )
261        .with_aliases(aliases)]);
262    }
263    if let LinearFormat::E4M3BlockFp8(fp8) = format {
264        fp8.validate()
265            .map_err(|error| MatrixConstraintError::Invalid {
266                detail: error.to_string(),
267            })?;
268        let scale = scale_companion
269            .ok_or_else(|| MatrixConstraintError::MissingBlockScaleName { name: name.clone() })?;
270        let row_axis = shape
271            .len()
272            .checked_sub(2)
273            .ok_or_else(|| MatrixConstraintError::Scalar { name: name.clone() })?;
274        let column_axis = shape.len() - 1;
275        let rows = *shape
276            .get(row_axis)
277            .ok_or_else(|| MatrixConstraintError::Scalar { name: name.clone() })?;
278        let columns = *shape
279            .get(column_axis)
280            .ok_or_else(|| MatrixConstraintError::Scalar { name: name.clone() })?;
281        let block_rows =
282            usize::try_from(fp8.block_rows).map_err(|_| MatrixConstraintError::Invalid {
283                detail: format!("block-FP8 row geometry for {name:?} exceeds usize"),
284            })?;
285        let block_columns =
286            usize::try_from(fp8.block_columns).map_err(|_| MatrixConstraintError::Invalid {
287                detail: format!("block-FP8 column geometry for {name:?} exceeds usize"),
288            })?;
289        let mut scale_shape = shape.clone();
290        scale_shape[row_axis] = rows.div_ceil(block_rows);
291        scale_shape[column_axis] = columns.div_ceil(block_columns);
292        let scale_dtype = match fp8.scale_encoding {
293            BlockFp8ScaleEncoding::FloatingPoint => StoredDtypeConstraint::Floating,
294            BlockFp8ScaleEncoding::Ue8m0 => StoredDtypeConstraint::Exact(StoredDtype::F8E8M0),
295        };
296        return Ok(vec![
297            SafetensorsTensorConstraint::required(
298                name.clone(),
299                shape,
300                StoredDtypeConstraint::Exact(StoredDtype::F8E4M3),
301            )
302            .with_aliases(aliases),
303            SafetensorsTensorConstraint::required(scale.key, scale_shape, scale_dtype)
304                .with_aliases(scale.aliases)
305                .linear_companion(name.clone(), LinearCompanionKind::Scale),
306        ]);
307    }
308    let quantization = format
309        .weight_quantization()
310        .expect("non-dense, non-FP8 linear formats use packed quantization");
311    let input = *shape
312        .last()
313        .ok_or_else(|| MatrixConstraintError::Scalar { name: name.clone() })?;
314    let bits =
315        usize::try_from(quantization.bits()).map_err(|_| MatrixConstraintError::Invalid {
316            detail: format!("quantization bit width for {name:?} exceeds usize"),
317        })?;
318    let group =
319        usize::try_from(quantization.group_size()).map_err(|_| MatrixConstraintError::Invalid {
320            detail: format!("quantization group size for {name:?} exceeds usize"),
321        })?;
322    let packed_bits = input
323        .checked_mul(bits)
324        .ok_or_else(|| MatrixConstraintError::Invalid {
325            detail: format!("quantized matrix {name:?} packing geometry overflows"),
326        })?;
327    if group == 0
328        || !input.is_multiple_of(group)
329        || !input.is_multiple_of(32)
330        || !packed_bits.is_multiple_of(32)
331    {
332        return Err(MatrixConstraintError::Invalid {
333            detail: format!(
334                "quantized matrix {name:?} input dimension {input} is incompatible with group size {group} and {bits}-bit packing"
335            ),
336        });
337    }
338    let mut packed = shape.clone();
339    *packed.last_mut().expect("matrix shape") = packed_bits / 32;
340    let mut companion = shape;
341    *companion.last_mut().expect("matrix shape") = input / group;
342    let prefix = name.strip_suffix(".weight").unwrap_or(&name).to_string();
343    let companion_dtype = || {
344        StoredDtypeConstraint::OneOf(vec![
345            StoredDtype::F16,
346            StoredDtype::BF16,
347            StoredDtype::F32,
348            StoredDtype::U8,
349        ])
350    };
351    let companion_alias = |component: &str| {
352        aliases
353            .iter()
354            .map(|alias| {
355                let prefix = alias.strip_suffix(".weight").unwrap_or(alias);
356                format!("{prefix}.{component}")
357            })
358            .collect::<Vec<_>>()
359    };
360    let mut constraints = vec![SafetensorsTensorConstraint::required(
361        name.clone(),
362        packed,
363        StoredDtypeConstraint::Exact(StoredDtype::U32),
364    )
365    .with_aliases(aliases.clone())];
366    let scale = scale_companion.unwrap_or_else(|| MatrixScaleNames {
367        key: format!("{prefix}.scales"),
368        aliases: companion_alias("scales"),
369    });
370    constraints.push(
371        SafetensorsTensorConstraint::required(scale.key, companion.clone(), companion_dtype())
372            .with_aliases(scale.aliases)
373            .linear_companion(name.clone(), LinearCompanionKind::Scale),
374    );
375    if quantization.has_biases() {
376        constraints.push(
377            SafetensorsTensorConstraint::required(
378                format!("{prefix}.biases"),
379                companion,
380                companion_dtype(),
381            )
382            .with_aliases(companion_alias("biases"))
383            .linear_companion(name.clone(), LinearCompanionKind::AffineBias),
384        );
385    }
386    Ok(constraints)
387}
388
389/// Whether a physical tensor must be present in the selected layout.
390#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
391pub enum TensorRequirement {
392    Required,
393    Optional,
394}
395
396/// How failures for a constraint are classified.
397#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
398pub enum TensorRole {
399    Tensor,
400    Companion,
401}
402
403/// Semantic role of one encoded-linear companion output.
404#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
405pub enum LinearCompanionKind {
406    /// Per-group or per-block scale values.
407    Scale,
408    /// Per-group affine zero-point/bias values.
409    AffineBias,
410}
411
412/// Exact primary relationship for a physical encoded-linear companion.
413#[derive(Debug, Clone, Eq, PartialEq)]
414pub struct LinearCompanionConstraint {
415    /// Canonical primary weight identity.
416    pub primary: String,
417    /// Companion semantic role.
418    pub kind: LinearCompanionKind,
419}
420
421/// Declarative SafeTensors storage constraint.
422#[derive(Debug, Clone, Eq, PartialEq)]
423pub enum StoredDtypeConstraint {
424    Exact(StoredDtype),
425    OneOf(Vec<StoredDtype>),
426    /// Repository-supported floating storage: F16, BF16, or F32.
427    Floating,
428}
429
430impl StoredDtypeConstraint {
431    pub fn accepts(&self, actual: &StoredDtype) -> bool {
432        match self {
433            Self::Exact(expected) => expected == actual,
434            Self::OneOf(expected) => expected.contains(actual),
435            Self::Floating => matches!(
436                actual,
437                StoredDtype::F16 | StoredDtype::BF16 | StoredDtype::F32
438            ),
439        }
440    }
441
442    fn normalize(&mut self) {
443        if let Self::OneOf(dtypes) = self {
444            dtypes.sort_by_key(|dtype| format!("{dtype:?}"));
445            dtypes.dedup();
446        }
447    }
448}
449
450/// One physical SafeTensors tensor.
451#[derive(Debug, Clone, Eq, PartialEq)]
452pub struct SafetensorsTensorConstraint {
453    pub key: String,
454    /// Alternative physical names for the same logical tensor.
455    pub aliases: Vec<String>,
456    pub shape: Vec<usize>,
457    /// Additional accepted physical shapes with equivalent runtime semantics.
458    pub alternate_shapes: Vec<Vec<usize>>,
459    /// Accept any physical shape with this many elements. `shape` remains the
460    /// canonical shape used by loading recipes.
461    pub element_count: Option<usize>,
462    pub dtype: StoredDtypeConstraint,
463    pub requirement: TensorRequirement,
464    pub role: TensorRole,
465    /// Exact primary relationship when this is an encoded-linear companion.
466    pub linear_companion: Option<LinearCompanionConstraint>,
467}
468
469impl SafetensorsTensorConstraint {
470    pub fn required(
471        key: impl Into<String>,
472        shape: impl Into<Vec<usize>>,
473        dtype: StoredDtypeConstraint,
474    ) -> Self {
475        Self {
476            key: key.into(),
477            aliases: Vec::new(),
478            shape: shape.into(),
479            alternate_shapes: Vec::new(),
480            element_count: None,
481            dtype,
482            requirement: TensorRequirement::Required,
483            role: TensorRole::Tensor,
484            linear_companion: None,
485        }
486    }
487
488    pub fn optional(mut self) -> Self {
489        self.requirement = TensorRequirement::Optional;
490        self
491    }
492
493    pub fn with_aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
494        self.aliases = aliases.into_iter().map(Into::into).collect();
495        self
496    }
497
498    pub fn with_element_count(mut self, element_count: usize) -> Self {
499        self.element_count = Some(element_count);
500        self
501    }
502
503    pub fn with_alternate_shapes(
504        mut self,
505        shapes: impl IntoIterator<Item = impl Into<Vec<usize>>>,
506    ) -> Self {
507        self.alternate_shapes = shapes.into_iter().map(Into::into).collect();
508        self
509    }
510
511    pub fn companion(mut self) -> Self {
512        self.role = TensorRole::Companion;
513        self
514    }
515
516    /// Marks this tensor as an exact encoded-linear companion.
517    pub fn linear_companion(
518        mut self,
519        primary: impl Into<String>,
520        kind: LinearCompanionKind,
521    ) -> Self {
522        self.role = TensorRole::Companion;
523        self.linear_companion = Some(LinearCompanionConstraint {
524            primary: primary.into(),
525            kind,
526        });
527        self
528    }
529}
530
531/// Generic GGUF operation classes supported by runtime kernels.
532#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
533pub enum TensorOperation {
534    Matrix,
535    Vector,
536    Dense,
537    I32,
538    MxFp4Matrix,
539}
540
541/// Declarative GGUF physical encoding constraint.
542#[derive(Debug, Clone, Eq, PartialEq)]
543pub enum GgufTypeConstraint {
544    OperationClass(TensorOperation),
545}
546
547impl GgufTypeConstraint {
548    pub fn accepts(&self, actual: GgufType) -> bool {
549        match self {
550            Self::OperationClass(operation) => gguf_encoding_supported(*operation, actual),
551        }
552    }
553
554    fn normalize(&mut self) {}
555}
556
557/// Generic mapping from a numerical operation to accepted GGUF encodings.
558pub fn gguf_encoding_supported(operation: TensorOperation, encoding: GgufType) -> bool {
559    match operation {
560        TensorOperation::Vector | TensorOperation::Dense => {
561            matches!(encoding, GgufType::F32 | GgufType::F16 | GgufType::Bf16)
562        }
563        TensorOperation::I32 => encoding == GgufType::I32,
564        TensorOperation::MxFp4Matrix => encoding == GgufType::MxFp4,
565        TensorOperation::Matrix => !matches!(
566            encoding,
567            GgufType::I8
568                | GgufType::I16
569                | GgufType::I32
570                | GgufType::I64
571                | GgufType::F64
572                | GgufType::RemovedIQ4NL4_4
573                | GgufType::RemovedIQ4NL4_8
574                | GgufType::RemovedIQ4NL8_8
575                | GgufType::Unknown(_)
576        ),
577    }
578}
579
580/// One physical GGUF tensor.
581#[derive(Debug, Clone, Eq, PartialEq)]
582pub struct GgufTensorConstraint {
583    pub key: String,
584    /// Alternative physical names for the same logical tensor.
585    pub aliases: Vec<String>,
586    pub shape: Vec<usize>,
587    /// Additional accepted physical shapes for encodings with equivalent
588    /// runtime semantics (for example, flattened and singleton-axis kernels).
589    pub alternate_shapes: Vec<Vec<usize>>,
590    /// Accept any physical shape with this many elements. `shape` remains the
591    /// canonical shape used by loading recipes.
592    pub element_count: Option<usize>,
593    pub encoding: GgufTypeConstraint,
594    pub requirement: TensorRequirement,
595    pub role: TensorRole,
596}
597
598impl GgufTensorConstraint {
599    pub fn required(
600        key: impl Into<String>,
601        shape: impl Into<Vec<usize>>,
602        encoding: GgufTypeConstraint,
603    ) -> Self {
604        Self {
605            key: key.into(),
606            aliases: Vec::new(),
607            shape: shape.into(),
608            alternate_shapes: Vec::new(),
609            element_count: None,
610            encoding,
611            requirement: TensorRequirement::Required,
612            role: TensorRole::Tensor,
613        }
614    }
615
616    pub fn with_aliases(mut self, aliases: impl IntoIterator<Item = impl Into<String>>) -> Self {
617        self.aliases = aliases.into_iter().map(Into::into).collect();
618        self
619    }
620
621    pub fn with_alternate_shapes(
622        mut self,
623        shapes: impl IntoIterator<Item = impl Into<Vec<usize>>>,
624    ) -> Self {
625        self.alternate_shapes = shapes.into_iter().map(Into::into).collect();
626        self
627    }
628
629    pub fn with_element_count(mut self, element_count: usize) -> Self {
630        self.element_count = Some(element_count);
631        self
632    }
633}
634
635/// One mutually exclusive physical layout.
636#[derive(Debug, Clone, Eq, PartialEq)]
637pub struct LayoutVariant<T> {
638    pub id: String,
639    pub tensors: Vec<T>,
640    pub discriminator_keys: Vec<String>,
641}
642
643/// A required or optional group of mutually exclusive layouts.
644#[derive(Debug, Clone, Eq, PartialEq)]
645pub struct AlternativeLayoutGroup<T> {
646    pub id: String,
647    pub required: bool,
648    pub variants: Vec<LayoutVariant<T>>,
649}
650
651/// Exact-catalog policy applied after selecting layouts.
652#[derive(Debug, Clone, Eq, PartialEq)]
653pub struct CatalogPolicy {
654    pub strict: bool,
655    pub explicitly_allowed_keys: BTreeSet<String>,
656    pub allowed_prefixes: Vec<String>,
657    pub allowed_suffixes: Vec<String>,
658}
659
660impl CatalogPolicy {
661    pub fn strict() -> Self {
662        Self {
663            strict: true,
664            explicitly_allowed_keys: BTreeSet::new(),
665            allowed_prefixes: Vec::new(),
666            allowed_suffixes: Vec::new(),
667        }
668    }
669
670    pub fn non_strict() -> Self {
671        Self {
672            strict: false,
673            ..Self::strict()
674        }
675    }
676
677    fn normalize(&mut self) {
678        self.allowed_prefixes.sort();
679        self.allowed_prefixes.dedup();
680        self.allowed_suffixes.sort();
681        self.allowed_suffixes.dedup();
682    }
683}
684
685/// Invalid or ambiguous declarative plan.
686#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
687pub enum CheckpointPlanError {
688    #[error("checkpoint plan identity must not be empty")]
689    EmptyIdentity,
690    #[error("checkpoint plan contains an empty {kind} id")]
691    EmptyId { kind: &'static str },
692    #[error("checkpoint layout group {group:?} has no variants")]
693    EmptyLayoutGroup { group: String },
694    #[error("checkpoint layout variant {variant:?} has no tensors")]
695    EmptyLayoutVariant { variant: String },
696    #[error("checkpoint tensor key must not be empty")]
697    EmptyTensorKey,
698    #[error("checkpoint tensor {key:?} contains an empty physical alias")]
699    EmptyTensorAlias { key: String },
700    #[error("checkpoint tensor {key:?} has invalid shape {shape:?}")]
701    InvalidShape { key: String, shape: Vec<usize> },
702    #[error("checkpoint tensor {key:?} shape element count overflows")]
703    ShapeOverflow { key: String },
704    #[error("checkpoint tensor {key:?} has invalid required element count {element_count}")]
705    InvalidElementCount { key: String, element_count: usize },
706    #[error("checkpoint tensor {key:?} canonical shape contains {shape_elements} elements, but its required element count is {element_count}")]
707    ElementCountMismatch {
708        key: String,
709        shape_elements: usize,
710        element_count: usize,
711    },
712    #[error("checkpoint plan contains duplicate tensor key {key:?}")]
713    DuplicateTensorKey { key: String },
714    #[error("checkpoint layout variant {variant:?} has invalid discriminator {key:?}")]
715    InvalidDiscriminator { variant: String, key: String },
716    #[error("checkpoint plan contains duplicate {kind} id {id:?}")]
717    DuplicateId { kind: &'static str, id: String },
718    #[error("checkpoint catalog policy contains an empty explicitly allowed key")]
719    EmptyAllowedKey,
720    #[error("checkpoint catalog policy contains an empty allowed prefix")]
721    EmptyAllowedPrefix,
722    #[error("checkpoint catalog policy contains an empty allowed suffix")]
723    EmptyAllowedSuffix,
724    #[error("checkpoint tensor {key:?} has an empty encoding alternative set")]
725    EmptyEncodingSet { key: String },
726}
727
728trait PhysicalConstraint {
729    fn key(&self) -> &str;
730    fn aliases(&self) -> &[String];
731    fn aliases_mut(&mut self) -> &mut Vec<String>;
732    fn shape(&self) -> &[usize];
733    fn alternate_shapes(&self) -> &[Vec<usize>];
734    fn element_count(&self) -> Option<usize>;
735    fn normalize(&mut self);
736    fn has_empty_encoding_set(&self) -> bool;
737}
738
739impl PhysicalConstraint for SafetensorsTensorConstraint {
740    fn key(&self) -> &str {
741        &self.key
742    }
743    fn aliases(&self) -> &[String] {
744        &self.aliases
745    }
746    fn aliases_mut(&mut self) -> &mut Vec<String> {
747        &mut self.aliases
748    }
749    fn shape(&self) -> &[usize] {
750        &self.shape
751    }
752    fn alternate_shapes(&self) -> &[Vec<usize>] {
753        &self.alternate_shapes
754    }
755    fn element_count(&self) -> Option<usize> {
756        self.element_count
757    }
758    fn normalize(&mut self) {
759        self.dtype.normalize();
760        self.alternate_shapes.sort();
761        self.alternate_shapes.dedup();
762        self.alternate_shapes.retain(|shape| shape != &self.shape);
763    }
764    fn has_empty_encoding_set(&self) -> bool {
765        matches!(&self.dtype, StoredDtypeConstraint::OneOf(dtypes) if dtypes.is_empty())
766    }
767}
768
769impl PhysicalConstraint for GgufTensorConstraint {
770    fn key(&self) -> &str {
771        &self.key
772    }
773    fn aliases(&self) -> &[String] {
774        &self.aliases
775    }
776    fn aliases_mut(&mut self) -> &mut Vec<String> {
777        &mut self.aliases
778    }
779    fn shape(&self) -> &[usize] {
780        &self.shape
781    }
782    fn alternate_shapes(&self) -> &[Vec<usize>] {
783        &self.alternate_shapes
784    }
785    fn element_count(&self) -> Option<usize> {
786        self.element_count
787    }
788    fn normalize(&mut self) {
789        self.encoding.normalize();
790        self.alternate_shapes.sort();
791        self.alternate_shapes.dedup();
792        self.alternate_shapes.retain(|shape| shape != &self.shape);
793    }
794    fn has_empty_encoding_set(&self) -> bool {
795        false
796    }
797}
798
799fn normalize_plan<T: PhysicalConstraint>(
800    identity: &str,
801    common: &mut [T],
802    groups: &mut [AlternativeLayoutGroup<T>],
803    policy: &mut CatalogPolicy,
804) -> Result<(), CheckpointPlanError> {
805    if identity.trim().is_empty() {
806        return Err(CheckpointPlanError::EmptyIdentity);
807    }
808    let normalize_tensor = |tensor: &mut T| {
809        tensor.normalize();
810        if tensor.key().trim().is_empty() {
811            return Err(CheckpointPlanError::EmptyTensorKey);
812        }
813        tensor.aliases_mut().sort();
814        tensor.aliases_mut().dedup();
815        if tensor.aliases().iter().any(|alias| alias.trim().is_empty()) {
816            return Err(CheckpointPlanError::EmptyTensorAlias {
817                key: tensor.key().into(),
818            });
819        }
820        // Empty shapes are scalar tensors. A zero-sized dimension is invalid.
821        let shapes = std::iter::once(tensor.shape()).chain(
822            tensor
823                .alternate_shapes()
824                .iter()
825                .map(|shape| shape.as_slice()),
826        );
827        let mut canonical_elements = None;
828        for (index, shape) in shapes.enumerate() {
829            if shape.contains(&0) {
830                return Err(CheckpointPlanError::InvalidShape {
831                    key: tensor.key().into(),
832                    shape: shape.to_vec(),
833                });
834            }
835            let elements = shape
836                .iter()
837                .try_fold(1usize, |count, dimension| count.checked_mul(*dimension))
838                .ok_or_else(|| CheckpointPlanError::ShapeOverflow {
839                    key: tensor.key().into(),
840                })?;
841            if index == 0 {
842                canonical_elements = Some(elements);
843            }
844        }
845        if let Some(element_count) = tensor.element_count() {
846            if element_count == 0 {
847                return Err(CheckpointPlanError::InvalidElementCount {
848                    key: tensor.key().into(),
849                    element_count,
850                });
851            }
852            if canonical_elements != Some(element_count) {
853                return Err(CheckpointPlanError::ElementCountMismatch {
854                    key: tensor.key().into(),
855                    shape_elements: canonical_elements.expect("canonical shape was checked"),
856                    element_count,
857                });
858            }
859        }
860        if tensor.has_empty_encoding_set() {
861            return Err(CheckpointPlanError::EmptyEncodingSet {
862                key: tensor.key().into(),
863            });
864        }
865        Ok(())
866    };
867    let physical_keys = |tensor: &T| {
868        std::iter::once(tensor.key().to_string())
869            .chain(tensor.aliases().iter().cloned())
870            .collect::<Vec<_>>()
871    };
872    let mut keys = BTreeSet::new();
873    for tensor in common.iter_mut() {
874        normalize_tensor(tensor)?;
875        for physical_key in physical_keys(tensor) {
876            if !keys.insert(physical_key.clone()) {
877                return Err(CheckpointPlanError::DuplicateTensorKey { key: physical_key });
878            }
879        }
880    }
881    common.sort_by(|left, right| left.key().cmp(right.key()));
882
883    let mut group_ids = BTreeSet::new();
884    for group in groups.iter_mut() {
885        if group.id.trim().is_empty() {
886            return Err(CheckpointPlanError::EmptyId {
887                kind: "layout group",
888            });
889        }
890        if !group_ids.insert(group.id.clone()) {
891            return Err(CheckpointPlanError::DuplicateId {
892                kind: "layout group",
893                id: group.id.clone(),
894            });
895        }
896        if group.variants.is_empty() {
897            return Err(CheckpointPlanError::EmptyLayoutGroup {
898                group: group.id.clone(),
899            });
900        }
901        let mut variant_ids = BTreeSet::new();
902        let mut group_keys = BTreeSet::new();
903        for variant in &mut group.variants {
904            if variant.id.trim().is_empty() {
905                return Err(CheckpointPlanError::EmptyId {
906                    kind: "layout variant",
907                });
908            }
909            if !variant_ids.insert(variant.id.clone()) {
910                return Err(CheckpointPlanError::DuplicateId {
911                    kind: "layout variant",
912                    id: variant.id.clone(),
913                });
914            }
915            if variant.tensors.is_empty() {
916                return Err(CheckpointPlanError::EmptyLayoutVariant {
917                    variant: variant.id.clone(),
918                });
919            }
920            let mut variant_keys = keys.clone();
921            for tensor in &mut variant.tensors {
922                normalize_tensor(tensor)?;
923                for physical_key in physical_keys(tensor) {
924                    if !variant_keys.insert(physical_key.clone()) {
925                        return Err(CheckpointPlanError::DuplicateTensorKey { key: physical_key });
926                    }
927                    group_keys.insert(physical_key);
928                }
929            }
930            variant
931                .tensors
932                .sort_by(|left, right| left.key().cmp(right.key()));
933            if variant.discriminator_keys.is_empty() {
934                variant.discriminator_keys = variant
935                    .tensors
936                    .iter()
937                    .map(|tensor| tensor.key().to_string())
938                    .collect();
939            }
940            variant.discriminator_keys.sort();
941            variant.discriminator_keys.dedup();
942            let variant_keys = variant
943                .tensors
944                .iter()
945                .map(|tensor| tensor.key())
946                .collect::<BTreeSet<_>>();
947            if let Some(key) = variant
948                .discriminator_keys
949                .iter()
950                .find(|key| !variant_keys.contains(key.as_str()))
951            {
952                return Err(CheckpointPlanError::InvalidDiscriminator {
953                    variant: variant.id.clone(),
954                    key: key.clone(),
955                });
956            }
957        }
958        keys.extend(group_keys);
959        group.variants.sort_by(|left, right| left.id.cmp(&right.id));
960    }
961    groups.sort_by(|left, right| left.id.cmp(&right.id));
962    if policy
963        .explicitly_allowed_keys
964        .iter()
965        .any(|key| key.trim().is_empty())
966    {
967        return Err(CheckpointPlanError::EmptyAllowedKey);
968    }
969    if policy
970        .allowed_prefixes
971        .iter()
972        .any(|prefix| prefix.trim().is_empty())
973    {
974        return Err(CheckpointPlanError::EmptyAllowedPrefix);
975    }
976    if policy
977        .allowed_suffixes
978        .iter()
979        .any(|suffix| suffix.trim().is_empty())
980    {
981        return Err(CheckpointPlanError::EmptyAllowedSuffix);
982    }
983    policy.normalize();
984    Ok(())
985}
986
987macro_rules! checkpoint_plan {
988    ($name:ident, $constraint:ty) => {
989        #[derive(Debug, Clone, Eq, PartialEq)]
990        pub struct $name {
991            pub identity: String,
992            pub common_tensors: Vec<$constraint>,
993            pub layout_groups: Vec<AlternativeLayoutGroup<$constraint>>,
994            pub catalog_policy: CatalogPolicy,
995        }
996
997        impl $name {
998            pub fn new(
999                identity: impl Into<String>,
1000                mut common_tensors: Vec<$constraint>,
1001                mut layout_groups: Vec<AlternativeLayoutGroup<$constraint>>,
1002                mut catalog_policy: CatalogPolicy,
1003            ) -> Result<Self, CheckpointPlanError> {
1004                let identity = identity.into();
1005                normalize_plan(
1006                    &identity,
1007                    &mut common_tensors,
1008                    &mut layout_groups,
1009                    &mut catalog_policy,
1010                )?;
1011                Ok(Self {
1012                    identity,
1013                    common_tensors,
1014                    layout_groups,
1015                    catalog_policy,
1016                })
1017            }
1018        }
1019    };
1020}
1021
1022checkpoint_plan!(SafetensorsCheckpointPlan, SafetensorsTensorConstraint);
1023checkpoint_plan!(GgufCheckpointPlan, GgufTensorConstraint);
1024
1025#[cfg(test)]
1026mod tests {
1027    use super::*;
1028    use crate::{BlockFp8Format, BlockFp8ScaleEncoding, WeightQuantization};
1029
1030    #[test]
1031    fn block_fp8_matrix_declares_exact_weight_scale_geometry_and_dtype() {
1032        let constraints = matrix_for_linear_format(
1033            "projection.weight",
1034            ["projection.alias.weight"],
1035            vec![8, 257, 129],
1036            LinearFormat::E4M3BlockFp8(
1037                BlockFp8Format::new(128, 128, BlockFp8ScaleEncoding::Ue8m0).unwrap(),
1038            ),
1039            Some(MatrixScaleNames {
1040                key: "projection.weight_scale_inv".into(),
1041                aliases: vec!["projection.alias.weight_scale_inv".into()],
1042            }),
1043        )
1044        .unwrap();
1045        assert_eq!(constraints.len(), 2);
1046        assert_eq!(constraints[0].shape, vec![8, 257, 129]);
1047        assert_eq!(
1048            constraints[0].dtype,
1049            StoredDtypeConstraint::Exact(StoredDtype::F8E4M3)
1050        );
1051        assert_eq!(constraints[1].shape, vec![8, 3, 2]);
1052        assert_eq!(
1053            constraints[1].dtype,
1054            StoredDtypeConstraint::Exact(StoredDtype::F8E8M0)
1055        );
1056        assert_eq!(constraints[1].role, TensorRole::Companion);
1057    }
1058
1059    #[test]
1060    fn block_fp8_matrix_requires_architecture_supplied_scale_identity() {
1061        assert!(matches!(
1062            matrix_for_linear_format(
1063                "projection.weight",
1064                Vec::<String>::new(),
1065                vec![128, 128],
1066                LinearFormat::E4M3BlockFp8(
1067                    BlockFp8Format::new(128, 128, BlockFp8ScaleEncoding::FloatingPoint,).unwrap(),
1068                ),
1069                None,
1070            ),
1071            Err(MatrixConstraintError::MissingBlockScaleName { .. })
1072        ));
1073    }
1074
1075    #[test]
1076    fn packed_matrix_accepts_only_declared_physical_aliases() {
1077        let format = LinearFormat::from(WeightQuantization::Affine(
1078            crate::AffineQuantization::new(32, 4).unwrap(),
1079        ));
1080        let constraints = matrix_for_linear_format(
1081            "projection.weight",
1082            ["projection.alias.weight"],
1083            vec![64, 64],
1084            format,
1085            None,
1086        )
1087        .unwrap();
1088
1089        assert_eq!(constraints[0].aliases, ["projection.alias.weight"]);
1090    }
1091
1092    #[test]
1093    fn construction_sorts_and_rejects_duplicate_or_invalid_shapes() {
1094        let tensor = |key: &str, shape| {
1095            SafetensorsTensorConstraint::required(key, shape, StoredDtypeConstraint::Floating)
1096        };
1097        let plan = SafetensorsCheckpointPlan::new(
1098            "stable",
1099            vec![
1100                tensor("z", vec![2]),
1101                tensor("a", vec![1]),
1102                tensor("scalar", vec![]),
1103            ],
1104            Vec::new(),
1105            CatalogPolicy::strict(),
1106        )
1107        .unwrap();
1108        assert_eq!(
1109            plan.common_tensors
1110                .iter()
1111                .map(|tensor| tensor.key.as_str())
1112                .collect::<Vec<_>>(),
1113            ["a", "scalar", "z"]
1114        );
1115        assert!(matches!(
1116            SafetensorsCheckpointPlan::new(
1117                "duplicate",
1118                vec![tensor("a", vec![1]), tensor("a", vec![1])],
1119                Vec::new(),
1120                CatalogPolicy::strict(),
1121            ),
1122            Err(CheckpointPlanError::DuplicateTensorKey { .. })
1123        ));
1124        let aliased = tensor("logical", vec![1]).with_aliases(["physical"]);
1125        assert!(matches!(
1126            SafetensorsCheckpointPlan::new(
1127                "duplicate alias",
1128                vec![aliased, tensor("physical", vec![1])],
1129                Vec::new(),
1130                CatalogPolicy::strict(),
1131            ),
1132            Err(CheckpointPlanError::DuplicateTensorKey { key }) if key == "physical"
1133        ));
1134        assert!(matches!(
1135            SafetensorsCheckpointPlan::new(
1136                "zero",
1137                vec![tensor("a", vec![0])],
1138                Vec::new(),
1139                CatalogPolicy::strict(),
1140            ),
1141            Err(CheckpointPlanError::InvalidShape { .. })
1142        ));
1143        assert!(matches!(
1144            SafetensorsCheckpointPlan::new(
1145                "overflow",
1146                vec![tensor("a", vec![usize::MAX, 2])],
1147                Vec::new(),
1148                CatalogPolicy::strict(),
1149            ),
1150            Err(CheckpointPlanError::ShapeOverflow { .. })
1151        ));
1152        assert!(matches!(
1153            SafetensorsCheckpointPlan::new(
1154                "invalid element count",
1155                vec![tensor("a", vec![2, 2]).with_element_count(3)],
1156                Vec::new(),
1157                CatalogPolicy::strict(),
1158            ),
1159            Err(CheckpointPlanError::ElementCountMismatch { .. })
1160        ));
1161        assert!(matches!(
1162            SafetensorsCheckpointPlan::new(
1163                "zero element count",
1164                vec![tensor("a", vec![2, 2]).with_element_count(0)],
1165                Vec::new(),
1166                CatalogPolicy::strict(),
1167            ),
1168            Err(CheckpointPlanError::InvalidElementCount { .. })
1169        ));
1170        assert!(matches!(
1171            GgufCheckpointPlan::new(
1172                "invalid alternate",
1173                vec![GgufTensorConstraint::required(
1174                    "a",
1175                    vec![1],
1176                    GgufTypeConstraint::OperationClass(TensorOperation::Dense),
1177                )
1178                .with_alternate_shapes([vec![1, 0]])],
1179                Vec::new(),
1180                CatalogPolicy::strict(),
1181            ),
1182            Err(CheckpointPlanError::InvalidShape { .. })
1183        ));
1184        assert!(matches!(
1185            SafetensorsCheckpointPlan::new(
1186                "invalid SafeTensors alternate",
1187                vec![tensor("a", vec![1]).with_alternate_shapes([vec![1, 0]])],
1188                Vec::new(),
1189                CatalogPolicy::strict(),
1190            ),
1191            Err(CheckpointPlanError::InvalidShape { .. })
1192        ));
1193        assert!(matches!(
1194            SafetensorsCheckpointPlan::new(
1195                "empty encoding",
1196                vec![SafetensorsTensorConstraint::required(
1197                    "a",
1198                    vec![1],
1199                    StoredDtypeConstraint::OneOf(Vec::new()),
1200                )],
1201                Vec::new(),
1202                CatalogPolicy::strict(),
1203            ),
1204            Err(CheckpointPlanError::EmptyEncodingSet { .. })
1205        ));
1206        let mut empty_suffix = CatalogPolicy::strict();
1207        empty_suffix.allowed_suffixes.push(" ".into());
1208        assert!(matches!(
1209            SafetensorsCheckpointPlan::new(
1210                "empty allowed suffix",
1211                vec![tensor("a", vec![1])],
1212                Vec::new(),
1213                empty_suffix,
1214            ),
1215            Err(CheckpointPlanError::EmptyAllowedSuffix)
1216        ));
1217
1218        let shared = tensor("shared", vec![1]);
1219        let sibling_shared = SafetensorsCheckpointPlan::new(
1220            "sibling shared key",
1221            Vec::new(),
1222            vec![AlternativeLayoutGroup {
1223                id: "layout".into(),
1224                required: true,
1225                variants: vec![
1226                    LayoutVariant {
1227                        id: "a".into(),
1228                        tensors: vec![tensor("a", vec![1]), shared.clone()],
1229                        discriminator_keys: vec!["a".into()],
1230                    },
1231                    LayoutVariant {
1232                        id: "b".into(),
1233                        tensors: vec![tensor("b", vec![1]), shared],
1234                        discriminator_keys: vec!["b".into()],
1235                    },
1236                ],
1237            }],
1238            CatalogPolicy::strict(),
1239        )
1240        .unwrap();
1241        assert_eq!(sibling_shared.layout_groups[0].variants.len(), 2);
1242    }
1243
1244    #[test]
1245    fn hybrid_operator_schemas_freeze_segments_convolution_axes_and_recurrent_groups() {
1246        let projection = FusedSegmentedProjectionSchema::new(
1247            16,
1248            [
1249                FusedProjectionSegment::new("gate", 8).unwrap(),
1250                FusedProjectionSegment::new("state", 6).unwrap(),
1251                FusedProjectionSegment::new("time", 2).unwrap(),
1252            ],
1253        )
1254        .unwrap();
1255        assert_eq!(projection.matrix_shape(), [16, 16]);
1256        assert_eq!(projection.segment_ranges(), [0..8, 8..14, 14..16]);
1257        assert!(FusedSegmentedProjectionSchema::new(
1258            4,
1259            [
1260                FusedProjectionSegment::new("same", 2).unwrap(),
1261                FusedProjectionSegment::new("same", 2).unwrap(),
1262            ]
1263        )
1264        .is_err());
1265
1266        let convolution = DepthwiseConvolutionSchema::with_axes(
1267            12,
1268            3,
1269            DepthwiseKernelAxes::ChannelsKernelSingleton,
1270            DepthwiseKernelAxes::ChannelsSingletonKernel,
1271            true,
1272        )
1273        .unwrap();
1274        assert_eq!(convolution.storage_shape(), [12, 3, 1]);
1275        assert_eq!(convolution.execution_shape(), [12, 1, 3]);
1276        assert_eq!(convolution.bias_shape(), Some(vec![12]));
1277        assert_eq!(convolution.element_count(), 36);
1278
1279        let recurrent = RecurrentParameterGroupSchema::new(8, 2, 4, 3).unwrap();
1280        assert_eq!(recurrent.per_head_shape(), [8]);
1281        assert_eq!(recurrent.grouped_state_width(), 6);
1282        assert_eq!(recurrent.recurrent_state_shape(), [8, 4, 3]);
1283        assert!(RecurrentParameterGroupSchema::new(7, 2, 4, 3).is_err());
1284    }
1285}