1use std::{
8 collections::{BTreeMap, BTreeSet},
9 ops::Range,
10};
11
12use eredu_checkpoint::LinearFormat;
13use eredu_nn::{LinearFormatSpec, ParameterMetadata, ParameterVisitor, Parameterized, Tensor};
14
15#[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 #[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 pub fn topology(&self) -> T
52 where
53 T: Clone,
54 {
55 self.topology.clone()
56 }
57
58 pub fn effective_model_type(&self) -> &str {
60 &self.effective_model_type
61 }
62
63 pub fn owned_tensors(&self) -> &[String] {
65 &self.owned_tensors
66 }
67
68 pub const fn local_parameter_bytes(&self) -> u64 {
70 self.local_parameter_bytes
71 }
72
73 pub const fn global_parameter_bytes(&self) -> u64 {
75 self.global_parameter_bytes
76 }
77
78 pub const fn pinned_device_parameter_bytes(&self) -> u64 {
80 self.pinned_device_parameter_bytes
81 }
82
83 pub const fn maximum_device_parameter_bytes(&self) -> u64 {
85 self.maximum_device_parameter_bytes
86 }
87}
88
89#[derive(Debug, Clone, Copy, Eq, PartialEq)]
91pub enum ParameterRole {
92 Replicated,
94 ColumnProjection,
96 RowProjection,
98 Vocabulary,
100 AttentionHeads,
102 FeedForwardIntermediate,
104 ExpertIntermediate,
106 SharedExpertIntermediate,
108 Channels,
110 Segmented,
112}
113
114#[derive(Debug, Clone, Copy, Eq, PartialEq)]
116pub enum ProjectionSharding {
117 Replicated,
119 Column,
121 Row,
123}
124
125#[derive(Debug, Clone, Eq, PartialEq)]
127pub enum MemberSharding {
128 Replicated,
130 Equal {
132 axis: usize,
134 },
135 Balanced {
137 axis: usize,
139 },
140 Partitioned {
142 axis: usize,
144 },
145 PartitionedSegments {
147 axis: usize,
149 segments: Vec<Range<usize>>,
151 },
152 Segmented {
154 axis: usize,
156 segments: Vec<Range<usize>>,
158 },
159}
160
161#[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 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 pub fn target(&self) -> &str {
209 &self.target
210 }
211
212 pub fn global_shape(&self) -> &[usize] {
214 &self.global_shape
215 }
216
217 pub const fn sharding(&self) -> &MemberSharding {
219 &self.sharding
220 }
221
222 pub const fn linear_companion(&self) -> Option<eredu_nn::LinearCompanionRole> {
224 self.linear_companion
225 }
226
227 pub fn linear_companion_of(&self) -> Option<&str> {
229 self.linear_companion_of.as_deref()
230 }
231}
232
233#[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 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 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 pub fn logical_name(&self) -> &str {
318 &self.logical_name
319 }
320
321 pub const fn role(&self) -> ParameterRole {
323 self.role
324 }
325
326 pub const fn partition_units(&self) -> Option<usize> {
328 self.partition_units
329 }
330
331 pub fn members(&self) -> &[ParameterMemberSpec] {
333 &self.members
334 }
335}
336
337pub 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
404pub 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
470pub 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
500pub 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
534pub 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
621pub 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
643pub 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#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
896pub enum ShardingPolicy {
897 #[default]
899 Require,
900 ReplicateUnsupported,
902}
903
904#[derive(Debug, Clone, Eq, PartialEq)]
906pub enum TensorPlacement {
907 Replicated,
909 Local,
911 Omit,
913 Rank {
915 rank: usize,
917 },
918 Shard {
920 axis: usize,
922 index: usize,
924 parts: usize,
926 },
927 Range {
929 axis: usize,
931 start: usize,
933 end: usize,
935 },
936 Indices {
938 axis: usize,
940 indices: Vec<usize>,
942 },
943}
944
945#[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 #[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 pub fn logical_name(&self) -> &str {
987 &self.logical_name
988 }
989
990 pub const fn role(&self) -> ParameterRole {
992 self.role
993 }
994
995 pub fn global_shape(&self) -> &[usize] {
997 &self.global_shape
998 }
999
1000 pub fn local_shape(&self) -> &[usize] {
1002 &self.local_shape
1003 }
1004
1005 pub const fn placement(&self) -> &P {
1007 &self.placement
1008 }
1009
1010 pub fn additional_placements(&self) -> &[TensorPlacement] {
1017 &self.additional_placements
1018 }
1019
1020 pub fn with_additional_placement(mut self, placement: TensorPlacement) -> Self {
1023 self.additional_placements.push(placement);
1024 self
1025 }
1026
1027 pub fn logical_range(&self) -> Option<&Range<usize>> {
1029 self.logical_range.as_ref()
1030 }
1031
1032 pub const fn logical_units(&self) -> Option<usize> {
1034 self.logical_units
1035 }
1036
1037 pub const fn fell_back_to_replication(&self) -> bool {
1039 self.fell_back_to_replication
1040 }
1041}
1042
1043#[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 pub fn contains(&self, target: &str) -> bool {
1060 self.tensors.contains_key(target)
1061 }
1062
1063 pub fn insert(&mut self, target: String, layout: LocalTensorLayout<P>) {
1065 self.tensors.insert(target, layout);
1066 }
1067
1068 pub fn tensor(&self, target: &str) -> Option<&LocalTensorLayout<P>> {
1070 self.tensors.get(target)
1071 }
1072
1073 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 pub fn len(&self) -> usize {
1082 self.tensors.len()
1083 }
1084
1085 pub fn is_empty(&self) -> bool {
1087 self.tensors.is_empty()
1088 }
1089}
1090
1091#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1093pub enum ParallelPlanError {
1094 #[error("{0}")]
1096 InvalidGroup(String),
1097 #[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}