1use std::ops::Deref;
4use std::{collections::BTreeMap, collections::BTreeSet, ops::Range};
5
6use crate::{
7 ArchitectureStatePartitionError, ArchitectureStatePartitionPlan, ArchitectureStatePlacement,
8 ExecutionGraph, ExecutionGroupId, ExecutionUnitLayout, LayeredForwardState,
9 LayeredPartitionInput, LayeredPartitionOutput, ParameterGroupSpec,
10 PartitionedLayeredArchitecture, RuntimeState, StateLayout,
11};
12
13#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
15#[non_exhaustive]
16pub enum ParameterGroupOwner {
17 StaticRole(String),
19 StaticAnyOf(Vec<String>),
21 #[non_exhaustive]
23 ExecutionUnit {
24 group: ExecutionGroupId,
26 global_unit: usize,
28 },
29}
30
31impl ParameterGroupOwner {
32 pub fn static_role(role: impl Into<String>) -> Self {
34 Self::StaticRole(role.into())
35 }
36
37 pub fn static_any_of(roles: impl IntoIterator<Item = impl Into<String>>) -> Self {
39 Self::StaticAnyOf(roles.into_iter().map(Into::into).collect())
40 }
41
42 pub fn execution_unit(group: ExecutionGroupId, global_unit: usize) -> Self {
44 Self::ExecutionUnit { group, global_unit }
45 }
46
47 fn is_local<G, A>(&self, partition: &ArchitecturePartition<G, A>) -> bool {
48 match self {
49 Self::StaticRole(role) => partition.ownership().owns_static_role(role),
50 Self::StaticAnyOf(roles) => roles
51 .iter()
52 .any(|role| partition.ownership().owns_static_role(role)),
53 Self::ExecutionUnit { group, global_unit } => {
54 partition.owns_unit(group.as_str(), *global_unit)
55 }
56 }
57 }
58
59 fn is_local_partition_parts(
60 &self,
61 groups: &[PartitionGroup],
62 ownership: &PartitionOwnership,
63 ) -> bool {
64 match self {
65 Self::StaticRole(role) => ownership.owns_static_role(role),
66 Self::StaticAnyOf(roles) => roles.iter().any(|role| ownership.owns_static_role(role)),
67 Self::ExecutionUnit { group, global_unit } => groups
68 .iter()
69 .any(|owned| owned.group() == group && owned.contains(*global_unit)),
70 }
71 }
72
73 fn static_storage_role(&self) -> Option<&str> {
74 match self {
75 Self::StaticRole(role) => Some(role),
76 Self::StaticAnyOf(roles) => roles.first().map(String::as_str),
77 Self::ExecutionUnit { .. } => None,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Eq, PartialEq)]
84pub struct OwnedParameterGroupSpec {
85 owner: ParameterGroupOwner,
86 group: ParameterGroupSpec,
87}
88
89impl OwnedParameterGroupSpec {
90 pub fn new(owner: ParameterGroupOwner, group: ParameterGroupSpec) -> Self {
92 Self { owner, group }
93 }
94
95 pub const fn owner(&self) -> &ParameterGroupOwner {
97 &self.owner
98 }
99
100 pub const fn group(&self) -> &ParameterGroupSpec {
102 &self.group
103 }
104
105 pub fn into_group(self) -> ParameterGroupSpec {
107 self.group
108 }
109}
110
111impl Deref for OwnedParameterGroupSpec {
112 type Target = ParameterGroupSpec;
113
114 fn deref(&self) -> &Self::Target {
115 &self.group
116 }
117}
118
119#[derive(Debug, Clone, Eq, PartialEq)]
121pub struct ArchitectureParameterDescription {
122 graph: ExecutionGraph,
123 unit_layout: ExecutionUnitLayout,
124 groups: Vec<OwnedParameterGroupSpec>,
125}
126
127impl ArchitectureParameterDescription {
128 pub fn new(
131 graph: &ExecutionGraph,
132 layout: &ExecutionUnitLayout,
133 expected: impl IntoIterator<Item = ParameterGroupSpec>,
134 groups: impl IntoIterator<Item = OwnedParameterGroupSpec>,
135 ) -> Result<Self, ArchitectureParameterError> {
136 validate_canonical_layout(graph, layout)
137 .map_err(|error| ArchitectureParameterError::InvalidLayout(error.to_string()))?;
138 let expected = parameter_targets(expected)?;
139 let groups = groups.into_iter().collect::<Vec<_>>();
140 let mut actual = BTreeMap::new();
141 for tagged in &groups {
142 match tagged.owner() {
143 ParameterGroupOwner::StaticRole(role) => {
144 if role.trim().is_empty() {
145 return Err(ArchitectureParameterError::EmptyStaticRole);
146 }
147 }
148 ParameterGroupOwner::StaticAnyOf(roles) => {
149 if roles.is_empty() || roles.iter().any(|role| role.trim().is_empty()) {
150 return Err(ArchitectureParameterError::EmptyStaticRole);
151 }
152 let unique = roles.iter().collect::<BTreeSet<_>>();
153 if unique.len() != roles.len() {
154 return Err(ArchitectureParameterError::DuplicateStaticRole);
155 }
156 }
157 ParameterGroupOwner::ExecutionUnit { group, global_unit } => {
158 let Some(group_index) = graph
159 .groups()
160 .iter()
161 .position(|candidate| candidate.id() == group.as_str())
162 else {
163 return Err(ArchitectureParameterError::UnknownExecutionGroup(
164 group.as_str().to_owned(),
165 ));
166 };
167 let available = layout
168 .group_range(group_index)
169 .expect("validated canonical layout contains every group")
170 .len();
171 if *global_unit >= available {
172 return Err(ArchitectureParameterError::UnitOutOfRange {
173 group: group.as_str().to_owned(),
174 global_unit: *global_unit,
175 available,
176 });
177 }
178 }
179 }
180 for member in tagged.group().members() {
181 if let Some(previous) = actual.insert(member.target().to_owned(), tagged.owner()) {
182 return Err(ArchitectureParameterError::DuplicateOwnership {
183 target: member.target().to_owned(),
184 first: previous.clone(),
185 second: tagged.owner().clone(),
186 });
187 }
188 }
189 }
190 let actual_targets = actual.keys().cloned().collect::<BTreeSet<_>>();
191 let expected_targets = expected.keys().cloned().collect::<BTreeSet<_>>();
192 if let Some(target) = expected_targets.difference(&actual_targets).next() {
193 return Err(ArchitectureParameterError::MissingOwnership(target.clone()));
194 }
195 if let Some(target) = actual_targets.difference(&expected_targets).next() {
196 return Err(ArchitectureParameterError::UnexpectedOwnership(
197 target.clone(),
198 ));
199 }
200 Ok(Self {
201 graph: graph.clone(),
202 unit_layout: layout.clone(),
203 groups,
204 })
205 }
206
207 pub const fn graph(&self) -> &ExecutionGraph {
209 &self.graph
210 }
211
212 pub const fn unit_layout(&self) -> &ExecutionUnitLayout {
215 &self.unit_layout
216 }
217
218 pub fn validate_architecture<B, S, M>(
220 &self,
221 architecture: &M,
222 ) -> Result<(), ArchitecturePartitionError>
223 where
224 B: eredu_nn::NeuralBackend,
225 S: crate::RuntimeState<B>,
226 M: crate::LayeredArchitecture<B, S>,
227 M::Error: std::fmt::Display,
228 {
229 let (graph, unit_layout) = canonical_architecture_layout::<B, S, M>(architecture)?;
230 if graph != self.graph {
231 return Err(ArchitecturePartitionError::ArchitectureGraphMismatch);
232 }
233 if unit_layout != self.unit_layout {
234 return Err(ArchitecturePartitionError::ArchitectureUnitLayoutMismatch);
235 }
236 Ok(())
237 }
238
239 pub fn groups(&self) -> &[OwnedParameterGroupSpec] {
241 &self.groups
242 }
243
244 pub fn targets_for_role(&self, role: crate::ParameterRole) -> BTreeSet<String> {
250 self.groups
251 .iter()
252 .filter(|owned| owned.group().role() == role)
253 .flat_map(|owned| owned.group().members())
254 .map(|member| member.target().to_owned())
255 .collect()
256 }
257
258 pub fn select_owned<G, A>(
260 &self,
261 partition: &ArchitecturePartition<G, A>,
262 ) -> Vec<OwnedParameterGroupSpec> {
263 self.groups
264 .iter()
265 .filter(|tagged| tagged.owner().is_local(partition))
266 .cloned()
267 .collect()
268 }
269
270 pub fn select_static_roles<'a, G, A>(
275 &'a self,
276 partition: &ArchitecturePartition<G, A>,
277 ) -> Vec<&'a str> {
278 self.groups
279 .iter()
280 .filter(|tagged| tagged.owner().is_local(partition))
281 .filter_map(|tagged| tagged.owner().static_storage_role())
282 .collect::<BTreeSet<_>>()
283 .into_iter()
284 .collect()
285 }
286}
287
288fn parameter_targets(
289 groups: impl IntoIterator<Item = ParameterGroupSpec>,
290) -> Result<BTreeMap<String, String>, ArchitectureParameterError> {
291 let mut targets = BTreeMap::new();
292 for group in groups {
293 for member in group.members() {
294 if let Some(previous) =
295 targets.insert(member.target().to_owned(), group.logical_name().to_owned())
296 {
297 return Err(ArchitectureParameterError::DuplicateExpectedTarget {
298 target: member.target().to_owned(),
299 first: previous,
300 second: group.logical_name().to_owned(),
301 });
302 }
303 }
304 }
305 Ok(targets)
306}
307
308#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
310#[non_exhaustive]
311pub enum ArchitectureParameterError {
312 #[error("invalid architecture parameter layout: {0}")]
314 InvalidLayout(String),
315 #[error("architecture parameter static role must not be empty")]
317 EmptyStaticRole,
318 #[error("architecture shared parameter owner repeats a static role")]
320 DuplicateStaticRole,
321 #[error("architecture parameter owner names unknown execution group {0:?}")]
323 UnknownExecutionGroup(String),
324 #[error("architecture parameter owner {group}:{global_unit} exceeds {available} units")]
326 UnitOutOfRange {
327 group: String,
329 global_unit: usize,
331 available: usize,
333 },
334 #[error("expected parameter target {target:?} appears in both {first:?} and {second:?}")]
336 DuplicateExpectedTarget {
337 target: String,
339 first: String,
341 second: String,
343 },
344 #[error("parameter target {target:?} is owned by both {first:?} and {second:?}")]
346 DuplicateOwnership {
347 target: String,
349 first: ParameterGroupOwner,
351 second: ParameterGroupOwner,
353 },
354 #[error("parameter target {0:?} has no architecture owner")]
356 MissingOwnership(String),
357 #[error("parameter target {0:?} is not present in the authoritative parameter groups")]
359 UnexpectedOwnership(String),
360}
361
362#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
367#[non_exhaustive]
368pub enum BoundaryTensorDtype {
369 Activation,
371 Uint32,
373 Int32,
375}
376
377#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
383#[non_exhaustive]
384pub enum PipelineActivationDtype {
385 Float16,
387 Bfloat16,
389 Float32,
391}
392
393#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
395pub struct PipelineWireContract {
396 activation_dtype: PipelineActivationDtype,
397}
398
399impl PipelineWireContract {
400 pub const fn new(activation_dtype: PipelineActivationDtype) -> Self {
403 Self { activation_dtype }
404 }
405
406 pub const fn activation_dtype(self) -> PipelineActivationDtype {
408 self.activation_dtype
409 }
410}
411
412#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
414#[non_exhaustive]
415pub enum BoundaryTensorDimension {
416 Batch,
418 Sequence,
420 Fixed(i32),
422}
423
424#[derive(Debug, Clone, Eq, Hash, PartialEq)]
426pub struct BoundaryTensorSpec {
427 role: String,
428 shape: Vec<BoundaryTensorDimension>,
429 dtype: BoundaryTensorDtype,
430}
431
432impl BoundaryTensorSpec {
433 pub fn new(
435 role: impl Into<String>,
436 shape: impl IntoIterator<Item = BoundaryTensorDimension>,
437 dtype: BoundaryTensorDtype,
438 ) -> Self {
439 Self {
440 role: role.into(),
441 shape: shape.into_iter().collect(),
442 dtype,
443 }
444 }
445
446 pub fn primary_activation(hidden_size: i32) -> Self {
448 Self::new(
449 "hidden",
450 [
451 BoundaryTensorDimension::Batch,
452 BoundaryTensorDimension::Sequence,
453 BoundaryTensorDimension::Fixed(hidden_size),
454 ],
455 BoundaryTensorDtype::Activation,
456 )
457 }
458
459 pub fn role(&self) -> &str {
461 &self.role
462 }
463
464 pub fn shape(&self) -> &[BoundaryTensorDimension] {
466 &self.shape
467 }
468
469 pub const fn dtype(&self) -> BoundaryTensorDtype {
471 self.dtype
472 }
473}
474
475#[derive(Debug, Clone, Eq, Hash, PartialEq)]
477pub struct ResolvedBoundaryTensorSpec {
478 role: String,
479 shape: Vec<i32>,
480 dtype: BoundaryTensorDtype,
481}
482
483impl ResolvedBoundaryTensorSpec {
484 pub fn role(&self) -> &str {
486 &self.role
487 }
488
489 pub fn shape(&self) -> &[i32] {
491 &self.shape
492 }
493
494 pub const fn dtype(&self) -> BoundaryTensorDtype {
496 self.dtype
497 }
498}
499
500#[derive(Debug, Clone, Eq, Hash, PartialEq)]
502pub struct BoundaryWireSchema {
503 identity: &'static str,
504 primary: BoundaryTensorSpec,
505 auxiliary: Vec<BoundaryTensorSpec>,
506}
507
508impl BoundaryWireSchema {
509 pub fn new(
511 identity: &'static str,
512 primary: BoundaryTensorSpec,
513 auxiliary: impl IntoIterator<Item = BoundaryTensorSpec>,
514 ) -> Result<Self, ArchitectureBoundaryError> {
515 if identity.trim().is_empty() {
516 return Err(ArchitectureBoundaryError::EmptyIdentity);
517 }
518 if primary.dtype != BoundaryTensorDtype::Activation {
519 return Err(ArchitectureBoundaryError::InvalidPrimaryDtype { boundary: identity });
520 }
521 let auxiliary = auxiliary.into_iter().collect::<Vec<_>>();
522 let mut roles = BTreeSet::new();
523 for tensor in std::iter::once(&primary).chain(&auxiliary) {
524 if tensor.role.trim().is_empty() {
525 return Err(ArchitectureBoundaryError::EmptyTensorRole { boundary: identity });
526 }
527 if !roles.insert(tensor.role.as_str()) {
528 return Err(ArchitectureBoundaryError::DuplicateTensorRole {
529 boundary: identity,
530 role: tensor.role.clone(),
531 });
532 }
533 if tensor.shape.is_empty() {
534 return Err(ArchitectureBoundaryError::EmptyTensorShape {
535 boundary: identity,
536 role: tensor.role.clone(),
537 });
538 }
539 if tensor
540 .shape
541 .iter()
542 .any(|dimension| matches!(dimension, BoundaryTensorDimension::Fixed(value) if *value <= 0))
543 {
544 return Err(ArchitectureBoundaryError::InvalidTensorDimension {
545 boundary: identity,
546 role: tensor.role.clone(),
547 });
548 }
549 }
550 Ok(Self {
551 identity,
552 primary,
553 auxiliary,
554 })
555 }
556
557 pub const fn identity(&self) -> &'static str {
559 self.identity
560 }
561
562 pub const fn primary(&self) -> &BoundaryTensorSpec {
564 &self.primary
565 }
566
567 pub fn auxiliary(&self) -> &[BoundaryTensorSpec] {
569 &self.auxiliary
570 }
571
572 pub fn resolve(
574 &self,
575 batch_size: i32,
576 sequence_length: i32,
577 ) -> Result<ResolvedBoundaryWireSchema, ArchitectureBoundaryError> {
578 self.resolve_each(
579 batch_size,
580 std::iter::repeat_n(sequence_length, 1 + self.auxiliary.len()),
581 )
582 }
583
584 pub fn resolve_each(
591 &self,
592 batch_size: i32,
593 sequence_lengths: impl IntoIterator<Item = i32>,
594 ) -> Result<ResolvedBoundaryWireSchema, ArchitectureBoundaryError> {
595 let sequence_lengths = sequence_lengths.into_iter().collect::<Vec<_>>();
596 if sequence_lengths.len() != 1 + self.auxiliary.len() {
597 return Err(ArchitectureBoundaryError::TensorCount {
598 boundary: self.identity,
599 expected: 1 + self.auxiliary.len(),
600 actual: sequence_lengths.len(),
601 });
602 }
603 if batch_size <= 0 || sequence_lengths.iter().any(|sequence| *sequence <= 0) {
604 return Err(ArchitectureBoundaryError::InvalidInvocationGeometry {
605 boundary: self.identity,
606 batch_size,
607 sequence_length: sequence_lengths
608 .into_iter()
609 .find(|value| *value <= 0)
610 .unwrap_or(0),
611 });
612 }
613 let resolve = |tensor: &BoundaryTensorSpec, sequence_length| ResolvedBoundaryTensorSpec {
614 role: tensor.role.clone(),
615 shape: tensor
616 .shape
617 .iter()
618 .map(|dimension| match dimension {
619 BoundaryTensorDimension::Batch => batch_size,
620 BoundaryTensorDimension::Sequence => sequence_length,
621 BoundaryTensorDimension::Fixed(value) => *value,
622 })
623 .collect(),
624 dtype: tensor.dtype,
625 };
626 let mut sequences = sequence_lengths.into_iter();
627 Ok(ResolvedBoundaryWireSchema {
628 identity: self.identity,
629 primary: resolve(
630 &self.primary,
631 sequences.next().expect("validated primary sequence"),
632 ),
633 auxiliary: self
634 .auxiliary
635 .iter()
636 .zip(sequences)
637 .map(|(tensor, sequence)| resolve(tensor, sequence))
638 .collect(),
639 })
640 }
641}
642
643#[derive(Debug, Clone, Eq, Hash, PartialEq)]
645pub struct ResolvedBoundaryWireSchema {
646 identity: &'static str,
647 primary: ResolvedBoundaryTensorSpec,
648 auxiliary: Vec<ResolvedBoundaryTensorSpec>,
649}
650
651impl ResolvedBoundaryWireSchema {
652 pub const fn identity(&self) -> &'static str {
654 self.identity
655 }
656
657 pub const fn primary(&self) -> &ResolvedBoundaryTensorSpec {
659 &self.primary
660 }
661
662 pub fn auxiliary(&self) -> &[ResolvedBoundaryTensorSpec] {
664 &self.auxiliary
665 }
666}
667
668pub trait ArchitectureBoundary: Sized {
675 type Boundary<T>;
677
678 const IDENTITY: &'static str;
681
682 fn primary_tensor_spec(&self) -> BoundaryTensorSpec;
684
685 fn auxiliary_tensor_specs(&self) -> Vec<BoundaryTensorSpec>;
687
688 fn encode<T>(
693 &self,
694 boundary: Self::Boundary<T>,
695 ) -> Result<Vec<ArchitectureBoundaryValue<T>>, ArchitectureBoundaryError>;
696
697 fn decode<T>(&self, tensors: Vec<T>) -> Result<Self::Boundary<T>, ArchitectureBoundaryError>;
699
700 fn wire_schema(&self) -> Result<BoundaryWireSchema, ArchitectureBoundaryError> {
702 BoundaryWireSchema::new(
703 Self::IDENTITY,
704 self.primary_tensor_spec(),
705 self.auxiliary_tensor_specs(),
706 )
707 }
708}
709
710#[derive(Debug, Clone, Eq, PartialEq)]
716pub struct ArchitectureBoundaryValue<T> {
717 role: String,
718 tensor: T,
719}
720
721impl<T> ArchitectureBoundaryValue<T> {
722 pub fn new(role: impl Into<String>, tensor: T) -> Result<Self, ArchitectureBoundaryError> {
724 let role = role.into();
725 if role.trim().is_empty() {
726 return Err(ArchitectureBoundaryError::EmptyTaggedTensorRole);
727 }
728 Ok(Self { role, tensor })
729 }
730
731 pub fn role(&self) -> &str {
733 &self.role
734 }
735
736 pub const fn tensor(&self) -> &T {
738 &self.tensor
739 }
740
741 pub fn into_parts(self) -> (String, T) {
743 (self.role, self.tensor)
744 }
745}
746
747#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
753pub struct NoAuxiliaryBoundary;
754
755#[derive(Debug, Clone, Copy, Eq, PartialEq)]
757pub struct NoAuxiliaryBoundarySchema {
758 hidden_size: i32,
759}
760
761impl NoAuxiliaryBoundarySchema {
762 pub const fn new(hidden_size: i32) -> Self {
764 Self { hidden_size }
765 }
766}
767
768impl ArchitectureBoundary for NoAuxiliaryBoundarySchema {
769 type Boundary<T> = NoAuxiliaryBoundary;
770
771 const IDENTITY: &'static str = "none";
772
773 fn primary_tensor_spec(&self) -> BoundaryTensorSpec {
774 BoundaryTensorSpec::primary_activation(self.hidden_size)
775 }
776
777 fn auxiliary_tensor_specs(&self) -> Vec<BoundaryTensorSpec> {
778 Vec::new()
779 }
780
781 fn encode<T>(
782 &self,
783 _boundary: Self::Boundary<T>,
784 ) -> Result<Vec<ArchitectureBoundaryValue<T>>, ArchitectureBoundaryError> {
785 Ok(Vec::new())
786 }
787
788 fn decode<T>(&self, tensors: Vec<T>) -> Result<Self::Boundary<T>, ArchitectureBoundaryError> {
789 validate_boundary_tensor_count(self, &tensors)?;
790 Ok(NoAuxiliaryBoundary)
791 }
792}
793
794pub fn validate_boundary_tensor_count<B, T>(
797 boundary: &B,
798 tensors: &[T],
799) -> Result<(), ArchitectureBoundaryError>
800where
801 B: ArchitectureBoundary,
802{
803 let expected = boundary.wire_schema()?.auxiliary().len();
804 let actual = tensors.len();
805 if actual != expected {
806 return Err(ArchitectureBoundaryError::TensorCount {
807 boundary: B::IDENTITY,
808 expected,
809 actual,
810 });
811 }
812 Ok(())
813}
814
815#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
817#[non_exhaustive]
818pub enum ArchitectureBoundaryError {
819 #[error("architecture boundary identity must not be empty")]
821 EmptyIdentity,
822 #[error("architecture boundary value contains an empty tensor role")]
824 EmptyTaggedTensorRole,
825 #[error("architecture boundary {boundary:?} primary tensor must use activation dtype")]
827 InvalidPrimaryDtype {
828 boundary: &'static str,
830 },
831 #[error("architecture boundary {boundary:?} contains an empty tensor role")]
833 EmptyTensorRole {
834 boundary: &'static str,
836 },
837 #[error("architecture boundary {boundary:?} repeats tensor role {role:?}")]
839 DuplicateTensorRole {
840 boundary: &'static str,
842 role: String,
844 },
845 #[error("architecture boundary {boundary:?} tensor {role:?} has no dimensions")]
847 EmptyTensorShape {
848 boundary: &'static str,
850 role: String,
852 },
853 #[error("architecture boundary {boundary:?} tensor {role:?} has a non-positive dimension")]
855 InvalidTensorDimension {
856 boundary: &'static str,
858 role: String,
860 },
861 #[error(
863 "architecture boundary {boundary:?} requires positive invocation geometry, got batch {batch_size} and sequence {sequence_length}"
864 )]
865 InvalidInvocationGeometry {
866 boundary: &'static str,
868 batch_size: i32,
870 sequence_length: i32,
872 },
873 #[error(
875 "architecture boundary {boundary:?} expected {expected} tensors but received {actual}"
876 )]
877 TensorCount {
878 boundary: &'static str,
880 expected: usize,
882 actual: usize,
884 },
885 #[error("architecture boundary {boundary:?} is invalid: {detail}")]
887 Invalid {
888 boundary: &'static str,
890 detail: String,
892 },
893}
894
895#[derive(Debug, Clone, Eq, PartialEq)]
897pub struct PartitionOwnership {
898 input: bool,
899 output: bool,
900 static_roles: Vec<String>,
901}
902
903impl PartitionOwnership {
904 pub fn new(
906 input: bool,
907 output: bool,
908 static_roles: impl IntoIterator<Item = impl Into<String>>,
909 ) -> Result<Self, ArchitecturePartitionError> {
910 let static_roles = static_roles.into_iter().map(Into::into).collect::<Vec<_>>();
911 let mut unique = BTreeSet::new();
912 for role in &static_roles {
913 if role.trim().is_empty() {
914 return Err(ArchitecturePartitionError::EmptyStaticRole);
915 }
916 if !unique.insert(role.clone()) {
917 return Err(ArchitecturePartitionError::DuplicateStaticRole(
918 role.clone(),
919 ));
920 }
921 }
922 Ok(Self {
923 input,
924 output,
925 static_roles,
926 })
927 }
928
929 pub const fn owns_input(&self) -> bool {
931 self.input
932 }
933
934 pub const fn owns_output(&self) -> bool {
936 self.output
937 }
938
939 pub fn static_roles(&self) -> &[String] {
941 &self.static_roles
942 }
943
944 pub fn owns_static_role(&self, role: &str) -> bool {
946 self.static_roles.iter().any(|candidate| candidate == role)
947 }
948}
949
950#[derive(Debug, Clone, Eq, PartialEq)]
952pub struct PartitionState {
953 layout: StateLayout,
954 global_layers: Range<usize>,
955}
956
957impl PartitionState {
958 pub fn new(
960 layout: StateLayout,
961 global_layer_offset: usize,
962 ) -> Result<Self, ArchitecturePartitionError> {
963 let end = global_layer_offset.checked_add(layout.len()).ok_or(
964 ArchitecturePartitionError::StateOffsetOverflow {
965 offset: global_layer_offset,
966 layers: layout.len(),
967 },
968 )?;
969 Ok(Self {
970 layout,
971 global_layers: global_layer_offset..end,
972 })
973 }
974
975 pub const fn layout(&self) -> &StateLayout {
977 &self.layout
978 }
979
980 pub const fn global_layer_offset(&self) -> usize {
982 self.global_layers.start
983 }
984
985 pub fn global_layers(&self) -> Range<usize> {
987 self.global_layers.clone()
988 }
989
990 pub fn prompt_cache_identity<B, M>(
992 &self,
993 architecture: &M,
994 topology: eredu_core::cache::PromptCacheTopology,
995 ) -> Result<eredu_core::cache::PromptCacheModelIdentity, ArchitecturePartitionError>
996 where
997 B: eredu_nn::NeuralBackend,
998 M: crate::ArchitectureParameters<B>,
999 M::DefinitionError: std::fmt::Display,
1000 {
1001 architecture
1002 .state_identity(self, topology)
1003 .map_err(|error| ArchitecturePartitionError::ArchitectureState(error.to_string()))?
1004 .prompt_cache_identity(self.layout())
1005 .map_err(|error| ArchitecturePartitionError::PromptCacheIdentity(error.to_string()))
1006 }
1007}
1008
1009#[derive(Debug, Clone, Eq, PartialEq)]
1011pub struct PartitionGroup {
1012 group: ExecutionGroupId,
1013 group_index: usize,
1014 global_units: Range<usize>,
1015}
1016
1017impl PartitionGroup {
1018 pub const fn group(&self) -> &ExecutionGroupId {
1020 &self.group
1021 }
1022
1023 pub const fn group_index(&self) -> usize {
1025 self.group_index
1026 }
1027
1028 pub fn global_units(&self) -> Range<usize> {
1030 self.global_units.clone()
1031 }
1032
1033 pub fn contains(&self, global_unit: usize) -> bool {
1035 self.global_units.contains(&global_unit)
1036 }
1037}
1038
1039#[derive(Debug, Clone)]
1044pub struct ArchitecturePartition<G, A> {
1045 graph: ExecutionGraph,
1046 unit_layout: ExecutionUnitLayout,
1047 groups: Vec<PartitionGroup>,
1048 ownership: PartitionOwnership,
1049 state: Option<PartitionState>,
1050 local_geometry: G,
1051 boundary_schema: A,
1052 parameter_bindings: Vec<OwnedParameterGroupSpec>,
1053}
1054
1055impl<G, A> ArchitecturePartition<G, A> {
1056 #[allow(clippy::too_many_arguments)]
1065 pub fn from_architecture<B, S, M, N>(
1066 architecture: &M,
1067 group_ranges: impl IntoIterator<Item = (N, Range<usize>)>,
1068 ownership: PartitionOwnership,
1069 local_geometry: G,
1070 boundary_schema: A,
1071 parameters: &ArchitectureParameterDescription,
1072 ) -> Result<Self, ArchitecturePartitionError>
1073 where
1074 B: eredu_nn::NeuralBackend,
1075 S: crate::RuntimeState<B>,
1076 M: crate::LayeredArchitecture<B, S>,
1077 M::Error: std::fmt::Display,
1078 N: Into<String>,
1079 A: ArchitectureBoundary,
1080 {
1081 let (graph, unit_layout) = canonical_architecture_layout::<B, S, M>(architecture)?;
1082 boundary_schema.wire_schema()?;
1083 if parameters.graph() != &graph {
1084 return Err(ArchitecturePartitionError::ArchitectureGraphMismatch);
1085 }
1086 if parameters.unit_layout() != &unit_layout {
1087 return Err(ArchitecturePartitionError::ArchitectureUnitLayoutMismatch);
1088 }
1089 let complete_state = architecture
1090 .state_layout()
1091 .map_err(|error| ArchitecturePartitionError::ArchitectureState(error.to_string()))?;
1092 let plan = architecture.state_partition_plan(&complete_state);
1093 let mut partition = Self::new(
1094 graph,
1095 unit_layout,
1096 group_ranges,
1097 ownership,
1098 None,
1099 local_geometry,
1100 boundary_schema,
1101 std::iter::empty(),
1102 )?;
1103 partition.state = partition
1104 .resolve_state_partition(&complete_state, &plan)
1105 .map_err(|error| ArchitecturePartitionError::ArchitectureState(error.to_string()))?;
1106 partition.parameter_bindings = parameters.select_owned(&partition);
1107 Ok(partition)
1108 }
1109
1110 #[allow(clippy::too_many_arguments)]
1117 pub fn from_description<N>(
1118 parameters: &ArchitectureParameterDescription,
1119 group_ranges: impl IntoIterator<Item = (N, Range<usize>)>,
1120 ownership: PartitionOwnership,
1121 complete_state: &StateLayout,
1122 state_plan: &ArchitectureStatePartitionPlan,
1123 local_geometry: G,
1124 boundary_schema: A,
1125 ) -> Result<Self, ArchitecturePartitionError>
1126 where
1127 N: Into<String>,
1128 A: ArchitectureBoundary,
1129 {
1130 let graph = parameters.graph().clone();
1131 let unit_layout = parameters.unit_layout().clone();
1132 validate_canonical_layout(&graph, &unit_layout)?;
1133 boundary_schema.wire_schema()?;
1134 let mut partition = Self::new(
1135 graph,
1136 unit_layout,
1137 group_ranges,
1138 ownership,
1139 None,
1140 local_geometry,
1141 boundary_schema,
1142 std::iter::empty(),
1143 )?;
1144 partition.state = partition
1145 .resolve_state_partition(complete_state, state_plan)
1146 .map_err(|error| ArchitecturePartitionError::ArchitectureState(error.to_string()))?;
1147 partition.parameter_bindings = parameters.select_owned(&partition);
1148 Ok(partition)
1149 }
1150
1151 #[allow(clippy::too_many_arguments)]
1154 fn new<S>(
1155 graph: ExecutionGraph,
1156 unit_layout: ExecutionUnitLayout,
1157 group_ranges: impl IntoIterator<Item = (S, Range<usize>)>,
1158 ownership: PartitionOwnership,
1159 state: Option<PartitionState>,
1160 local_geometry: G,
1161 boundary_schema: A,
1162 parameter_bindings: impl IntoIterator<Item = OwnedParameterGroupSpec>,
1163 ) -> Result<Self, ArchitecturePartitionError>
1164 where
1165 S: Into<String>,
1166 {
1167 validate_canonical_layout(&graph, &unit_layout)?;
1168 let mut seen_groups = BTreeSet::new();
1169 let mut groups = Vec::new();
1170 for (group, global_units) in group_ranges {
1171 let group = group.into();
1172 let group_index = graph
1173 .groups()
1174 .iter()
1175 .position(|candidate| candidate.id() == group)
1176 .ok_or_else(|| ArchitecturePartitionError::UnknownGroup(group.clone()))?;
1177 if !seen_groups.insert(group.clone()) {
1178 return Err(ArchitecturePartitionError::DuplicateGroup(group));
1179 }
1180 if global_units.is_empty() {
1181 return Err(ArchitecturePartitionError::EmptyGroupRange { group });
1182 }
1183 let available = unit_layout
1184 .group_range(group_index)
1185 .expect("canonical layout contains every graph group")
1186 .len();
1187 if global_units.end > available {
1188 return Err(ArchitecturePartitionError::GroupRangeOutOfBounds {
1189 group,
1190 start: global_units.start,
1191 end: global_units.end,
1192 available,
1193 });
1194 }
1195 groups.push(PartitionGroup {
1196 group: unit_layout
1197 .group_id(group_index)
1198 .expect("canonical layout contains every graph group identity")
1199 .clone(),
1200 group_index,
1201 global_units,
1202 });
1203 }
1204 groups.sort_by_key(PartitionGroup::group_index);
1205
1206 let parameter_bindings = parameter_bindings.into_iter().collect::<Vec<_>>();
1207 let mut targets = BTreeSet::new();
1208 for binding in ¶meter_bindings {
1209 if !binding
1210 .owner()
1211 .is_local_partition_parts(&groups, &ownership)
1212 {
1213 return Err(ArchitecturePartitionError::NonLocalParameterOwner(
1214 binding.owner().clone(),
1215 ));
1216 }
1217 for member in binding.members() {
1218 if !targets.insert(member.target().to_owned()) {
1219 return Err(ArchitecturePartitionError::DuplicateParameterTarget(
1220 member.target().to_owned(),
1221 ));
1222 }
1223 }
1224 }
1225
1226 Ok(Self {
1227 graph,
1228 unit_layout,
1229 groups,
1230 ownership,
1231 state,
1232 local_geometry,
1233 boundary_schema,
1234 parameter_bindings,
1235 })
1236 }
1237
1238 pub const fn graph(&self) -> &ExecutionGraph {
1240 &self.graph
1241 }
1242
1243 pub const fn unit_layout(&self) -> &ExecutionUnitLayout {
1245 &self.unit_layout
1246 }
1247
1248 pub fn groups(&self) -> &[PartitionGroup] {
1250 &self.groups
1251 }
1252
1253 pub fn units(&self) -> impl Iterator<Item = crate::ExecutionUnitAddress> + '_ {
1255 self.groups.iter().flat_map(move |owned| {
1256 let group = owned.group_index;
1257 let base = self
1258 .unit_layout
1259 .group_range(group)
1260 .expect("partition group belongs to its canonical layout")
1261 .start;
1262 owned.global_units.clone().map(move |index| {
1263 self.unit_layout
1264 .address(base + index)
1265 .expect("partition unit belongs to its canonical layout")
1266 })
1267 })
1268 }
1269
1270 pub fn owns_unit(&self, group: &str, global_unit: usize) -> bool {
1272 self.groups
1273 .iter()
1274 .any(|owned| owned.group.as_str() == group && owned.contains(global_unit))
1275 }
1276
1277 pub const fn ownership(&self) -> &PartitionOwnership {
1279 &self.ownership
1280 }
1281
1282 pub const fn state(&self) -> Option<&PartitionState> {
1284 self.state.as_ref()
1285 }
1286
1287 pub fn prompt_cache_identity<B, M>(
1289 &self,
1290 architecture: &M,
1291 topology: eredu_core::cache::PromptCacheTopology,
1292 ) -> Result<eredu_core::cache::PromptCacheModelIdentity, ArchitecturePartitionError>
1293 where
1294 B: eredu_nn::NeuralBackend,
1295 M: crate::ArchitectureParameters<B>,
1296 M::DefinitionError: std::fmt::Display,
1297 {
1298 let state = self
1299 .state()
1300 .ok_or(ArchitecturePartitionError::MissingArchitectureState)?;
1301 state.prompt_cache_identity::<B, M>(architecture, topology)
1302 }
1303
1304 pub fn resolve_state_partition(
1310 &self,
1311 complete: &StateLayout,
1312 plan: &ArchitectureStatePartitionPlan,
1313 ) -> Result<Option<PartitionState>, ArchitectureStatePartitionError> {
1314 if plan.rules().is_empty() {
1315 return Err(ArchitectureStatePartitionError::EmptyPlan);
1316 }
1317
1318 let mut rules = plan.rules().iter().collect::<Vec<_>>();
1319 rules.sort_by_key(|rule| rule.layers().start);
1320 let mut frontier = 0usize;
1321 for rule in &rules {
1322 let layers = rule.layers();
1323 if layers.is_empty() {
1324 return Err(ArchitectureStatePartitionError::EmptyRange {
1325 start: layers.start,
1326 end: layers.end,
1327 });
1328 }
1329 if layers.end > complete.len() {
1330 return Err(ArchitectureStatePartitionError::RangeOutOfBounds {
1331 start: layers.start,
1332 end: layers.end,
1333 layers: complete.len(),
1334 });
1335 }
1336 if layers.start < frontier {
1337 return Err(ArchitectureStatePartitionError::OverlappingRange {
1338 start: layers.start,
1339 frontier,
1340 });
1341 }
1342 if layers.start > frontier {
1343 return Err(ArchitectureStatePartitionError::UnassignedLayer { layer: frontier });
1344 }
1345 if let ArchitectureStatePlacement::GroupUnits { group } = rule.placement() {
1346 let units = self
1347 .unit_layout
1348 .group_range(group)
1349 .ok_or(ArchitectureStatePartitionError::UnknownGroup { group })?
1350 .len();
1351 if layers.len() != units {
1352 return Err(ArchitectureStatePartitionError::GroupLengthMismatch {
1353 group,
1354 start: layers.start,
1355 end: layers.end,
1356 units,
1357 });
1358 }
1359 }
1360 frontier = layers.end;
1361 }
1362 if frontier != complete.len() {
1363 return Err(ArchitectureStatePartitionError::UnassignedLayer { layer: frontier });
1364 }
1365
1366 let mut selected = Vec::new();
1367 for rule in plan.rules() {
1368 let layers = rule.layers();
1369 match rule.placement() {
1370 ArchitectureStatePlacement::GroupUnits { group } => {
1371 if let Some(owned) = self
1372 .groups
1373 .iter()
1374 .find(|owned| owned.group_index() == group)
1375 {
1376 let units = owned.global_units();
1377 selected.push(layers.start + units.start..layers.start + units.end);
1378 }
1379 }
1380 ArchitectureStatePlacement::OutputOwner if self.ownership.owns_output() => {
1381 selected.push(layers);
1382 }
1383 ArchitectureStatePlacement::OutputOwner => {}
1384 }
1385 }
1386 if selected.is_empty() {
1387 return Ok(None);
1388 }
1389 selected.sort_by_key(|layers| layers.start);
1390 let start = selected[0].start;
1391 let mut end = selected[0].end;
1392 for layers in selected.iter().skip(1) {
1393 if layers.start != end {
1394 return Err(ArchitectureStatePartitionError::DiscontiguousSelection {
1395 frontier: end,
1396 start: layers.start,
1397 });
1398 }
1399 end = layers.end;
1400 }
1401 let layout = complete
1402 .slice(start..end)
1403 .map_err(|error| ArchitectureStatePartitionError::InvalidLayout(error.to_string()))?;
1404 PartitionState::new(layout, start)
1405 .map(Some)
1406 .map_err(|error| ArchitectureStatePartitionError::InvalidLayout(error.to_string()))
1407 }
1408
1409 pub const fn local_geometry(&self) -> &G {
1411 &self.local_geometry
1412 }
1413
1414 pub const fn boundary_schema(&self) -> &A {
1416 &self.boundary_schema
1417 }
1418
1419 pub fn boundary_schema_mut(&mut self) -> &mut A {
1421 &mut self.boundary_schema
1422 }
1423
1424 pub fn parameter_bindings(&self) -> &[OwnedParameterGroupSpec] {
1426 &self.parameter_bindings
1427 }
1428
1429 pub fn parameter_bindings_for_owner<'a>(
1431 &'a self,
1432 owner: &'a ParameterGroupOwner,
1433 ) -> impl Iterator<Item = &'a ParameterGroupSpec> + 'a {
1434 self.parameter_bindings
1435 .iter()
1436 .filter(move |binding| binding.owner() == owner)
1437 .map(OwnedParameterGroupSpec::group)
1438 }
1439
1440 pub fn validate_architecture<B, S, M>(
1447 &self,
1448 architecture: &M,
1449 ) -> Result<(), ArchitecturePartitionError>
1450 where
1451 B: eredu_nn::NeuralBackend,
1452 S: crate::RuntimeState<B>,
1453 M: crate::LayeredArchitecture<B, S>,
1454 M::Error: std::fmt::Display,
1455 {
1456 let (graph, unit_layout) = canonical_architecture_layout::<B, S, M>(architecture)?;
1457 if graph != self.graph {
1458 return Err(ArchitecturePartitionError::ArchitectureGraphMismatch);
1459 }
1460 if unit_layout != self.unit_layout {
1461 return Err(ArchitecturePartitionError::ArchitectureUnitLayoutMismatch);
1462 }
1463 Ok(())
1464 }
1465}
1466
1467#[derive(Debug, Clone)]
1473pub struct LayeredPartitionDriver {
1474 group: usize,
1475 range: Range<usize>,
1476 state_layout: Option<StateLayout>,
1477 owns_input: bool,
1478 owns_output: bool,
1479}
1480
1481impl LayeredPartitionDriver {
1482 pub fn new<G, A>(
1484 partition: &ArchitecturePartition<G, A>,
1485 group_index: usize,
1486 storage_range: Range<usize>,
1487 ) -> Result<Self, LayeredPartitionError> {
1488 Self::new_with_state_ownership(partition, group_index, storage_range, true)
1489 }
1490
1491 pub fn new_with_state_ownership<G, A>(
1497 partition: &ArchitecturePartition<G, A>,
1498 group_index: usize,
1499 storage_range: Range<usize>,
1500 group_owns_state: bool,
1501 ) -> Result<Self, LayeredPartitionError> {
1502 let group = partition
1503 .groups()
1504 .iter()
1505 .find(|group| group.group_index() == group_index)
1506 .ok_or(LayeredPartitionError::GroupNotOwned { group: group_index })?;
1507 let range = group.global_units();
1508 if storage_range != range {
1509 return Err(LayeredPartitionError::StorageRange {
1510 storage: storage_range,
1511 partition: range,
1512 });
1513 }
1514 if group_owns_state {
1515 let state = partition
1516 .state()
1517 .ok_or(LayeredPartitionError::MissingState)?;
1518 if state.global_layers().start > range.start || state.global_layers().end < range.end {
1519 return Err(LayeredPartitionError::StateRange {
1520 state: state.global_layers(),
1521 partition: range,
1522 });
1523 }
1524 }
1525 Ok(Self {
1526 group: group.group_index(),
1527 range,
1528 state_layout: group_owns_state
1529 .then(|| partition.state().map(|state| state.layout().clone()))
1530 .flatten(),
1531 owns_input: partition.ownership().owns_input(),
1532 owns_output: partition.ownership().owns_output(),
1533 })
1534 }
1535
1536 pub fn range(&self) -> Range<usize> {
1538 self.range.clone()
1539 }
1540
1541 pub const fn group_index(&self) -> usize {
1543 self.group
1544 }
1545
1546 pub fn state_layout(&self) -> &StateLayout {
1548 self.state_layout
1549 .as_ref()
1550 .expect("state_layout requires a state-owning layered partition driver")
1551 }
1552
1553 pub const fn optional_state_layout(&self) -> Option<&StateLayout> {
1555 self.state_layout.as_ref()
1556 }
1557
1558 pub const fn owns_input(&self) -> bool {
1560 self.owns_input
1561 }
1562
1563 pub const fn owns_output(&self) -> bool {
1565 self.owns_output
1566 }
1567
1568 pub fn input<'a, T, A>(
1570 &self,
1571 input: LayeredPartitionInput<'a, T, A>,
1572 ) -> Result<LayeredPartitionInput<'a, T, A>, LayeredPartitionError> {
1573 match (&input, self.owns_input) {
1574 (LayeredPartitionInput::Tokens(_), true)
1575 | (LayeredPartitionInput::Hidden { .. }, _) => Ok(input),
1576 (LayeredPartitionInput::Tokens(_), false) => {
1577 Err(LayeredPartitionError::TokensOnNonInputOwner)
1578 }
1579 }
1580 }
1581
1582 pub fn exchange_boundary<B>(
1588 &self,
1589 value: B::Tensor,
1590 group: &B::Group,
1591 executor: &B::Executor,
1592 ) -> Result<B::Tensor, B::CollectiveError>
1593 where
1594 B: crate::CollectiveBackend,
1595 {
1596 B::all_to_all(value, group, executor)
1597 }
1598
1599 #[allow(
1601 clippy::too_many_arguments,
1602 clippy::type_complexity,
1603 reason = "the result preserves the concrete architecture error without erased dispatch"
1604 )]
1605 pub fn begin<'a, B, S, M>(
1606 &self,
1607 architecture: &mut M,
1608 input: LayeredPartitionInput<
1609 'a,
1610 B::Tensor,
1611 <M::Boundary as ArchitectureBoundary>::Boundary<B::Tensor>,
1612 >,
1613 mask: Option<&B::Tensor>,
1614 state: &mut S,
1615 parallel: Option<&B::ParallelContext>,
1616 context: &<B::Tensor as eredu_nn::Tensor>::Context,
1617 ) -> Result<
1618 LayeredForwardState<B::Tensor, M::ForwardContext>,
1619 LayeredPartitionBeginError<M::Error>,
1620 >
1621 where
1622 B: eredu_nn::NeuralBackend,
1623 S: RuntimeState<B>,
1624 M: PartitionedLayeredArchitecture<B, S>,
1625 M::Error: std::fmt::Display,
1626 {
1627 let expected = self
1628 .state_layout
1629 .as_ref()
1630 .ok_or(LayeredPartitionBeginError::MissingState { group: self.group })?;
1631 let mut forward = match parallel {
1635 Some(parallel) => architecture
1636 .begin_partition_parallel(input, mask, state, expected, 0, parallel, context),
1637 None => architecture.begin_partition(input, mask, state, expected, 0, context),
1638 }
1639 .map_err(LayeredPartitionBeginError::Architecture)?;
1640 forward.hidden = architecture
1641 .enter_partition_group(
1642 self.group,
1643 &forward.hidden,
1644 state,
1645 &mut forward.context,
1646 parallel,
1647 context,
1648 )
1649 .map_err(LayeredPartitionBeginError::Architecture)?;
1650 Ok(forward)
1651 }
1652
1653 #[allow(
1655 clippy::too_many_arguments,
1656 clippy::type_complexity,
1657 reason = "the signature exposes the backend and architecture boundary types explicitly"
1658 )]
1659 pub fn finish<B, S, M>(
1660 &self,
1661 architecture: &mut M,
1662 hidden: &B::Tensor,
1663 state: &mut S,
1664 forward: &mut M::ForwardContext,
1665 parallel: Option<&B::ParallelContext>,
1666 context: &<B::Tensor as eredu_nn::Tensor>::Context,
1667 ) -> Result<
1668 LayeredPartitionOutput<
1669 B::Tensor,
1670 <M::Boundary as ArchitectureBoundary>::Boundary<B::Tensor>,
1671 >,
1672 M::Error,
1673 >
1674 where
1675 B: eredu_nn::NeuralBackend,
1676 S: RuntimeState<B>,
1677 M: PartitionedLayeredArchitecture<B, S>,
1678 {
1679 let hidden = architecture
1680 .leave_partition_group(self.group, hidden, state, forward, parallel, context)?;
1681 architecture.finish_partition(&hidden, state, forward, self.owns_output, parallel, context)
1682 }
1683}
1684
1685#[derive(Debug, thiserror::Error)]
1687pub enum LayeredPartitionBeginError<E>
1688where
1689 E: std::fmt::Display,
1690{
1691 #[error("stateless partition group {group} requires an architecture stateless entry strategy")]
1693 MissingState {
1694 group: usize,
1696 },
1697 #[error("partition architecture entry failed: {0}")]
1699 Architecture(E),
1700}
1701
1702#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1704#[non_exhaustive]
1705pub enum LayeredPartitionError {
1706 #[error("layered partition does not own execution group {group}")]
1708 GroupNotOwned {
1709 group: usize,
1711 },
1712 #[error("partition storage range {storage:?} disagrees with canonical range {partition:?}")]
1714 StorageRange {
1715 storage: Range<usize>,
1717 partition: Range<usize>,
1719 },
1720 #[error("layered partition has no runtime state")]
1722 MissingState,
1723 #[error("partition state range {state:?} disagrees with canonical range {partition:?}")]
1725 StateRange {
1726 state: Range<usize>,
1728 partition: Range<usize>,
1730 },
1731 #[error("non-input partition received token ids")]
1733 TokensOnNonInputOwner,
1734}
1735
1736fn canonical_architecture_layout<B, S, M>(
1737 architecture: &M,
1738) -> Result<(ExecutionGraph, ExecutionUnitLayout), ArchitecturePartitionError>
1739where
1740 B: eredu_nn::NeuralBackend,
1741 S: crate::RuntimeState<B>,
1742 M: crate::LayeredArchitecture<B, S>,
1743 M::Error: std::fmt::Display,
1744{
1745 let graph = architecture
1746 .execution_graph()
1747 .map_err(|error| ArchitecturePartitionError::ArchitectureTopology(error.to_string()))?;
1748 let primary = architecture.primary_execution_group();
1749 let primary_index = graph.group_index(primary).ok_or_else(|| {
1750 ArchitecturePartitionError::ArchitectureTopology(format!(
1751 "primary execution group {primary:?} is not present in the canonical graph"
1752 ))
1753 })?;
1754 let primary_transport = architecture.group_transport(primary_index);
1755 if primary_transport.kind != crate::ArchitectureGroupKind::Decoder
1756 || primary_transport.placement != crate::ArchitectureGroupPlacement::Pipeline
1757 {
1758 return Err(ArchitecturePartitionError::ArchitectureTopology(format!(
1759 "primary execution group {primary:?} must be a pipeline decoder"
1760 )));
1761 }
1762 let mut declared_groups = BTreeSet::from([primary.to_owned()]);
1763 for prediction in architecture.prediction_execution_groups() {
1764 let prediction_index = graph.group_index(&prediction).ok_or_else(|| {
1765 ArchitecturePartitionError::ArchitectureTopology(format!(
1766 "prediction execution group {prediction:?} is not present in the canonical graph"
1767 ))
1768 })?;
1769 let prediction_transport = architecture.group_transport(prediction_index);
1770 if prediction_transport.kind != crate::ArchitectureGroupKind::Prediction
1771 || prediction_transport.placement != crate::ArchitectureGroupPlacement::OutputOwner
1772 {
1773 return Err(ArchitecturePartitionError::ArchitectureTopology(format!(
1774 "prediction execution group {prediction:?} must be an output-owner prediction"
1775 )));
1776 }
1777 if !declared_groups.insert(prediction.clone()) {
1778 return Err(ArchitecturePartitionError::ArchitectureTopology(format!(
1779 "execution group {prediction:?} is declared as a primary or prediction group more than once"
1780 )));
1781 }
1782 }
1783 let mut counts = Vec::with_capacity(graph.groups().len());
1784 let mut paths = BTreeSet::new();
1785 for group in 0..graph.groups().len() {
1786 let count = architecture
1787 .group_unit_count(group)
1788 .map_err(|error| ArchitecturePartitionError::ArchitectureTopology(error.to_string()))?;
1789 counts.push(count);
1790 for index in 0..count {
1791 let path = architecture.unit_path(group, index).map_err(|error| {
1792 ArchitecturePartitionError::ArchitectureTopology(error.to_string())
1793 })?;
1794 if path.trim().is_empty() {
1795 return Err(ArchitecturePartitionError::EmptyArchitectureUnitPath { group, index });
1796 }
1797 if !paths.insert(path.clone()) {
1798 return Err(ArchitecturePartitionError::DuplicateArchitectureUnitPath(
1799 path,
1800 ));
1801 }
1802 }
1803 }
1804 let unit_layout = ExecutionUnitLayout::new(&graph, counts)
1805 .map_err(|error| ArchitecturePartitionError::ArchitectureTopology(error.to_string()))?;
1806 Ok((graph, unit_layout))
1807}
1808
1809fn validate_canonical_layout(
1810 graph: &ExecutionGraph,
1811 layout: &ExecutionUnitLayout,
1812) -> Result<(), ArchitecturePartitionError> {
1813 if graph.groups().len() != layout.group_count() {
1814 return Err(ArchitecturePartitionError::LayoutGroupCountMismatch {
1815 graph: graph.groups().len(),
1816 layout: layout.group_count(),
1817 });
1818 }
1819 for (index, group) in graph.groups().iter().enumerate() {
1820 let layout_group = layout
1821 .group_id(index)
1822 .expect("matching group counts provide every layout identity");
1823 if layout_group.as_str() != group.id() {
1824 return Err(ArchitecturePartitionError::LayoutGroupMismatch {
1825 index,
1826 graph: group.id().to_owned(),
1827 layout: layout_group.as_str().to_owned(),
1828 });
1829 }
1830 }
1831 Ok(())
1832}
1833
1834#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1836#[non_exhaustive]
1837pub enum ArchitecturePartitionError {
1838 #[error("invalid architecture partition boundary: {0}")]
1840 InvalidBoundary(#[from] ArchitectureBoundaryError),
1841 #[error("neutral architecture topology is invalid: {0}")]
1844 ArchitectureTopology(String),
1845 #[error("neutral architecture state is invalid: {0}")]
1847 ArchitectureState(String),
1848 #[error("architecture partition owns no mutable state")]
1850 MissingArchitectureState,
1851 #[error("architecture prompt-cache identity is invalid: {0}")]
1853 PromptCacheIdentity(String),
1854 #[error("neutral architecture unit {group}:{index} has an empty path")]
1856 EmptyArchitectureUnitPath {
1857 group: usize,
1859 index: usize,
1861 },
1862 #[error("neutral architecture repeats unit path {0:?}")]
1864 DuplicateArchitectureUnitPath(String),
1865 #[error("architecture partition dependency graph differs from the neutral architecture")]
1867 ArchitectureGraphMismatch,
1868 #[error("architecture partition unit layout differs from the neutral architecture")]
1870 ArchitectureUnitLayoutMismatch,
1871 #[error("execution graph contains {graph} groups but its unit layout contains {layout}")]
1873 LayoutGroupCountMismatch {
1874 graph: usize,
1876 layout: usize,
1878 },
1879 #[error("execution group {index} is {graph:?} in the graph but {layout:?} in the unit layout")]
1881 LayoutGroupMismatch {
1882 index: usize,
1884 graph: String,
1886 layout: String,
1888 },
1889 #[error("architecture partition names unknown execution group {0:?}")]
1891 UnknownGroup(String),
1892 #[error("architecture partition repeats execution group {0:?}")]
1894 DuplicateGroup(String),
1895 #[error("architecture partition declares an empty unit range for group {group:?}")]
1897 EmptyGroupRange {
1898 group: String,
1900 },
1901 #[error(
1903 "architecture partition range {start}..{end} for group {group:?} exceeds {available} units"
1904 )]
1905 GroupRangeOutOfBounds {
1906 group: String,
1908 start: usize,
1910 end: usize,
1912 available: usize,
1914 },
1915 #[error("architecture partition static role must not be empty")]
1917 EmptyStaticRole,
1918 #[error("architecture partition repeats static role {0:?}")]
1920 DuplicateStaticRole(String),
1921 #[error("state layer offset {offset} plus {layers} local layers overflowed usize")]
1923 StateOffsetOverflow {
1924 offset: usize,
1926 layers: usize,
1928 },
1929 #[error("architecture partition repeats parameter target {0:?}")]
1931 DuplicateParameterTarget(String),
1932 #[error("architecture partition includes non-local parameter owner {0:?}")]
1934 NonLocalParameterOwner(ParameterGroupOwner),
1935}
1936
1937#[cfg(test)]
1938mod tests {
1939 use super::*;
1940 use crate::{MemberSharding, ParameterMemberSpec, ParameterRole};
1941 use eredu_core::{cache::LayerCachePolicy, LayerSchedule};
1942
1943 #[derive(Debug, Clone, Eq, PartialEq)]
1944 struct Geometry(&'static str);
1945
1946 #[derive(Debug, Clone, Eq, PartialEq)]
1947 struct Boundary {
1948 route: usize,
1949 }
1950
1951 #[derive(Debug, Clone, Eq, PartialEq)]
1952 struct PairBoundary<T> {
1953 tokens: T,
1954 embedded: T,
1955 }
1956
1957 #[derive(Debug, Clone, Copy)]
1958 struct PairBoundarySchema;
1959
1960 impl ArchitectureBoundary for PairBoundarySchema {
1961 type Boundary<T> = PairBoundary<T>;
1962
1963 const IDENTITY: &'static str = "fixture.target";
1964
1965 fn primary_tensor_spec(&self) -> BoundaryTensorSpec {
1966 BoundaryTensorSpec::primary_activation(8)
1967 }
1968
1969 fn auxiliary_tensor_specs(&self) -> Vec<BoundaryTensorSpec> {
1970 vec![
1971 BoundaryTensorSpec::new(
1972 "tokens",
1973 [
1974 BoundaryTensorDimension::Batch,
1975 BoundaryTensorDimension::Sequence,
1976 ],
1977 BoundaryTensorDtype::Uint32,
1978 ),
1979 BoundaryTensorSpec::new(
1980 "embedded",
1981 [
1982 BoundaryTensorDimension::Batch,
1983 BoundaryTensorDimension::Sequence,
1984 BoundaryTensorDimension::Fixed(16),
1985 ],
1986 BoundaryTensorDtype::Activation,
1987 ),
1988 ]
1989 }
1990
1991 fn encode<T>(
1992 &self,
1993 boundary: Self::Boundary<T>,
1994 ) -> Result<Vec<ArchitectureBoundaryValue<T>>, ArchitectureBoundaryError> {
1995 Ok(vec![
1996 ArchitectureBoundaryValue::new("tokens", boundary.tokens)?,
1997 ArchitectureBoundaryValue::new("embedded", boundary.embedded)?,
1998 ])
1999 }
2000
2001 fn decode<T>(
2002 &self,
2003 mut tensors: Vec<T>,
2004 ) -> Result<Self::Boundary<T>, ArchitectureBoundaryError> {
2005 validate_boundary_tensor_count(self, &tensors)?;
2006 let embedded = tensors.pop().expect("validated embedded tensor");
2007 let tokens = tensors.pop().expect("validated token tensor");
2008 Ok(PairBoundary { tokens, embedded })
2009 }
2010 }
2011
2012 fn graph() -> ExecutionGraph {
2013 ExecutionGraph::chain(["primary", "prediction"]).unwrap()
2014 }
2015
2016 fn layout(graph: &ExecutionGraph) -> ExecutionUnitLayout {
2017 ExecutionUnitLayout::new(graph, [4, 3]).unwrap()
2018 }
2019
2020 fn state_layout(layers: usize) -> StateLayout {
2021 StateLayout::new(
2022 LayerSchedule::new(layers, vec![LayerCachePolicy::NoState; layers]).unwrap(),
2023 )
2024 .unwrap()
2025 }
2026
2027 fn parameter(logical: &str, target: &str) -> ParameterGroupSpec {
2028 ParameterGroupSpec::new(
2029 logical,
2030 ParameterRole::Replicated,
2031 [ParameterMemberSpec::new(
2032 target,
2033 vec![2, 2],
2034 MemberSharding::Replicated,
2035 )],
2036 )
2037 .unwrap()
2038 }
2039
2040 fn valid_partition() -> ArchitecturePartition<Geometry, Boundary> {
2041 let graph = graph();
2042 let layout = layout(&graph);
2043 ArchitecturePartition::new(
2044 graph,
2045 layout,
2046 [("prediction", 0..2), ("primary", 1..4)],
2047 PartitionOwnership::new(true, false, ["embedding", "normalization"]).unwrap(),
2048 Some(PartitionState::new(state_layout(2), 7).unwrap()),
2049 Geometry("local"),
2050 Boundary { route: 3 },
2051 [
2052 OwnedParameterGroupSpec::new(
2053 ParameterGroupOwner::static_role("embedding"),
2054 parameter("model.embed_tokens", "model.embed_tokens.weight"),
2055 ),
2056 OwnedParameterGroupSpec::new(
2057 ParameterGroupOwner::execution_unit(
2058 ExecutionGroupId::new("primary").unwrap(),
2059 1,
2060 ),
2061 parameter("model.layers.1", "model.layers.1.weight"),
2062 ),
2063 ],
2064 )
2065 .unwrap()
2066 }
2067
2068 fn state_plan_partition(
2069 primary: Range<usize>,
2070 ownership: PartitionOwnership,
2071 ) -> ArchitecturePartition<(), ()> {
2072 let graph = graph();
2073 ArchitecturePartition::new(
2074 graph.clone(),
2075 layout(&graph),
2076 [("primary", primary)],
2077 ownership,
2078 None,
2079 (),
2080 (),
2081 [],
2082 )
2083 .unwrap()
2084 }
2085
2086 #[test]
2087 fn architecture_state_plan_attaches_declared_tail_to_output_owner() {
2088 let complete = state_layout(6);
2089 let plan = ArchitectureStatePartitionPlan::new([
2090 crate::ArchitectureStatePartitionRule::group_units(0, 0..4),
2091 crate::ArchitectureStatePartitionRule::output_owner(4..6),
2092 ]);
2093 let interior = state_plan_partition(
2094 1..3,
2095 PartitionOwnership::new(false, false, std::iter::empty::<&str>()).unwrap(),
2096 );
2097 let output = state_plan_partition(
2098 3..4,
2099 PartitionOwnership::new(false, true, std::iter::empty::<&str>()).unwrap(),
2100 );
2101
2102 assert_eq!(
2103 interior
2104 .resolve_state_partition(&complete, &plan)
2105 .unwrap()
2106 .unwrap()
2107 .global_layers(),
2108 1..3
2109 );
2110 assert_eq!(
2111 output
2112 .resolve_state_partition(&complete, &plan)
2113 .unwrap()
2114 .unwrap()
2115 .global_layers(),
2116 3..6
2117 );
2118 }
2119
2120 #[test]
2121 fn architecture_state_plan_rejects_noncontiguous_local_state() {
2122 let complete = state_layout(6);
2123 let plan = ArchitectureStatePartitionPlan::new([
2124 crate::ArchitectureStatePartitionRule::output_owner(0..2),
2125 crate::ArchitectureStatePartitionRule::group_units(0, 2..6),
2126 ]);
2127 let output = state_plan_partition(
2128 3..4,
2129 PartitionOwnership::new(false, true, std::iter::empty::<&str>()).unwrap(),
2130 );
2131
2132 assert_eq!(
2133 output.resolve_state_partition(&complete, &plan),
2134 Err(ArchitectureStatePartitionError::DiscontiguousSelection {
2135 frontier: 2,
2136 start: 5,
2137 })
2138 );
2139 }
2140
2141 fn parameter_description(
2142 expected: Vec<ParameterGroupSpec>,
2143 groups: Vec<OwnedParameterGroupSpec>,
2144 ) -> Result<ArchitectureParameterDescription, ArchitectureParameterError> {
2145 let graph = graph();
2146 ArchitectureParameterDescription::new(&graph, &layout(&graph), expected, groups)
2147 }
2148
2149 #[test]
2150 fn description_driven_partition_selects_state_and_parameters_before_construction() {
2151 let embedding = parameter("embedding", "model.embed_tokens.weight");
2152 let layer = parameter("layer", "model.layers.1.weight");
2153 let description = parameter_description(
2154 vec![embedding.clone(), layer.clone()],
2155 vec![
2156 OwnedParameterGroupSpec::new(
2157 ParameterGroupOwner::static_role("embedding"),
2158 embedding,
2159 ),
2160 OwnedParameterGroupSpec::new(
2161 ParameterGroupOwner::execution_unit(
2162 ExecutionGroupId::new("primary").unwrap(),
2163 1,
2164 ),
2165 layer,
2166 ),
2167 ],
2168 )
2169 .unwrap();
2170 let ownership = PartitionOwnership::new(true, false, ["embedding"]).unwrap();
2171 let state = state_layout(4);
2172 let state_plan = ArchitectureStatePartitionPlan::new([
2173 crate::ArchitectureStatePartitionRule::group_units(0, 0..4),
2174 ]);
2175
2176 let partition = ArchitecturePartition::from_description(
2177 &description,
2178 [("primary", 1..3)],
2179 ownership,
2180 &state,
2181 &state_plan,
2182 Geometry("selected-before-allocation"),
2183 PairBoundarySchema,
2184 )
2185 .unwrap();
2186
2187 assert_eq!(partition.groups()[0].global_units(), 1..3);
2188 assert_eq!(partition.state().unwrap().global_layers(), 1..3);
2189 assert_eq!(
2190 partition.local_geometry(),
2191 &Geometry("selected-before-allocation")
2192 );
2193 assert_eq!(partition.parameter_bindings().len(), 2);
2194 assert_eq!(
2195 partition
2196 .parameter_bindings()
2197 .iter()
2198 .flat_map(|group| group.members())
2199 .map(ParameterMemberSpec::target)
2200 .collect::<Vec<_>>(),
2201 ["model.embed_tokens.weight", "model.layers.1.weight"]
2202 );
2203 }
2204
2205 #[test]
2206 fn parameter_description_selects_static_roles_and_canonical_units() {
2207 let embedding = parameter("embedding", "model.embed_tokens.weight");
2208 let layer = parameter("layer", "model.layers.1.weight");
2209 let description = parameter_description(
2210 vec![embedding.clone(), layer.clone()],
2211 vec![
2212 OwnedParameterGroupSpec::new(
2213 ParameterGroupOwner::static_role("embedding"),
2214 embedding,
2215 ),
2216 OwnedParameterGroupSpec::new(
2217 ParameterGroupOwner::execution_unit(
2218 ExecutionGroupId::new("primary").unwrap(),
2219 1,
2220 ),
2221 layer,
2222 ),
2223 ],
2224 )
2225 .unwrap();
2226 let partition = valid_partition();
2227 assert_eq!(description.graph(), partition.graph());
2228 assert_eq!(description.unit_layout(), partition.unit_layout());
2229 let selected = description.select_owned(&partition);
2230 assert_eq!(selected.len(), 2);
2231 assert_eq!(selected[0].logical_name(), "embedding");
2232 assert_eq!(selected[1].logical_name(), "layer");
2233 assert_eq!(
2234 selected[0].owner(),
2235 &ParameterGroupOwner::static_role("embedding")
2236 );
2237 assert_eq!(
2238 selected[1].owner(),
2239 &ParameterGroupOwner::execution_unit(ExecutionGroupId::new("primary").unwrap(), 1,)
2240 );
2241 }
2242
2243 #[test]
2244 fn parameter_description_selects_every_owned_target_for_a_role() {
2245 let expert = ParameterGroupSpec::new(
2246 "model.layers.1.expert_intermediate",
2247 ParameterRole::ExpertIntermediate,
2248 [
2249 ParameterMemberSpec::new(
2250 "model.layers.1.moe.packed.weight",
2251 vec![4, 2],
2252 MemberSharding::Replicated,
2253 ),
2254 ParameterMemberSpec::new(
2255 "model.layers.1.moe.packed.scales",
2256 vec![4, 1],
2257 MemberSharding::Replicated,
2258 ),
2259 ParameterMemberSpec::new(
2260 "model.layers.1.moe.alias.biases",
2261 vec![4, 1],
2262 MemberSharding::Replicated,
2263 ),
2264 ],
2265 )
2266 .unwrap();
2267 let replicated = parameter("router", "model.layers.1.moe.router.weight");
2268 let owner =
2269 ParameterGroupOwner::execution_unit(ExecutionGroupId::new("primary").unwrap(), 1);
2270 let description = parameter_description(
2271 vec![expert.clone(), replicated.clone()],
2272 vec![
2273 OwnedParameterGroupSpec::new(owner.clone(), expert),
2274 OwnedParameterGroupSpec::new(owner, replicated),
2275 ],
2276 )
2277 .unwrap();
2278
2279 assert_eq!(
2280 description.targets_for_role(ParameterRole::ExpertIntermediate),
2281 BTreeSet::from([
2282 "model.layers.1.moe.alias.biases".to_owned(),
2283 "model.layers.1.moe.packed.scales".to_owned(),
2284 "model.layers.1.moe.packed.weight".to_owned(),
2285 ])
2286 );
2287 }
2288
2289 #[test]
2290 fn parameter_description_selects_shared_static_owner_by_any_consumer() {
2291 let embedding = parameter("embedding", "model.embed_tokens.weight");
2292 let description = parameter_description(
2293 vec![embedding.clone()],
2294 vec![OwnedParameterGroupSpec::new(
2295 ParameterGroupOwner::static_any_of(["output", "embedding"]),
2296 embedding,
2297 )],
2298 )
2299 .unwrap();
2300 assert_eq!(description.select_owned(&valid_partition()).len(), 1);
2301
2302 let duplicate = parameter("embedding", "model.embed_tokens.weight");
2303 assert_eq!(
2304 parameter_description(
2305 vec![duplicate.clone()],
2306 vec![OwnedParameterGroupSpec::new(
2307 ParameterGroupOwner::static_any_of(["embedding", "embedding"]),
2308 duplicate,
2309 )],
2310 )
2311 .unwrap_err(),
2312 ArchitectureParameterError::DuplicateStaticRole,
2313 );
2314 }
2315
2316 #[test]
2317 fn partition_rejects_parameter_owner_outside_local_unit_ranges() {
2318 let graph = graph();
2319 let error = ArchitecturePartition::new(
2320 graph.clone(),
2321 layout(&graph),
2322 [("primary", 1..4)],
2323 PartitionOwnership::new(false, false, ["embedding"]).unwrap(),
2324 None,
2325 (),
2326 (),
2327 [OwnedParameterGroupSpec::new(
2328 ParameterGroupOwner::execution_unit(
2329 ExecutionGroupId::new("prediction").unwrap(),
2330 0,
2331 ),
2332 parameter("prediction", "prediction.weight"),
2333 )],
2334 )
2335 .unwrap_err();
2336 assert!(matches!(
2337 error,
2338 ArchitecturePartitionError::NonLocalParameterOwner(
2339 ParameterGroupOwner::ExecutionUnit { .. }
2340 )
2341 ));
2342 }
2343
2344 #[test]
2345 fn parameter_description_rejects_missing_duplicate_and_out_of_range_ownership() {
2346 let embedding = parameter("embedding", "model.embed_tokens.weight");
2347 let layer = parameter("layer", "model.layers.1.weight");
2348 assert_eq!(
2349 parameter_description(
2350 vec![embedding.clone(), layer.clone()],
2351 vec![OwnedParameterGroupSpec::new(
2352 ParameterGroupOwner::static_role("embedding"),
2353 embedding.clone(),
2354 )],
2355 )
2356 .unwrap_err(),
2357 ArchitectureParameterError::MissingOwnership("model.layers.1.weight".into())
2358 );
2359 assert!(matches!(
2360 parameter_description(
2361 vec![embedding.clone()],
2362 vec![
2363 OwnedParameterGroupSpec::new(
2364 ParameterGroupOwner::static_role("embedding"),
2365 embedding.clone(),
2366 ),
2367 OwnedParameterGroupSpec::new(
2368 ParameterGroupOwner::static_role("output"),
2369 embedding.clone(),
2370 ),
2371 ],
2372 )
2373 .unwrap_err(),
2374 ArchitectureParameterError::DuplicateOwnership { .. }
2375 ));
2376 assert_eq!(
2377 parameter_description(
2378 vec![layer.clone()],
2379 vec![OwnedParameterGroupSpec::new(
2380 ParameterGroupOwner::execution_unit(
2381 ExecutionGroupId::new("prediction").unwrap(),
2382 3,
2383 ),
2384 layer,
2385 )],
2386 )
2387 .unwrap_err(),
2388 ArchitectureParameterError::UnitOutOfRange {
2389 group: "prediction".into(),
2390 global_unit: 3,
2391 available: 3,
2392 }
2393 );
2394 }
2395
2396 #[test]
2397 fn retains_canonical_topology_ownership_and_typed_family_values() {
2398 let mut partition = valid_partition();
2399 assert_eq!(partition.graph().groups().len(), 2);
2400 assert_eq!(partition.unit_layout().len(), 7);
2401 assert_eq!(partition.groups()[0].group().as_str(), "primary");
2402 assert_eq!(partition.groups()[0].group_index(), 0);
2403 assert_eq!(partition.groups()[0].global_units(), 1..4);
2404 assert!(partition.owns_unit("primary", 3));
2405 assert!(!partition.owns_unit("primary", 0));
2406 assert!(partition.ownership().owns_input());
2407 assert!(!partition.ownership().owns_output());
2408 assert!(partition.ownership().owns_static_role("embedding"));
2409 assert_eq!(
2410 partition
2411 .units()
2412 .map(|unit| (unit.group(), unit.index()))
2413 .collect::<Vec<_>>(),
2414 [(0, 1), (0, 2), (0, 3), (1, 0), (1, 1)]
2415 );
2416 assert_eq!(partition.state().unwrap().global_layers(), 7..9);
2417 assert_eq!(partition.local_geometry(), &Geometry("local"));
2418 partition.boundary_schema_mut().route = 5;
2419 assert_eq!(partition.boundary_schema().route, 5);
2420 assert_eq!(partition.parameter_bindings().len(), 2);
2421 }
2422
2423 #[test]
2424 fn typed_boundary_owns_roles_order_and_atomic_cardinality_validation() {
2425 let boundary = PairBoundary {
2426 tokens: 3,
2427 embedded: 7,
2428 };
2429 let schema = PairBoundarySchema;
2430 let values = schema.encode(boundary).unwrap();
2431 assert_eq!(values[0].role(), "tokens");
2432 assert_eq!(values[1].role(), "embedded");
2433 let tensors = values
2434 .into_iter()
2435 .map(ArchitectureBoundaryValue::into_parts)
2436 .map(|(_, tensor)| tensor)
2437 .collect();
2438 assert_eq!(
2439 schema.decode(tensors).unwrap(),
2440 PairBoundary {
2441 tokens: 3,
2442 embedded: 7
2443 }
2444 );
2445 let resolved = schema.wire_schema().unwrap().resolve(2, 3).unwrap();
2446 assert_eq!(resolved.primary().shape(), [2, 3, 8]);
2447 assert_eq!(resolved.primary().dtype(), BoundaryTensorDtype::Activation);
2448 assert_eq!(resolved.auxiliary()[0].shape(), [2, 3]);
2449 assert_eq!(resolved.auxiliary()[0].dtype(), BoundaryTensorDtype::Uint32);
2450 assert_eq!(resolved.auxiliary()[1].shape(), [2, 3, 16]);
2451 assert_eq!(
2452 resolved.auxiliary()[1].dtype(),
2453 BoundaryTensorDtype::Activation
2454 );
2455 assert_eq!(
2456 schema.decode(vec![3]).unwrap_err(),
2457 ArchitectureBoundaryError::TensorCount {
2458 boundary: "fixture.target",
2459 expected: 2,
2460 actual: 1,
2461 }
2462 );
2463 }
2464
2465 #[test]
2466 fn boundary_schema_rejects_role_and_geometry_drift_before_transport() {
2467 let invalid_primary = BoundaryWireSchema::new(
2468 "fixture.invalid",
2469 BoundaryTensorSpec::new(
2470 "hidden",
2471 [BoundaryTensorDimension::Fixed(8)],
2472 BoundaryTensorDtype::Uint32,
2473 ),
2474 [],
2475 )
2476 .unwrap_err();
2477 assert_eq!(
2478 invalid_primary,
2479 ArchitectureBoundaryError::InvalidPrimaryDtype {
2480 boundary: "fixture.invalid",
2481 }
2482 );
2483
2484 let duplicate = BoundaryWireSchema::new(
2485 "fixture.invalid",
2486 BoundaryTensorSpec::primary_activation(8),
2487 [
2488 BoundaryTensorSpec::new(
2489 "state",
2490 [BoundaryTensorDimension::Fixed(1)],
2491 BoundaryTensorDtype::Activation,
2492 ),
2493 BoundaryTensorSpec::new(
2494 "state",
2495 [BoundaryTensorDimension::Fixed(2)],
2496 BoundaryTensorDtype::Activation,
2497 ),
2498 ],
2499 )
2500 .unwrap_err();
2501 assert_eq!(
2502 duplicate,
2503 ArchitectureBoundaryError::DuplicateTensorRole {
2504 boundary: "fixture.invalid",
2505 role: "state".into(),
2506 }
2507 );
2508
2509 let invalid = BoundaryWireSchema::new(
2510 "fixture.invalid",
2511 BoundaryTensorSpec::primary_activation(8),
2512 [BoundaryTensorSpec::new(
2513 "state",
2514 [BoundaryTensorDimension::Fixed(0)],
2515 BoundaryTensorDtype::Activation,
2516 )],
2517 )
2518 .unwrap_err();
2519 assert_eq!(
2520 invalid,
2521 ArchitectureBoundaryError::InvalidTensorDimension {
2522 boundary: "fixture.invalid",
2523 role: "state".into(),
2524 }
2525 );
2526 }
2527
2528 #[test]
2529 fn rejects_noncanonical_unknown_and_duplicate_groups() {
2530 let graph = graph();
2531 let mismatched_graph = ExecutionGraph::chain(["primary", "other"]).unwrap();
2532 let error = ArchitecturePartition::new(
2533 graph.clone(),
2534 layout(&mismatched_graph),
2535 [("primary", 0..1)],
2536 PartitionOwnership::new(false, false, std::iter::empty::<String>()).unwrap(),
2537 None,
2538 (),
2539 (),
2540 std::iter::empty(),
2541 )
2542 .unwrap_err();
2543 assert!(matches!(
2544 error,
2545 ArchitecturePartitionError::LayoutGroupMismatch { .. }
2546 ));
2547
2548 let error = ArchitecturePartition::new(
2549 graph.clone(),
2550 layout(&graph),
2551 [("missing", 0..1)],
2552 PartitionOwnership::new(false, false, std::iter::empty::<String>()).unwrap(),
2553 None,
2554 (),
2555 (),
2556 std::iter::empty(),
2557 )
2558 .unwrap_err();
2559 assert_eq!(
2560 error,
2561 ArchitecturePartitionError::UnknownGroup("missing".into())
2562 );
2563
2564 let error = ArchitecturePartition::new(
2565 graph.clone(),
2566 layout(&graph),
2567 [("primary", 0..1), ("primary", 1..2)],
2568 PartitionOwnership::new(false, false, std::iter::empty::<String>()).unwrap(),
2569 None,
2570 (),
2571 (),
2572 std::iter::empty(),
2573 )
2574 .unwrap_err();
2575 assert_eq!(
2576 error,
2577 ArchitecturePartitionError::DuplicateGroup("primary".into())
2578 );
2579 }
2580
2581 #[test]
2582 fn rejects_empty_and_out_of_bounds_group_ranges() {
2583 let graph = graph();
2584 let error = ArchitecturePartition::new(
2585 graph.clone(),
2586 layout(&graph),
2587 [("primary", 2..2)],
2588 PartitionOwnership::new(false, false, std::iter::empty::<String>()).unwrap(),
2589 None,
2590 (),
2591 (),
2592 std::iter::empty(),
2593 )
2594 .unwrap_err();
2595 assert!(matches!(
2596 error,
2597 ArchitecturePartitionError::EmptyGroupRange { .. }
2598 ));
2599
2600 let error = ArchitecturePartition::new(
2601 graph.clone(),
2602 layout(&graph),
2603 [("prediction", 1..4)],
2604 PartitionOwnership::new(false, false, std::iter::empty::<String>()).unwrap(),
2605 None,
2606 (),
2607 (),
2608 std::iter::empty(),
2609 )
2610 .unwrap_err();
2611 assert!(matches!(
2612 error,
2613 ArchitecturePartitionError::GroupRangeOutOfBounds { .. }
2614 ));
2615 }
2616
2617 #[test]
2618 fn rejects_state_offset_overflow() {
2619 assert_eq!(
2620 PartitionState::new(state_layout(2), usize::MAX).unwrap_err(),
2621 ArchitecturePartitionError::StateOffsetOverflow {
2622 offset: usize::MAX,
2623 layers: 2,
2624 }
2625 );
2626 }
2627
2628 #[test]
2629 fn rejects_empty_static_roles_and_duplicate_parameter_targets() {
2630 assert_eq!(
2631 PartitionOwnership::new(false, false, [" "]).unwrap_err(),
2632 ArchitecturePartitionError::EmptyStaticRole
2633 );
2634
2635 let graph = graph();
2636 let error = ArchitecturePartition::new(
2637 graph.clone(),
2638 layout(&graph),
2639 [("primary", 0..1)],
2640 PartitionOwnership::new(false, false, ["embedding", "normalization"]).unwrap(),
2641 None,
2642 (),
2643 (),
2644 [
2645 OwnedParameterGroupSpec::new(
2646 ParameterGroupOwner::static_role("embedding"),
2647 parameter("first", "shared.weight"),
2648 ),
2649 OwnedParameterGroupSpec::new(
2650 ParameterGroupOwner::static_role("normalization"),
2651 parameter("second", "shared.weight"),
2652 ),
2653 ],
2654 )
2655 .unwrap_err();
2656 assert_eq!(
2657 error,
2658 ArchitecturePartitionError::DuplicateParameterTarget("shared.weight".into())
2659 );
2660 }
2661
2662 fn layered_partition(
2663 storage_state: Range<usize>,
2664 owns_input: bool,
2665 ) -> ArchitecturePartition<(), ()> {
2666 let graph = ExecutionGraph::chain(["decoder"]).unwrap();
2667 let layout = ExecutionUnitLayout::new(&graph, [4]).unwrap();
2668 ArchitecturePartition::new(
2669 graph,
2670 layout,
2671 [("decoder", 1..3)],
2672 PartitionOwnership::new(owns_input, false, std::iter::empty::<String>()).unwrap(),
2673 Some(
2674 PartitionState::new(state_layout(storage_state.len()), storage_state.start)
2675 .unwrap(),
2676 ),
2677 (),
2678 (),
2679 std::iter::empty(),
2680 )
2681 .unwrap()
2682 }
2683
2684 #[test]
2685 fn layered_driver_rejects_storage_and_state_range_drift() {
2686 let partition = layered_partition(1..3, true);
2687 assert!(LayeredPartitionDriver::new(&partition, 0, 1..3).is_ok());
2688 assert_eq!(
2689 LayeredPartitionDriver::new(&partition, 0, 0..2).unwrap_err(),
2690 LayeredPartitionError::StorageRange {
2691 storage: 0..2,
2692 partition: 1..3,
2693 }
2694 );
2695
2696 let partition = layered_partition(0..2, true);
2697 assert_eq!(
2698 LayeredPartitionDriver::new(&partition, 0, 1..3).unwrap_err(),
2699 LayeredPartitionError::StateRange {
2700 state: 0..2,
2701 partition: 1..3,
2702 }
2703 );
2704 }
2705
2706 #[test]
2707 fn layered_driver_represents_stateless_root_without_borrowing_decoder_state() {
2708 let graph = ExecutionGraph::chain(["vision", "decoder"]).unwrap();
2709 let layout = ExecutionUnitLayout::new(&graph, [1, 2]).unwrap();
2710 let partition = ArchitecturePartition::new(
2711 graph,
2712 layout,
2713 [("vision", 0..1), ("decoder", 0..2)],
2714 PartitionOwnership::new(true, true, std::iter::empty::<String>()).unwrap(),
2715 Some(PartitionState::new(state_layout(1), 1).unwrap()),
2716 (),
2717 (),
2718 std::iter::empty(),
2719 )
2720 .unwrap();
2721
2722 let vision =
2723 LayeredPartitionDriver::new_with_state_ownership(&partition, 0, 0..1, false).unwrap();
2724 assert!(vision.optional_state_layout().is_none());
2725 assert_eq!(vision.group_index(), 0);
2726
2727 let without_state = ArchitecturePartition::new(
2728 ExecutionGraph::chain(["vision"]).unwrap(),
2729 ExecutionUnitLayout::new(&ExecutionGraph::chain(["vision"]).unwrap(), [1]).unwrap(),
2730 [("vision", 0..1)],
2731 PartitionOwnership::new(true, false, std::iter::empty::<String>()).unwrap(),
2732 None,
2733 (),
2734 (),
2735 std::iter::empty(),
2736 )
2737 .unwrap();
2738 assert_eq!(
2739 LayeredPartitionDriver::new(&without_state, 0, 0..1).unwrap_err(),
2740 LayeredPartitionError::MissingState
2741 );
2742 assert!(
2743 LayeredPartitionDriver::new_with_state_ownership(&without_state, 0, 0..1, false)
2744 .unwrap()
2745 .optional_state_layout()
2746 .is_none()
2747 );
2748 }
2749
2750 #[test]
2751 fn layered_driver_restricts_tokens_but_accepts_architecture_prepared_hidden() {
2752 let input_owner =
2753 LayeredPartitionDriver::new(&layered_partition(1..3, true), 0, 1..3).unwrap();
2754 assert!(matches!(
2755 input_owner.input(LayeredPartitionInput::<i32, NoAuxiliaryBoundary>::Tokens(
2756 &7
2757 )),
2758 Ok(LayeredPartitionInput::Tokens(7))
2759 ));
2760 assert!(matches!(
2761 input_owner.input(LayeredPartitionInput::Hidden {
2762 hidden: 7,
2763 auxiliary: NoAuxiliaryBoundary,
2764 }),
2765 Ok(LayeredPartitionInput::Hidden {
2766 hidden: 7,
2767 auxiliary: NoAuxiliaryBoundary,
2768 })
2769 ));
2770
2771 let hidden_owner =
2772 LayeredPartitionDriver::new(&layered_partition(1..3, false), 0, 1..3).unwrap();
2773 assert_eq!(
2774 hidden_owner
2775 .input(LayeredPartitionInput::<i32, NoAuxiliaryBoundary>::Tokens(
2776 &7
2777 ))
2778 .unwrap_err(),
2779 LayeredPartitionError::TokensOnNonInputOwner
2780 );
2781 assert!(matches!(
2782 hidden_owner.input(LayeredPartitionInput::Hidden {
2783 hidden: 7,
2784 auxiliary: NoAuxiliaryBoundary,
2785 }),
2786 Ok(LayeredPartitionInput::Hidden {
2787 hidden: 7,
2788 auxiliary: NoAuxiliaryBoundary,
2789 })
2790 ));
2791 }
2792}