Skip to main content

eredu_runtime/
parallel.rs

1//! Backend-neutral semantic parameter sharding and rank-local layouts.
2//!
3//! Architectures describe physical checkpoint members in logical groups. An
4//! execution backend may then realize the resulting placement without knowing
5//! projection names, attention geometry, or other model-family semantics.
6
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    ops::Range,
10};
11
12use eredu_checkpoint::LinearFormat;
13use eredu_nn::{LinearFormatSpec, ParameterMetadata, ParameterVisitor, Parameterized, Tensor};
14
15/// Architecture-neutral information for one rank-local parallel model.
16#[derive(Debug, Clone)]
17pub struct ParallelModelInfo<T> {
18    topology: T,
19    effective_model_type: String,
20    owned_tensors: Vec<String>,
21    local_parameter_bytes: u64,
22    global_parameter_bytes: u64,
23    pinned_device_parameter_bytes: u64,
24    maximum_device_parameter_bytes: u64,
25}
26
27impl<T> ParallelModelInfo<T> {
28    /// Creates a complete rank-local parallel model summary.
29    #[allow(clippy::too_many_arguments)]
30    pub fn new(
31        topology: T,
32        effective_model_type: impl Into<String>,
33        owned_tensors: Vec<String>,
34        local_parameter_bytes: u64,
35        global_parameter_bytes: u64,
36        pinned_device_parameter_bytes: u64,
37        maximum_device_parameter_bytes: u64,
38    ) -> Self {
39        Self {
40            topology,
41            effective_model_type: effective_model_type.into(),
42            owned_tensors,
43            local_parameter_bytes,
44            global_parameter_bytes,
45            pinned_device_parameter_bytes,
46            maximum_device_parameter_bytes,
47        }
48    }
49
50    /// Returns the backend's concrete topology value unchanged.
51    pub fn topology(&self) -> T
52    where
53        T: Clone,
54    {
55        self.topology.clone()
56    }
57
58    /// Returns the parsed implementation or nested text-model type.
59    pub fn effective_model_type(&self) -> &str {
60        &self.effective_model_type
61    }
62
63    /// Returns exact checkpoint targets owned or replicated by this rank.
64    pub fn owned_tensors(&self) -> &[String] {
65        &self.owned_tensors
66    }
67
68    /// Returns planned rank-local parameter bytes across static and execution units.
69    pub const fn local_parameter_bytes(&self) -> u64 {
70        self.local_parameter_bytes
71    }
72
73    /// Returns the unsharded model parameter bytes represented by this checkpoint.
74    pub const fn global_parameter_bytes(&self) -> u64 {
75        self.global_parameter_bytes
76    }
77
78    /// Returns rank-local parameter bytes permanently pinned on the execution device.
79    pub const fn pinned_device_parameter_bytes(&self) -> u64 {
80        self.pinned_device_parameter_bytes
81    }
82
83    /// Returns the maximum planned rank-local parameter footprint on device.
84    pub const fn maximum_device_parameter_bytes(&self) -> u64 {
85        self.maximum_device_parameter_bytes
86    }
87}
88
89/// Semantic role of a logical parameter group.
90#[derive(Debug, Clone, Copy, Eq, PartialEq)]
91pub enum ParameterRole {
92    /// Small or otherwise non-partitioned state.
93    Replicated,
94    /// Projection whose output features are rank-local.
95    ColumnProjection,
96    /// Projection whose input features are rank-local and whose output is reduced.
97    RowProjection,
98    /// Token embedding or output projection partitioned by vocabulary.
99    Vocabulary,
100    /// Query, key, or value heads.
101    AttentionHeads,
102    /// Dense feed-forward intermediate channels shared by input and output projections.
103    FeedForwardIntermediate,
104    /// Routed expert intermediate channels partitioned over the expert axis.
105    ExpertIntermediate,
106    /// Always-on expert intermediate channels replicated over the expert axis.
107    SharedExpertIntermediate,
108    /// State-space, convolution, or recurrent channels.
109    Channels,
110    /// A fused tensor containing independently partitioned segments.
111    Segmented,
112}
113
114/// Logical sharding behavior for a parameterized affine projection.
115#[derive(Debug, Clone, Copy, Eq, PartialEq)]
116pub enum ProjectionSharding {
117    /// Keep every projection parameter complete on every rank.
118    Replicated,
119    /// Partition projection output features.
120    Column,
121    /// Partition projection input features and replicate output bias.
122    Row,
123}
124
125/// Rank-local selection rule for one physical checkpoint tensor.
126#[derive(Debug, Clone, Eq, PartialEq)]
127pub enum MemberSharding {
128    /// Materialize the complete member on every tensor-parallel rank.
129    Replicated,
130    /// Split an axis into equal contiguous shards.
131    Equal {
132        /// Source tensor axis to partition.
133        axis: usize,
134    },
135    /// Split an axis into balanced, potentially uneven contiguous ranges.
136    Balanced {
137        /// Source tensor axis to partition.
138        axis: usize,
139    },
140    /// Map the group's logical partition onto one physical tensor axis.
141    Partitioned {
142        /// Source tensor axis to partition.
143        axis: usize,
144    },
145    /// Map the same group-level logical range into each supplied source segment.
146    PartitionedSegments {
147        /// Source tensor axis containing the fused segments.
148        axis: usize,
149        /// Ordered, non-overlapping physical source ranges.
150        segments: Vec<Range<usize>>,
151    },
152    /// Partition each supplied source range independently.
153    Segmented {
154        /// Source tensor axis containing the fused segments.
155        axis: usize,
156        /// Ordered, non-overlapping source ranges.
157        segments: Vec<Range<usize>>,
158    },
159}
160
161/// One physical tensor belonging to a logical parameter group.
162#[derive(Debug, Clone, Eq, PartialEq)]
163pub struct ParameterMemberSpec {
164    target: String,
165    global_shape: Vec<usize>,
166    sharding: MemberSharding,
167    linear_companion: Option<eredu_nn::LinearCompanionRole>,
168    linear_companion_of: Option<String>,
169}
170
171impl ParameterMemberSpec {
172    /// Creates a member with an exact pre-selection checkpoint shape.
173    pub fn new(
174        target: impl Into<String>,
175        global_shape: impl Into<Vec<usize>>,
176        sharding: MemberSharding,
177    ) -> Self {
178        Self {
179            target: target.into(),
180            global_shape: global_shape.into(),
181            sharding,
182            linear_companion: None,
183            linear_companion_of: None,
184        }
185    }
186
187    fn with_parameter_metadata(mut self, metadata: &ParameterMetadata) -> Self {
188        self.linear_companion = metadata.linear_companion;
189        self.linear_companion_of = metadata
190            .linear_companion_of
191            .as_ref()
192            .map(|parameter| parameter.as_str().to_owned());
193        self
194    }
195
196    fn with_sharding(mut self, sharding: MemberSharding) -> Self {
197        self.sharding = sharding;
198        self
199    }
200
201    fn with_linear_companion(mut self, role: eredu_nn::LinearCompanionRole, primary: &str) -> Self {
202        self.linear_companion = Some(role);
203        self.linear_companion_of = Some(primary.to_owned());
204        self
205    }
206
207    /// Returns the rewritten checkpoint target.
208    pub fn target(&self) -> &str {
209        &self.target
210    }
211
212    /// Returns the complete source shape.
213    pub fn global_shape(&self) -> &[usize] {
214        &self.global_shape
215    }
216
217    /// Returns the requested rank-local selection.
218    pub const fn sharding(&self) -> &MemberSharding {
219        &self.sharding
220    }
221
222    /// Returns this member's encoded-linear companion role, when present.
223    pub const fn linear_companion(&self) -> Option<eredu_nn::LinearCompanionRole> {
224        self.linear_companion
225    }
226
227    /// Returns the primary linear weight owning this companion.
228    pub fn linear_companion_of(&self) -> Option<&str> {
229        self.linear_companion_of.as_deref()
230    }
231}
232
233/// Atomic logical parameter and all of its physical checkpoint companions.
234#[derive(Debug, Clone, Eq, PartialEq)]
235pub struct ParameterGroupSpec {
236    logical_name: String,
237    role: ParameterRole,
238    partition_units: Option<usize>,
239    members: Vec<ParameterMemberSpec>,
240}
241
242impl ParameterGroupSpec {
243    /// Creates a non-empty logical group.
244    pub fn new(
245        logical_name: impl Into<String>,
246        role: ParameterRole,
247        members: impl IntoIterator<Item = ParameterMemberSpec>,
248    ) -> Result<Self, ParallelPlanError> {
249        Self::build(logical_name.into(), role, None, members)
250    }
251
252    /// Creates a group whose partitioned members share one logical domain.
253    pub fn partitioned(
254        logical_name: impl Into<String>,
255        role: ParameterRole,
256        units: usize,
257        members: impl IntoIterator<Item = ParameterMemberSpec>,
258    ) -> Result<Self, ParallelPlanError> {
259        if units == 0 {
260            return Err(ParallelPlanError::InvalidGroup(
261                "parallel logical partition must contain at least one unit".into(),
262            ));
263        }
264        Self::build(logical_name.into(), role, Some(units), members)
265    }
266
267    fn build(
268        logical_name: String,
269        role: ParameterRole,
270        partition_units: Option<usize>,
271        members: impl IntoIterator<Item = ParameterMemberSpec>,
272    ) -> Result<Self, ParallelPlanError> {
273        if logical_name.trim().is_empty() {
274            return Err(ParallelPlanError::InvalidGroup(
275                "parallel parameter logical name must not be empty".into(),
276            ));
277        }
278        let members = members.into_iter().collect::<Vec<_>>();
279        if members.is_empty() {
280            return Err(ParallelPlanError::InvalidGroup(format!(
281                "parallel parameter group {logical_name:?} must contain at least one tensor"
282            )));
283        }
284        let mut targets = BTreeSet::new();
285        let mut has_partitioned_member = false;
286        for member in &members {
287            if member.target.trim().is_empty() {
288                return Err(ParallelPlanError::InvalidGroup(format!(
289                    "parallel parameter group {logical_name:?} contains an empty tensor target"
290                )));
291            }
292            if !targets.insert(member.target.clone()) {
293                return Err(ParallelPlanError::InvalidGroup(format!(
294                    "parallel parameter group {logical_name:?} repeats tensor target {:?}",
295                    member.target
296                )));
297            }
298            has_partitioned_member |= matches!(
299                member.sharding,
300                MemberSharding::Partitioned { .. } | MemberSharding::PartitionedSegments { .. }
301            );
302        }
303        if has_partitioned_member != partition_units.is_some() {
304            return Err(ParallelPlanError::InvalidGroup(format!(
305                "parallel parameter group {logical_name:?} must declare exactly one group-level logical partition for its partitioned members"
306            )));
307        }
308        Ok(Self {
309            logical_name,
310            role,
311            partition_units,
312            members,
313        })
314    }
315
316    /// Returns the stable logical name.
317    pub fn logical_name(&self) -> &str {
318        &self.logical_name
319    }
320
321    /// Returns the semantic role.
322    pub const fn role(&self) -> ParameterRole {
323        self.role
324    }
325
326    /// Returns the shared logical-unit count, when the group is partitioned.
327    pub const fn partition_units(&self) -> Option<usize> {
328        self.partition_units
329    }
330
331    /// Returns physical checkpoint members.
332    pub fn members(&self) -> &[ParameterMemberSpec] {
333        &self.members
334    }
335}
336
337/// Describes every parameter in a neutral module as one logical group.
338pub fn module_parameter_group<T, M>(
339    logical_name: impl Into<String>,
340    role: ParameterRole,
341    module: &M,
342    mut sharding: impl FnMut(&ParameterMetadata, &[usize]) -> Result<MemberSharding, ParallelPlanError>,
343) -> Result<ParameterGroupSpec, ParallelPlanError>
344where
345    T: Tensor,
346    M: Parameterized<T>,
347{
348    struct Collector<'a, F> {
349        members: Vec<ParameterMemberSpec>,
350        sharding: &'a mut F,
351        error: Option<ParallelPlanError>,
352    }
353
354    impl<'a, 'tensor, T, F> ParameterVisitor<'tensor, T> for Collector<'a, F>
355    where
356        T: Tensor,
357        F: FnMut(&ParameterMetadata, &[usize]) -> Result<MemberSharding, ParallelPlanError>,
358    {
359        fn visit(&mut self, metadata: ParameterMetadata, value: &'tensor T) {
360            if self.error.is_some() {
361                return;
362            }
363            let shape = value
364                .shape()
365                .iter()
366                .map(|dimension| {
367                    usize::try_from(*dimension).map_err(|_| {
368                        ParallelPlanError::InvalidTensor(format!(
369                            "parameter {} has negative dimension {dimension}",
370                            metadata.id.as_str()
371                        ))
372                    })
373                })
374                .collect::<Result<Vec<_>, _>>();
375            let shape = match shape {
376                Ok(shape) => shape,
377                Err(error) => {
378                    self.error = Some(error);
379                    return;
380                }
381            };
382            match (self.sharding)(&metadata, &shape) {
383                Ok(sharding) => self.members.push(
384                    ParameterMemberSpec::new(metadata.id.as_str(), shape, sharding)
385                        .with_parameter_metadata(&metadata),
386                ),
387                Err(error) => self.error = Some(error),
388            }
389        }
390    }
391
392    let mut collector = Collector {
393        members: Vec::new(),
394        sharding: &mut sharding,
395        error: None,
396    };
397    module.visit_parameters(&mut collector);
398    if let Some(error) = collector.error {
399        return Err(error);
400    }
401    ParameterGroupSpec::new(logical_name, role, collector.members)
402}
403
404/// Describes every parameter in a neutral module as one shared logical partition.
405pub fn partitioned_module_parameter_group<T, M>(
406    logical_name: impl Into<String>,
407    role: ParameterRole,
408    preferred_units: usize,
409    module: &M,
410    mut sharding: impl FnMut(&ParameterMetadata, &[usize]) -> Result<MemberSharding, ParallelPlanError>,
411) -> Result<ParameterGroupSpec, ParallelPlanError>
412where
413    T: Tensor,
414    M: Parameterized<T>,
415{
416    if preferred_units == 0 {
417        return Err(ParallelPlanError::InvalidGroup(
418            "partitioned module group has zero preferred units".into(),
419        ));
420    }
421    struct Collector<'a, F> {
422        members: Vec<ParameterMemberSpec>,
423        sharding: &'a mut F,
424        error: Option<ParallelPlanError>,
425    }
426    impl<'a, 'tensor, T, F> ParameterVisitor<'tensor, T> for Collector<'a, F>
427    where
428        T: Tensor,
429        F: FnMut(&ParameterMetadata, &[usize]) -> Result<MemberSharding, ParallelPlanError>,
430    {
431        fn visit(&mut self, metadata: ParameterMetadata, value: &'tensor T) {
432            if self.error.is_some() {
433                return;
434            }
435            let shape = value
436                .shape()
437                .iter()
438                .map(|dimension| {
439                    usize::try_from(*dimension).map_err(|_| {
440                        ParallelPlanError::InvalidTensor(format!(
441                            "parameter {} has negative dimension {dimension}",
442                            metadata.id.as_str()
443                        ))
444                    })
445                })
446                .collect::<Result<Vec<_>, _>>();
447            match shape.and_then(|shape| {
448                (self.sharding)(&metadata, &shape).map(|sharding| {
449                    ParameterMemberSpec::new(metadata.id.as_str(), shape, sharding)
450                        .with_parameter_metadata(&metadata)
451                })
452            }) {
453                Ok(member) => self.members.push(member),
454                Err(error) => self.error = Some(error),
455            }
456        }
457    }
458    let mut collector = Collector {
459        members: Vec::new(),
460        sharding: &mut sharding,
461        error: None,
462    };
463    module.visit_parameters(&mut collector);
464    if let Some(error) = collector.error {
465        return Err(error);
466    }
467    partitioned_group_with_preferred_units(logical_name, role, preferred_units, collector.members)
468}
469
470/// Describes one affine projection and all encoding companions.
471pub fn projection_parameter_group<T, M>(
472    logical_name: impl Into<String>,
473    role: ParameterRole,
474    module: &M,
475    placement: ProjectionSharding,
476) -> Result<ParameterGroupSpec, ParallelPlanError>
477where
478    T: Tensor,
479    M: Parameterized<T>,
480{
481    module_parameter_group(
482        logical_name,
483        role,
484        module,
485        |metadata, shape| match placement {
486            ProjectionSharding::Replicated => Ok(MemberSharding::Replicated),
487            ProjectionSharding::Column if shape.is_empty() => {
488                Err(ParallelPlanError::InvalidTensor(format!(
489                    "column projection parameter {} is scalar",
490                    metadata.id.as_str()
491                )))
492            }
493            ProjectionSharding::Column => Ok(MemberSharding::Equal { axis: 0 }),
494            ProjectionSharding::Row if shape.len() >= 2 => Ok(MemberSharding::Equal { axis: 1 }),
495            ProjectionSharding::Row => Ok(MemberSharding::Replicated),
496        },
497    )
498}
499
500/// Describes projections that consume one shared logical partition.
501pub fn partitioned_projection_group<T, M>(
502    logical_name: impl Into<String>,
503    role: ParameterRole,
504    projections: &[(&M, ProjectionSharding)],
505    preferred_units: usize,
506) -> Result<ParameterGroupSpec, ParallelPlanError>
507where
508    T: Tensor,
509    M: Parameterized<T>,
510{
511    if preferred_units == 0 {
512        return Err(ParallelPlanError::InvalidGroup(
513            "partitioned projection group has zero preferred units".into(),
514        ));
515    }
516    let mut members = Vec::new();
517    for (module, placement) in projections {
518        let group = projection_parameter_group::<T, M>("projection", role, *module, *placement)?;
519        for member in group.members {
520            let sharding = match (placement, member.global_shape.len()) {
521                (ProjectionSharding::Replicated, _) | (ProjectionSharding::Row, 0 | 1) => {
522                    MemberSharding::Replicated
523                }
524                (ProjectionSharding::Column, 0) => unreachable!("validated above"),
525                (ProjectionSharding::Column, _) => MemberSharding::Partitioned { axis: 0 },
526                (ProjectionSharding::Row, _) => MemberSharding::Partitioned { axis: 1 },
527            };
528            members.push(member.with_sharding(sharding));
529        }
530    }
531    partitioned_group_with_preferred_units(logical_name, role, preferred_units, members)
532}
533
534/// Describes a component-major fused column projection and its row-parallel
535/// output as one shared logical partition.
536///
537/// The same ordered segment selection is attached to the fused weight and all
538/// encoding companions exposed by the module. The row projection consumes the
539/// corresponding local hidden partition and is reduced once by the backend.
540pub fn segmented_projection_group<T, M>(
541    logical_name: impl Into<String>,
542    role: ParameterRole,
543    fused: &M,
544    row: &M,
545    segments: Vec<Range<usize>>,
546    preferred_units: usize,
547) -> Result<ParameterGroupSpec, ParallelPlanError>
548where
549    T: Tensor,
550    M: Parameterized<T>,
551{
552    if preferred_units == 0 || segments.is_empty() {
553        return Err(ParallelPlanError::InvalidGroup(
554            "segmented projection requires positive logical units and at least one segment".into(),
555        ));
556    }
557    let mut previous_end = 0usize;
558    for segment in &segments {
559        if segment.start != previous_end || segment.start >= segment.end {
560            return Err(ParallelPlanError::InvalidGroup(format!(
561                "segmented projection ranges must be positive, contiguous, and ordered, got {segments:?}"
562            )));
563        }
564        previous_end = segment.end;
565    }
566
567    let fused_group =
568        projection_parameter_group::<T, M>("fused", role, fused, ProjectionSharding::Column)?;
569    let row_group = projection_parameter_group::<T, M>("row", role, row, ProjectionSharding::Row)?;
570    assemble_segmented_projection_group(
571        logical_name,
572        role,
573        fused_group,
574        row_group,
575        segments,
576        preferred_units,
577        previous_end,
578    )
579}
580
581#[allow(clippy::too_many_arguments)]
582fn assemble_segmented_projection_group(
583    logical_name: impl Into<String>,
584    role: ParameterRole,
585    fused_group: ParameterGroupSpec,
586    row_group: ParameterGroupSpec,
587    segments: Vec<Range<usize>>,
588    units: usize,
589    expected_fused_width: usize,
590) -> Result<ParameterGroupSpec, ParallelPlanError> {
591    let mut members = Vec::new();
592    for member in fused_group.members {
593        let dimension = member.global_shape.first().copied().ok_or_else(|| {
594            ParallelPlanError::InvalidTensor(format!(
595                "segmented projection parameter {} is scalar",
596                member.target
597            ))
598        })?;
599        if dimension != expected_fused_width {
600            return Err(ParallelPlanError::InvalidTensor(format!(
601                "segmented projection parameter {} has output dimension {dimension}, expected {expected_fused_width}",
602                member.target
603            )));
604        }
605        members.push(member.with_sharding(MemberSharding::PartitionedSegments {
606            axis: 0,
607            segments: segments.clone(),
608        }));
609    }
610    for member in row_group.members {
611        let sharding = if member.global_shape.len() >= 2 {
612            MemberSharding::Partitioned { axis: 1 }
613        } else {
614            MemberSharding::Replicated
615        };
616        members.push(member.with_sharding(sharding));
617    }
618    partitioned_group_with_preferred_units(logical_name, role, units, members)
619}
620
621/// Returns the finest legal logical-unit count for an aligned partition.
622pub fn aligned_partition_units(
623    name: &str,
624    semantic_units: usize,
625    elements_per_unit: usize,
626    required_alignment: usize,
627) -> Result<usize, ParallelPlanError> {
628    if semantic_units == 0 || elements_per_unit == 0 || required_alignment == 0 {
629        return Err(ParallelPlanError::InvalidGroup(format!(
630            "{name} aligned partition dimensions must be positive, got units={semantic_units}, width={elements_per_unit}, alignment={required_alignment}"
631        )));
632    }
633    let units_per_partition =
634        required_alignment / greatest_common_divisor(elements_per_unit, required_alignment);
635    if !semantic_units.is_multiple_of(units_per_partition) {
636        return Err(ParallelPlanError::InvalidGroup(format!(
637            "{name} has {semantic_units} semantic units of width {elements_per_unit}, which cannot form complete alignment-{required_alignment} partitions"
638        )));
639    }
640    Ok(semantic_units / units_per_partition)
641}
642
643/// Rewrites semantic dense matrix declarations into their authoritative
644/// physical checkpoint representation and publishes every required companion
645/// in the same atomic parameter group.
646pub fn expand_linear_format_parameter_groups(
647    groups: Vec<ParameterGroupSpec>,
648    declaration: impl Fn(&ParameterMemberSpec) -> Result<Option<LinearFormatSpec>, ParallelPlanError>,
649) -> Result<Vec<ParameterGroupSpec>, ParallelPlanError> {
650    groups
651        .into_iter()
652        .map(|group| {
653            let mut members = Vec::new();
654            for source in group.members() {
655                members.extend(match declaration(source)? {
656                    Some(declaration) => expand_linear_format_member(source, &declaration)?,
657                    None => vec![source.clone()],
658                });
659            }
660            match group.partition_units() {
661                Some(units) => partitioned_group_with_preferred_units(
662                    group.logical_name(),
663                    group.role(),
664                    units,
665                    members,
666                ),
667                None => ParameterGroupSpec::new(group.logical_name(), group.role(), members),
668            }
669        })
670        .collect()
671}
672
673fn partitioned_group_with_preferred_units(
674    logical_name: impl Into<String>,
675    role: ParameterRole,
676    preferred_units: usize,
677    members: Vec<ParameterMemberSpec>,
678) -> Result<ParameterGroupSpec, ParallelPlanError> {
679    let mut units = preferred_units;
680    for member in &members {
681        match member.sharding() {
682            MemberSharding::Partitioned { axis } => {
683                let dimension = member.global_shape().get(*axis).ok_or_else(|| {
684                    ParallelPlanError::InvalidTensor(format!(
685                        "partitioned parameter {} has no axis {axis}",
686                        member.target()
687                    ))
688                })?;
689                units = greatest_common_divisor(units, *dimension);
690            }
691            MemberSharding::PartitionedSegments { axis, segments }
692            | MemberSharding::Segmented { axis, segments } => {
693                if member.global_shape().get(*axis).is_none() {
694                    return Err(ParallelPlanError::InvalidTensor(format!(
695                        "segmented parameter {} has no axis {axis}",
696                        member.target()
697                    )));
698                }
699                for segment in segments {
700                    units = greatest_common_divisor(units, segment.len());
701                }
702            }
703            MemberSharding::Replicated
704            | MemberSharding::Equal { .. }
705            | MemberSharding::Balanced { .. } => {}
706        }
707    }
708    ParameterGroupSpec::partitioned(logical_name, role, units, members)
709}
710
711fn remap_linear_segments(
712    sharding: &MemberSharding,
713    axis: usize,
714    divisor: usize,
715    name: &str,
716) -> Result<MemberSharding, ParallelPlanError> {
717    let remap = |segments: &[Range<usize>]| {
718        segments
719            .iter()
720            .map(|segment| {
721                if !segment.start.is_multiple_of(divisor) || !segment.end.is_multiple_of(divisor) {
722                    return Err(ParallelPlanError::InvalidTensor(format!(
723                        "packed companion {name} segment {segment:?} is not aligned to {divisor}"
724                    )));
725                }
726                Ok(segment.start / divisor..segment.end / divisor)
727            })
728            .collect::<Result<Vec<_>, _>>()
729    };
730    match sharding {
731        MemberSharding::PartitionedSegments {
732            axis: selected,
733            segments,
734        } if *selected == axis => Ok(MemberSharding::PartitionedSegments {
735            axis: *selected,
736            segments: remap(segments)?,
737        }),
738        MemberSharding::Segmented {
739            axis: selected,
740            segments,
741        } if *selected == axis => Ok(MemberSharding::Segmented {
742            axis: *selected,
743            segments: remap(segments)?,
744        }),
745        other => Ok(other.clone()),
746    }
747}
748
749fn expand_linear_format_member(
750    source: &ParameterMemberSpec,
751    declaration: &LinearFormatSpec,
752) -> Result<Vec<ParameterMemberSpec>, ParallelPlanError> {
753    let name = source.target();
754    let shape = source.global_shape();
755    let format = declaration.encoding();
756    if format == LinearFormat::Dense {
757        return if declaration.scale().is_none() && declaration.affine_bias().is_none() {
758            Ok(vec![source.clone()])
759        } else {
760            Err(ParallelPlanError::InvalidGroup(format!(
761                "dense linear parameter {name} declares physical companions"
762            )))
763        };
764    }
765    if shape.len() < 2 {
766        return Err(ParallelPlanError::InvalidTensor(format!(
767            "encoded linear parameter {name} must have at least two dimensions"
768        )));
769    }
770    let row_axis = shape.len() - 2;
771    let column_axis = shape.len() - 1;
772    let invalid = |detail: String| ParallelPlanError::InvalidTensor(detail);
773    match format {
774        LinearFormat::Dense => unreachable!(),
775        LinearFormat::E4M3BlockFp8(fp8) => {
776            let Some(scale) = declaration.scale() else {
777                return Err(ParallelPlanError::InvalidGroup(format!(
778                    "block-FP8 linear parameter {name} must declare exactly one scale companion"
779                )));
780            };
781            if declaration.affine_bias().is_some() {
782                return Err(ParallelPlanError::InvalidGroup(format!(
783                    "block-FP8 linear parameter {name} must not declare an affine-bias companion"
784                )));
785            }
786            fp8.validate().map_err(|error| invalid(error.to_string()))?;
787            let rows = usize::try_from(fp8.block_rows)
788                .map_err(|_| invalid(format!("invalid block rows for {name}")))?;
789            let columns = usize::try_from(fp8.block_columns)
790                .map_err(|_| invalid(format!("invalid block columns for {name}")))?;
791            let mut scale_shape = shape.to_vec();
792            scale_shape[row_axis] = scale_shape[row_axis].div_ceil(rows);
793            scale_shape[column_axis] = scale_shape[column_axis].div_ceil(columns);
794            let scale_sharding = remap_linear_segments(source.sharding(), row_axis, rows, name)
795                .and_then(|value| remap_linear_segments(&value, column_axis, columns, name))?;
796            Ok(vec![
797                source.clone(),
798                ParameterMemberSpec::new(scale.id.as_str(), scale_shape, scale_sharding)
799                    .with_linear_companion(eredu_nn::LinearCompanionRole::Scale, name),
800            ])
801        }
802        LinearFormat::GgufIQuant { ggml_type, .. } => {
803            if declaration.scale().is_some() || declaration.affine_bias().is_some() {
804                return Err(ParallelPlanError::InvalidGroup(format!(
805                    "GGUF linear parameter {name} must not declare companion tensors"
806                )));
807            }
808            let (block_values, block_bytes) = ggml_type
809                .block_and_bytes()
810                .map_err(|error| invalid(error.to_string()))?;
811            let block_values = usize::try_from(block_values)
812                .map_err(|_| invalid(format!("GGUF block width for {name} exceeds usize")))?;
813            let block_bytes = usize::try_from(block_bytes)
814                .map_err(|_| invalid(format!("GGUF block bytes for {name} exceeds usize")))?;
815            let input = shape[column_axis];
816            if !input.is_multiple_of(block_values) {
817                return Err(invalid(format!(
818                    "GGUF matrix {name} input {input} is not aligned to block {block_values}"
819                )));
820            }
821            let mut packed = shape.to_vec();
822            packed[column_axis] = input / block_values * block_bytes;
823            Ok(vec![ParameterMemberSpec::new(
824                name,
825                packed,
826                remap_linear_segments(source.sharding(), column_axis, block_values, name)?,
827            )])
828        }
829        LinearFormat::Affine(_) | LinearFormat::MxFp4 => {
830            let quantization = format.weight_quantization().expect("packed format");
831            let Some(scale) = declaration.scale() else {
832                return Err(ParallelPlanError::InvalidGroup(format!(
833                    "packed linear parameter {name} must declare a scale companion"
834                )));
835            };
836            let bias = declaration.affine_bias();
837            if quantization.has_biases() != bias.is_some() {
838                return Err(ParallelPlanError::InvalidGroup(format!(
839                    "packed linear parameter {name} declares companions inconsistent with its format"
840                )));
841            }
842            let bits = usize::try_from(quantization.bits())
843                .map_err(|_| invalid(format!("packed bit width for {name} exceeds usize")))?;
844            let group = usize::try_from(quantization.group_size())
845                .map_err(|_| invalid(format!("packed group width for {name} exceeds usize")))?;
846            let input = shape[column_axis];
847            let packed_bits = input
848                .checked_mul(bits)
849                .ok_or_else(|| invalid(format!("packed matrix {name} overflows")))?;
850            if group == 0 || !input.is_multiple_of(group) || !packed_bits.is_multiple_of(32) {
851                return Err(invalid(format!(
852                    "packed matrix {name} input {input} is incompatible with group {group} and {bits} bits"
853                )));
854            }
855            let mut packed = shape.to_vec();
856            packed[column_axis] = packed_bits / 32;
857            let mut companion = shape.to_vec();
858            companion[column_axis] = input / group;
859            let mut members = vec![ParameterMemberSpec::new(
860                name,
861                packed,
862                remap_linear_segments(source.sharding(), column_axis, 32 / bits, name)?,
863            )];
864            let companion_sharding =
865                remap_linear_segments(source.sharding(), column_axis, group, name)?;
866            members.push(
867                ParameterMemberSpec::new(
868                    scale.id.as_str(),
869                    companion.clone(),
870                    companion_sharding.clone(),
871                )
872                .with_linear_companion(eredu_nn::LinearCompanionRole::Scale, name),
873            );
874            if let Some(bias) = bias {
875                members.push(
876                    ParameterMemberSpec::new(bias.id.as_str(), companion, companion_sharding)
877                        .with_linear_companion(eredu_nn::LinearCompanionRole::AffineBias, name),
878                );
879            }
880            Ok(members)
881        }
882    }
883}
884
885const fn greatest_common_divisor(mut left: usize, mut right: usize) -> usize {
886    while right != 0 {
887        let remainder = left % right;
888        left = right;
889        right = remainder;
890    }
891    left
892}
893
894/// Behavior when a requested shard is not legal for the current TP size.
895#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
896pub enum ShardingPolicy {
897    /// Reject the complete plan with a precise shape/alignment error.
898    #[default]
899    Require,
900    /// Replicate the complete logical parameter group.
901    ReplicateUnsupported,
902}
903
904/// Backend-neutral placement decision for one physical tensor.
905#[derive(Debug, Clone, Eq, PartialEq)]
906pub enum TensorPlacement {
907    /// Materialize the complete tensor on every rank.
908    Replicated,
909    /// Materialize the complete tensor on this rank.
910    Local,
911    /// Intentionally omit this tensor on this rank.
912    Omit,
913    /// Materialize the complete tensor only on one global rank.
914    Rank {
915        /// Owning global rank.
916        rank: usize,
917    },
918    /// Materialize an equal contiguous source-tensor slice.
919    Shard {
920        /// Source tensor axis being sharded.
921        axis: usize,
922        /// Shard index.
923        index: usize,
924        /// Total shard count.
925        parts: usize,
926    },
927    /// Materialize an explicit contiguous source-tensor range.
928    Range {
929        /// Source tensor axis being sliced.
930        axis: usize,
931        /// Inclusive element offset on `axis`.
932        start: usize,
933        /// Exclusive element offset on `axis`.
934        end: usize,
935    },
936    /// Materialize selected source-tensor indices in the supplied order.
937    Indices {
938        /// Source tensor axis being selected.
939        axis: usize,
940        /// Distinct source indices in local output order.
941        indices: Vec<usize>,
942    },
943}
944
945/// Rank-local shape and placement for one planned physical tensor.
946#[derive(Debug, Clone, Eq, PartialEq)]
947pub struct LocalTensorLayout<P = TensorPlacement> {
948    logical_name: String,
949    role: ParameterRole,
950    global_shape: Vec<usize>,
951    local_shape: Vec<usize>,
952    placement: P,
953    additional_placements: Vec<TensorPlacement>,
954    logical_units: Option<usize>,
955    logical_range: Option<Range<usize>>,
956    fell_back_to_replication: bool,
957}
958
959impl<P> LocalTensorLayout<P> {
960    /// Creates one validated-planner output entry.
961    #[allow(clippy::too_many_arguments)]
962    pub fn new(
963        logical_name: impl Into<String>,
964        role: ParameterRole,
965        global_shape: Vec<usize>,
966        local_shape: Vec<usize>,
967        placement: P,
968        logical_units: Option<usize>,
969        logical_range: Option<Range<usize>>,
970        fell_back_to_replication: bool,
971    ) -> Self {
972        Self {
973            logical_name: logical_name.into(),
974            role,
975            global_shape,
976            local_shape,
977            placement,
978            additional_placements: Vec::new(),
979            logical_units,
980            logical_range,
981            fell_back_to_replication,
982        }
983    }
984
985    /// Returns the logical parameter group name.
986    pub fn logical_name(&self) -> &str {
987        &self.logical_name
988    }
989
990    /// Returns the semantic parameter role.
991    pub const fn role(&self) -> ParameterRole {
992        self.role
993    }
994
995    /// Returns the checkpoint-global shape.
996    pub fn global_shape(&self) -> &[usize] {
997        &self.global_shape
998    }
999
1000    /// Returns the shape materialized on this rank.
1001    pub fn local_shape(&self) -> &[usize] {
1002        &self.local_shape
1003    }
1004
1005    /// Returns the backend-realized placement.
1006    pub const fn placement(&self) -> &P {
1007        &self.placement
1008    }
1009
1010    /// Returns independent checkpoint-global selections applied before the
1011    /// primary placement.
1012    ///
1013    /// This is used when distinct parallel axes own distinct tensor axes, such
1014    /// as EP selection of packed experts followed by TP selection of each
1015    /// expert matrix. Empty means the primary placement is complete.
1016    pub fn additional_placements(&self) -> &[TensorPlacement] {
1017        &self.additional_placements
1018    }
1019
1020    /// Adds one exact checkpoint-global selection preceding the primary
1021    /// placement.
1022    pub fn with_additional_placement(mut self, placement: TensorPlacement) -> Self {
1023        self.additional_placements.push(placement);
1024        self
1025    }
1026
1027    /// Returns the rank-local range in the parameter group's semantic domain.
1028    pub fn logical_range(&self) -> Option<&Range<usize>> {
1029        self.logical_range.as_ref()
1030    }
1031
1032    /// Returns the size of the complete semantic partition domain.
1033    pub const fn logical_units(&self) -> Option<usize> {
1034        self.logical_units
1035    }
1036
1037    /// Returns whether permissive planning replicated an unsupported shard.
1038    pub const fn fell_back_to_replication(&self) -> bool {
1039        self.fell_back_to_replication
1040    }
1041}
1042
1043/// Complete rank-local model geometry produced alongside checkpoint placement.
1044#[derive(Debug, Clone, Eq, PartialEq)]
1045pub struct LocalModelLayout<P = TensorPlacement> {
1046    tensors: BTreeMap<String, LocalTensorLayout<P>>,
1047}
1048
1049impl<P> Default for LocalModelLayout<P> {
1050    fn default() -> Self {
1051        Self {
1052            tensors: BTreeMap::new(),
1053        }
1054    }
1055}
1056
1057impl<P> LocalModelLayout<P> {
1058    /// Returns whether a physical target has already been planned.
1059    pub fn contains(&self, target: &str) -> bool {
1060        self.tensors.contains_key(target)
1061    }
1062
1063    /// Inserts one planner-produced physical layout.
1064    pub fn insert(&mut self, target: String, layout: LocalTensorLayout<P>) {
1065        self.tensors.insert(target, layout);
1066    }
1067
1068    /// Returns one physical tensor layout by rewritten target name.
1069    pub fn tensor(&self, target: &str) -> Option<&LocalTensorLayout<P>> {
1070        self.tensors.get(target)
1071    }
1072
1073    /// Iterates physical layouts in deterministic target-name order.
1074    pub fn tensors(&self) -> impl Iterator<Item = (&str, &LocalTensorLayout<P>)> {
1075        self.tensors
1076            .iter()
1077            .map(|(target, layout)| (target.as_str(), layout))
1078    }
1079
1080    /// Returns the number of planned physical tensors.
1081    pub fn len(&self) -> usize {
1082        self.tensors.len()
1083    }
1084
1085    /// Returns whether no physical tensors were planned.
1086    pub fn is_empty(&self) -> bool {
1087        self.tensors.is_empty()
1088    }
1089}
1090
1091/// Invalid architecture-declared parallel semantics.
1092#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1093pub enum ParallelPlanError {
1094    /// A logical group is empty, ambiguous, or internally inconsistent.
1095    #[error("{0}")]
1096    InvalidGroup(String),
1097    /// A backend-native parameter exposed invalid logical geometry.
1098    #[error("{0}")]
1099    InvalidTensor(String),
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104    use eredu_checkpoint::{BlockFp8Format, BlockFp8ScaleEncoding};
1105
1106    use super::*;
1107
1108    #[test]
1109    fn groups_reject_duplicate_physical_targets() {
1110        let error = ParameterGroupSpec::new(
1111            "attention",
1112            ParameterRole::AttentionHeads,
1113            [
1114                ParameterMemberSpec::new("q.weight", [8, 8], MemberSharding::Replicated),
1115                ParameterMemberSpec::new("q.weight", [8, 8], MemberSharding::Replicated),
1116            ],
1117        )
1118        .unwrap_err();
1119        assert!(error.to_string().contains("repeats tensor target"));
1120    }
1121
1122    #[test]
1123    fn group_partition_contract_is_explicit() {
1124        assert!(ParameterGroupSpec::new(
1125            "query",
1126            ParameterRole::AttentionHeads,
1127            [ParameterMemberSpec::new(
1128                "q.weight",
1129                [8, 8],
1130                MemberSharding::Partitioned { axis: 0 },
1131            )],
1132        )
1133        .is_err());
1134        assert!(ParameterGroupSpec::partitioned(
1135            "query",
1136            ParameterRole::AttentionHeads,
1137            4,
1138            [ParameterMemberSpec::new(
1139                "q.weight",
1140                [8, 8],
1141                MemberSharding::Partitioned { axis: 0 },
1142            )],
1143        )
1144        .is_ok());
1145    }
1146
1147    #[test]
1148    fn preferred_partition_units_follow_every_physical_companion() {
1149        let group = partitioned_group_with_preferred_units(
1150            "experts.intermediate",
1151            ParameterRole::ExpertIntermediate,
1152            64,
1153            vec![
1154                ParameterMemberSpec::new(
1155                    "experts.up_proj",
1156                    [4, 64, 16],
1157                    MemberSharding::Partitioned { axis: 1 },
1158                ),
1159                ParameterMemberSpec::new(
1160                    "experts.down_proj",
1161                    [4, 16, 8],
1162                    MemberSharding::Partitioned { axis: 2 },
1163                ),
1164                ParameterMemberSpec::new(
1165                    "experts.down_proj_scales",
1166                    [4, 16, 2],
1167                    MemberSharding::Partitioned { axis: 2 },
1168                ),
1169            ],
1170        )
1171        .unwrap();
1172
1173        assert_eq!(group.partition_units(), Some(2));
1174    }
1175
1176    #[test]
1177    fn fp8_expansion_uses_architecture_declared_companion_identity() {
1178        let groups = vec![ParameterGroupSpec::partitioned(
1179            "query",
1180            ParameterRole::AttentionHeads,
1181            8,
1182            [ParameterMemberSpec::new(
1183                "opaque_matrix",
1184                [256, 256],
1185                MemberSharding::Partitioned { axis: 0 },
1186            )],
1187        )
1188        .unwrap()];
1189        let format = LinearFormat::E4M3BlockFp8(
1190            BlockFp8Format::new(128, 128, BlockFp8ScaleEncoding::Ue8m0).unwrap(),
1191        );
1192
1193        let expanded = expand_linear_format_parameter_groups(groups, |_| {
1194            Ok(Some(
1195                LinearFormatSpec::scaled(
1196                    format,
1197                    eredu_nn::ParameterSpec::trainable("opaque_scale").unwrap(),
1198                )
1199                .unwrap(),
1200            ))
1201        })
1202        .unwrap();
1203        assert_eq!(expanded.len(), 1);
1204        assert_eq!(expanded[0].partition_units(), Some(2));
1205        assert_eq!(expanded[0].members().len(), 2);
1206        assert_eq!(
1207            expanded[0]
1208                .members()
1209                .iter()
1210                .map(ParameterMemberSpec::target)
1211                .collect::<Vec<_>>(),
1212            ["opaque_matrix", "opaque_scale"]
1213        );
1214        assert_eq!(expanded[0].members()[1].global_shape(), [2, 2]);
1215        assert_eq!(
1216            expanded[0].members()[1].sharding(),
1217            &MemberSharding::Partitioned { axis: 0 }
1218        );
1219    }
1220
1221    #[test]
1222    fn segmented_projection_applies_identical_ranges_to_every_fused_companion() {
1223        let fused = ParameterGroupSpec::new(
1224            "fused",
1225            ParameterRole::FeedForwardIntermediate,
1226            [
1227                ParameterMemberSpec::new(
1228                    "gate_up.weight",
1229                    [12, 8],
1230                    MemberSharding::Equal { axis: 0 },
1231                ),
1232                ParameterMemberSpec::new(
1233                    "gate_up.scales",
1234                    [12, 2],
1235                    MemberSharding::Equal { axis: 0 },
1236                ),
1237                ParameterMemberSpec::new(
1238                    "gate_up.biases",
1239                    [12, 2],
1240                    MemberSharding::Equal { axis: 0 },
1241                ),
1242            ],
1243        )
1244        .unwrap();
1245        let row = ParameterGroupSpec::new(
1246            "row",
1247            ParameterRole::FeedForwardIntermediate,
1248            [
1249                ParameterMemberSpec::new("down.weight", [8, 6], MemberSharding::Equal { axis: 1 }),
1250                ParameterMemberSpec::new("down.scales", [8, 2], MemberSharding::Equal { axis: 1 }),
1251                ParameterMemberSpec::new("down.bias", [8], MemberSharding::Replicated),
1252            ],
1253        )
1254        .unwrap();
1255        let segments = vec![0..4, 4..8, 8..12];
1256        let group = assemble_segmented_projection_group(
1257            "mlp.projections",
1258            ParameterRole::FeedForwardIntermediate,
1259            fused,
1260            row,
1261            segments.clone(),
1262            2,
1263            12,
1264        )
1265        .unwrap();
1266        assert_eq!(group.partition_units(), Some(2));
1267        for member in &group.members()[..3] {
1268            assert_eq!(
1269                member.sharding(),
1270                &MemberSharding::PartitionedSegments {
1271                    axis: 0,
1272                    segments: segments.clone(),
1273                }
1274            );
1275        }
1276        assert_eq!(
1277            group.members()[3].sharding(),
1278            &MemberSharding::Partitioned { axis: 1 }
1279        );
1280        assert_eq!(
1281            group.members()[4].sharding(),
1282            &MemberSharding::Partitioned { axis: 1 }
1283        );
1284        assert_eq!(group.members()[5].sharding(), &MemberSharding::Replicated);
1285    }
1286
1287    #[test]
1288    fn segmented_projection_rejects_one_misaligned_companion_atomically() {
1289        let fused = ParameterGroupSpec::new(
1290            "fused",
1291            ParameterRole::AttentionHeads,
1292            [
1293                ParameterMemberSpec::new("qkv.weight", [12, 8], MemberSharding::Equal { axis: 0 }),
1294                ParameterMemberSpec::new("qkv.scales", [11, 2], MemberSharding::Equal { axis: 0 }),
1295            ],
1296        )
1297        .unwrap();
1298        let row = ParameterGroupSpec::new(
1299            "row",
1300            ParameterRole::AttentionHeads,
1301            [ParameterMemberSpec::new(
1302                "output.weight",
1303                [8, 4],
1304                MemberSharding::Equal { axis: 1 },
1305            )],
1306        )
1307        .unwrap();
1308        assert!(matches!(
1309            assemble_segmented_projection_group(
1310                "attention.projections",
1311                ParameterRole::AttentionHeads,
1312                fused,
1313                row,
1314                vec![0..4, 4..8, 8..12],
1315                2,
1316                12,
1317            ),
1318            Err(ParallelPlanError::InvalidTensor(_))
1319        ));
1320    }
1321
1322    #[test]
1323    fn parallel_model_info_preserves_opaque_topology_and_accounting() {
1324        let info = ParallelModelInfo::new(
1325            (2usize, 1usize),
1326            "generic",
1327            vec!["layer.weight".into()],
1328            10,
1329            20,
1330            4,
1331            8,
1332        );
1333        assert_eq!(info.topology(), (2, 1));
1334        assert_eq!(info.effective_model_type(), "generic");
1335        assert_eq!(info.owned_tensors(), ["layer.weight"]);
1336        assert_eq!(info.local_parameter_bytes(), 10);
1337        assert_eq!(info.global_parameter_bytes(), 20);
1338        assert_eq!(info.pinned_device_parameter_bytes(), 4);
1339        assert_eq!(info.maximum_device_parameter_bytes(), 8);
1340    }
1341}