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 if batch_size <= 0 || sequence_length <= 0 {
579 return Err(ArchitectureBoundaryError::InvalidInvocationGeometry {
580 boundary: self.identity,
581 batch_size,
582 sequence_length,
583 });
584 }
585 let resolve = |tensor: &BoundaryTensorSpec| ResolvedBoundaryTensorSpec {
586 role: tensor.role.clone(),
587 shape: tensor
588 .shape
589 .iter()
590 .map(|dimension| match dimension {
591 BoundaryTensorDimension::Batch => batch_size,
592 BoundaryTensorDimension::Sequence => sequence_length,
593 BoundaryTensorDimension::Fixed(value) => *value,
594 })
595 .collect(),
596 dtype: tensor.dtype,
597 };
598 Ok(ResolvedBoundaryWireSchema {
599 identity: self.identity,
600 primary: resolve(&self.primary),
601 auxiliary: self.auxiliary.iter().map(resolve).collect(),
602 })
603 }
604}
605
606#[derive(Debug, Clone, Eq, Hash, PartialEq)]
608pub struct ResolvedBoundaryWireSchema {
609 identity: &'static str,
610 primary: ResolvedBoundaryTensorSpec,
611 auxiliary: Vec<ResolvedBoundaryTensorSpec>,
612}
613
614impl ResolvedBoundaryWireSchema {
615 pub const fn identity(&self) -> &'static str {
617 self.identity
618 }
619
620 pub const fn primary(&self) -> &ResolvedBoundaryTensorSpec {
622 &self.primary
623 }
624
625 pub fn auxiliary(&self) -> &[ResolvedBoundaryTensorSpec] {
627 &self.auxiliary
628 }
629}
630
631pub trait ArchitectureBoundary: Sized {
638 type Boundary<T>;
640
641 const IDENTITY: &'static str;
644
645 fn primary_tensor_spec(&self) -> BoundaryTensorSpec;
647
648 fn auxiliary_tensor_specs(&self) -> Vec<BoundaryTensorSpec>;
650
651 fn encode<T>(&self, boundary: Self::Boundary<T>) -> Result<Vec<T>, ArchitectureBoundaryError>;
653
654 fn decode<T>(&self, tensors: Vec<T>) -> Result<Self::Boundary<T>, ArchitectureBoundaryError>;
656
657 fn wire_schema(&self) -> Result<BoundaryWireSchema, ArchitectureBoundaryError> {
659 BoundaryWireSchema::new(
660 Self::IDENTITY,
661 self.primary_tensor_spec(),
662 self.auxiliary_tensor_specs(),
663 )
664 }
665}
666
667#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
673pub struct NoAuxiliaryBoundary;
674
675#[derive(Debug, Clone, Copy, Eq, PartialEq)]
677pub struct NoAuxiliaryBoundarySchema {
678 hidden_size: i32,
679}
680
681impl NoAuxiliaryBoundarySchema {
682 pub const fn new(hidden_size: i32) -> Self {
684 Self { hidden_size }
685 }
686}
687
688impl ArchitectureBoundary for NoAuxiliaryBoundarySchema {
689 type Boundary<T> = NoAuxiliaryBoundary;
690
691 const IDENTITY: &'static str = "none";
692
693 fn primary_tensor_spec(&self) -> BoundaryTensorSpec {
694 BoundaryTensorSpec::primary_activation(self.hidden_size)
695 }
696
697 fn auxiliary_tensor_specs(&self) -> Vec<BoundaryTensorSpec> {
698 Vec::new()
699 }
700
701 fn encode<T>(&self, _boundary: Self::Boundary<T>) -> Result<Vec<T>, ArchitectureBoundaryError> {
702 Ok(Vec::new())
703 }
704
705 fn decode<T>(&self, tensors: Vec<T>) -> Result<Self::Boundary<T>, ArchitectureBoundaryError> {
706 validate_boundary_tensor_count(self, &tensors)?;
707 Ok(NoAuxiliaryBoundary)
708 }
709}
710
711pub fn validate_boundary_tensor_count<B, T>(
714 boundary: &B,
715 tensors: &[T],
716) -> Result<(), ArchitectureBoundaryError>
717where
718 B: ArchitectureBoundary,
719{
720 let expected = boundary.wire_schema()?.auxiliary().len();
721 let actual = tensors.len();
722 if actual != expected {
723 return Err(ArchitectureBoundaryError::TensorCount {
724 boundary: B::IDENTITY,
725 expected,
726 actual,
727 });
728 }
729 Ok(())
730}
731
732#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
734#[non_exhaustive]
735pub enum ArchitectureBoundaryError {
736 #[error("architecture boundary identity must not be empty")]
738 EmptyIdentity,
739 #[error("architecture boundary {boundary:?} primary tensor must use activation dtype")]
741 InvalidPrimaryDtype {
742 boundary: &'static str,
744 },
745 #[error("architecture boundary {boundary:?} contains an empty tensor role")]
747 EmptyTensorRole {
748 boundary: &'static str,
750 },
751 #[error("architecture boundary {boundary:?} repeats tensor role {role:?}")]
753 DuplicateTensorRole {
754 boundary: &'static str,
756 role: String,
758 },
759 #[error("architecture boundary {boundary:?} tensor {role:?} has no dimensions")]
761 EmptyTensorShape {
762 boundary: &'static str,
764 role: String,
766 },
767 #[error("architecture boundary {boundary:?} tensor {role:?} has a non-positive dimension")]
769 InvalidTensorDimension {
770 boundary: &'static str,
772 role: String,
774 },
775 #[error(
777 "architecture boundary {boundary:?} requires positive invocation geometry, got batch {batch_size} and sequence {sequence_length}"
778 )]
779 InvalidInvocationGeometry {
780 boundary: &'static str,
782 batch_size: i32,
784 sequence_length: i32,
786 },
787 #[error(
789 "architecture boundary {boundary:?} expected {expected} tensors but received {actual}"
790 )]
791 TensorCount {
792 boundary: &'static str,
794 expected: usize,
796 actual: usize,
798 },
799 #[error("architecture boundary {boundary:?} is invalid: {detail}")]
801 Invalid {
802 boundary: &'static str,
804 detail: String,
806 },
807}
808
809#[derive(Debug, Clone, Eq, PartialEq)]
811pub struct PartitionOwnership {
812 input: bool,
813 output: bool,
814 static_roles: Vec<String>,
815}
816
817impl PartitionOwnership {
818 pub fn new(
820 input: bool,
821 output: bool,
822 static_roles: impl IntoIterator<Item = impl Into<String>>,
823 ) -> Result<Self, ArchitecturePartitionError> {
824 let static_roles = static_roles.into_iter().map(Into::into).collect::<Vec<_>>();
825 let mut unique = BTreeSet::new();
826 for role in &static_roles {
827 if role.trim().is_empty() {
828 return Err(ArchitecturePartitionError::EmptyStaticRole);
829 }
830 if !unique.insert(role.clone()) {
831 return Err(ArchitecturePartitionError::DuplicateStaticRole(
832 role.clone(),
833 ));
834 }
835 }
836 Ok(Self {
837 input,
838 output,
839 static_roles,
840 })
841 }
842
843 pub const fn owns_input(&self) -> bool {
845 self.input
846 }
847
848 pub const fn owns_output(&self) -> bool {
850 self.output
851 }
852
853 pub fn static_roles(&self) -> &[String] {
855 &self.static_roles
856 }
857
858 pub fn owns_static_role(&self, role: &str) -> bool {
860 self.static_roles.iter().any(|candidate| candidate == role)
861 }
862}
863
864#[derive(Debug, Clone, Eq, PartialEq)]
866pub struct PartitionState {
867 layout: StateLayout,
868 global_layers: Range<usize>,
869}
870
871impl PartitionState {
872 pub fn new(
874 layout: StateLayout,
875 global_layer_offset: usize,
876 ) -> Result<Self, ArchitecturePartitionError> {
877 let end = global_layer_offset.checked_add(layout.len()).ok_or(
878 ArchitecturePartitionError::StateOffsetOverflow {
879 offset: global_layer_offset,
880 layers: layout.len(),
881 },
882 )?;
883 Ok(Self {
884 layout,
885 global_layers: global_layer_offset..end,
886 })
887 }
888
889 pub const fn layout(&self) -> &StateLayout {
891 &self.layout
892 }
893
894 pub const fn global_layer_offset(&self) -> usize {
896 self.global_layers.start
897 }
898
899 pub fn global_layers(&self) -> Range<usize> {
901 self.global_layers.clone()
902 }
903
904 pub fn prompt_cache_identity<B, M>(
906 &self,
907 architecture: &M,
908 topology: eredu_core::cache::PromptCacheTopology,
909 ) -> Result<eredu_core::cache::PromptCacheModelIdentity, ArchitecturePartitionError>
910 where
911 B: eredu_nn::NeuralBackend,
912 M: crate::ArchitectureParameters<B>,
913 M::DefinitionError: std::fmt::Display,
914 {
915 architecture
916 .state_identity(self, topology)
917 .map_err(|error| ArchitecturePartitionError::ArchitectureState(error.to_string()))?
918 .prompt_cache_identity(self.layout())
919 .map_err(|error| ArchitecturePartitionError::PromptCacheIdentity(error.to_string()))
920 }
921}
922
923#[derive(Debug, Clone, Eq, PartialEq)]
925pub struct PartitionGroup {
926 group: ExecutionGroupId,
927 group_index: usize,
928 global_units: Range<usize>,
929}
930
931impl PartitionGroup {
932 pub const fn group(&self) -> &ExecutionGroupId {
934 &self.group
935 }
936
937 pub const fn group_index(&self) -> usize {
939 self.group_index
940 }
941
942 pub fn global_units(&self) -> Range<usize> {
944 self.global_units.clone()
945 }
946
947 pub fn contains(&self, global_unit: usize) -> bool {
949 self.global_units.contains(&global_unit)
950 }
951}
952
953#[derive(Debug, Clone)]
958pub struct ArchitecturePartition<G, A> {
959 graph: ExecutionGraph,
960 unit_layout: ExecutionUnitLayout,
961 groups: Vec<PartitionGroup>,
962 ownership: PartitionOwnership,
963 state: Option<PartitionState>,
964 local_geometry: G,
965 boundary_schema: A,
966 parameter_bindings: Vec<OwnedParameterGroupSpec>,
967}
968
969impl<G, A> ArchitecturePartition<G, A> {
970 #[allow(clippy::too_many_arguments)]
978 pub fn from_architecture<B, S, M, N>(
979 architecture: &M,
980 group_ranges: impl IntoIterator<Item = (N, Range<usize>)>,
981 ownership: PartitionOwnership,
982 local_geometry: G,
983 boundary_schema: A,
984 parameters: &ArchitectureParameterDescription,
985 ) -> Result<Self, ArchitecturePartitionError>
986 where
987 B: eredu_nn::NeuralBackend,
988 S: crate::RuntimeState<B>,
989 M: crate::LayeredArchitecture<B, S>,
990 M::Error: std::fmt::Display,
991 N: Into<String>,
992 A: ArchitectureBoundary,
993 {
994 let (graph, unit_layout) = canonical_architecture_layout::<B, S, M>(architecture)?;
995 boundary_schema.wire_schema()?;
996 if parameters.graph() != &graph {
997 return Err(ArchitecturePartitionError::ArchitectureGraphMismatch);
998 }
999 if parameters.unit_layout() != &unit_layout {
1000 return Err(ArchitecturePartitionError::ArchitectureUnitLayoutMismatch);
1001 }
1002 let complete_state = architecture
1003 .state_layout()
1004 .map_err(|error| ArchitecturePartitionError::ArchitectureState(error.to_string()))?;
1005 let plan = architecture.state_partition_plan(&complete_state);
1006 let mut partition = Self::new(
1007 graph,
1008 unit_layout,
1009 group_ranges,
1010 ownership,
1011 None,
1012 local_geometry,
1013 boundary_schema,
1014 std::iter::empty(),
1015 )?;
1016 partition.state = partition
1017 .resolve_state_partition(&complete_state, &plan)
1018 .map_err(|error| ArchitecturePartitionError::ArchitectureState(error.to_string()))?;
1019 partition.parameter_bindings = parameters.select_owned(&partition);
1020 Ok(partition)
1021 }
1022
1023 #[allow(clippy::too_many_arguments)]
1026 fn new<S>(
1027 graph: ExecutionGraph,
1028 unit_layout: ExecutionUnitLayout,
1029 group_ranges: impl IntoIterator<Item = (S, Range<usize>)>,
1030 ownership: PartitionOwnership,
1031 state: Option<PartitionState>,
1032 local_geometry: G,
1033 boundary_schema: A,
1034 parameter_bindings: impl IntoIterator<Item = OwnedParameterGroupSpec>,
1035 ) -> Result<Self, ArchitecturePartitionError>
1036 where
1037 S: Into<String>,
1038 {
1039 validate_canonical_layout(&graph, &unit_layout)?;
1040 let mut seen_groups = BTreeSet::new();
1041 let mut groups = Vec::new();
1042 for (group, global_units) in group_ranges {
1043 let group = group.into();
1044 let group_index = graph
1045 .groups()
1046 .iter()
1047 .position(|candidate| candidate.id() == group)
1048 .ok_or_else(|| ArchitecturePartitionError::UnknownGroup(group.clone()))?;
1049 if !seen_groups.insert(group.clone()) {
1050 return Err(ArchitecturePartitionError::DuplicateGroup(group));
1051 }
1052 if global_units.is_empty() {
1053 return Err(ArchitecturePartitionError::EmptyGroupRange { group });
1054 }
1055 let available = unit_layout
1056 .group_range(group_index)
1057 .expect("canonical layout contains every graph group")
1058 .len();
1059 if global_units.end > available {
1060 return Err(ArchitecturePartitionError::GroupRangeOutOfBounds {
1061 group,
1062 start: global_units.start,
1063 end: global_units.end,
1064 available,
1065 });
1066 }
1067 groups.push(PartitionGroup {
1068 group: unit_layout
1069 .group_id(group_index)
1070 .expect("canonical layout contains every graph group identity")
1071 .clone(),
1072 group_index,
1073 global_units,
1074 });
1075 }
1076 groups.sort_by_key(PartitionGroup::group_index);
1077
1078 let parameter_bindings = parameter_bindings.into_iter().collect::<Vec<_>>();
1079 let mut targets = BTreeSet::new();
1080 for binding in ¶meter_bindings {
1081 if !binding
1082 .owner()
1083 .is_local_partition_parts(&groups, &ownership)
1084 {
1085 return Err(ArchitecturePartitionError::NonLocalParameterOwner(
1086 binding.owner().clone(),
1087 ));
1088 }
1089 for member in binding.members() {
1090 if !targets.insert(member.target().to_owned()) {
1091 return Err(ArchitecturePartitionError::DuplicateParameterTarget(
1092 member.target().to_owned(),
1093 ));
1094 }
1095 }
1096 }
1097
1098 Ok(Self {
1099 graph,
1100 unit_layout,
1101 groups,
1102 ownership,
1103 state,
1104 local_geometry,
1105 boundary_schema,
1106 parameter_bindings,
1107 })
1108 }
1109
1110 pub const fn graph(&self) -> &ExecutionGraph {
1112 &self.graph
1113 }
1114
1115 pub const fn unit_layout(&self) -> &ExecutionUnitLayout {
1117 &self.unit_layout
1118 }
1119
1120 pub fn groups(&self) -> &[PartitionGroup] {
1122 &self.groups
1123 }
1124
1125 pub fn units(&self) -> impl Iterator<Item = crate::ExecutionUnitAddress> + '_ {
1127 self.groups.iter().flat_map(move |owned| {
1128 let group = owned.group_index;
1129 let base = self
1130 .unit_layout
1131 .group_range(group)
1132 .expect("partition group belongs to its canonical layout")
1133 .start;
1134 owned.global_units.clone().map(move |index| {
1135 self.unit_layout
1136 .address(base + index)
1137 .expect("partition unit belongs to its canonical layout")
1138 })
1139 })
1140 }
1141
1142 pub fn owns_unit(&self, group: &str, global_unit: usize) -> bool {
1144 self.groups
1145 .iter()
1146 .any(|owned| owned.group.as_str() == group && owned.contains(global_unit))
1147 }
1148
1149 pub const fn ownership(&self) -> &PartitionOwnership {
1151 &self.ownership
1152 }
1153
1154 pub const fn state(&self) -> Option<&PartitionState> {
1156 self.state.as_ref()
1157 }
1158
1159 pub fn prompt_cache_identity<B, M>(
1161 &self,
1162 architecture: &M,
1163 topology: eredu_core::cache::PromptCacheTopology,
1164 ) -> Result<eredu_core::cache::PromptCacheModelIdentity, ArchitecturePartitionError>
1165 where
1166 B: eredu_nn::NeuralBackend,
1167 M: crate::ArchitectureParameters<B>,
1168 M::DefinitionError: std::fmt::Display,
1169 {
1170 let state = self
1171 .state()
1172 .ok_or(ArchitecturePartitionError::MissingArchitectureState)?;
1173 state.prompt_cache_identity::<B, M>(architecture, topology)
1174 }
1175
1176 pub fn resolve_state_partition(
1182 &self,
1183 complete: &StateLayout,
1184 plan: &ArchitectureStatePartitionPlan,
1185 ) -> Result<Option<PartitionState>, ArchitectureStatePartitionError> {
1186 if plan.rules().is_empty() {
1187 return Err(ArchitectureStatePartitionError::EmptyPlan);
1188 }
1189
1190 let mut rules = plan.rules().iter().collect::<Vec<_>>();
1191 rules.sort_by_key(|rule| rule.layers().start);
1192 let mut frontier = 0usize;
1193 for rule in &rules {
1194 let layers = rule.layers();
1195 if layers.is_empty() {
1196 return Err(ArchitectureStatePartitionError::EmptyRange {
1197 start: layers.start,
1198 end: layers.end,
1199 });
1200 }
1201 if layers.end > complete.len() {
1202 return Err(ArchitectureStatePartitionError::RangeOutOfBounds {
1203 start: layers.start,
1204 end: layers.end,
1205 layers: complete.len(),
1206 });
1207 }
1208 if layers.start < frontier {
1209 return Err(ArchitectureStatePartitionError::OverlappingRange {
1210 start: layers.start,
1211 frontier,
1212 });
1213 }
1214 if layers.start > frontier {
1215 return Err(ArchitectureStatePartitionError::UnassignedLayer { layer: frontier });
1216 }
1217 if let ArchitectureStatePlacement::GroupUnits { group } = rule.placement() {
1218 let units = self
1219 .unit_layout
1220 .group_range(group)
1221 .ok_or(ArchitectureStatePartitionError::UnknownGroup { group })?
1222 .len();
1223 if layers.len() != units {
1224 return Err(ArchitectureStatePartitionError::GroupLengthMismatch {
1225 group,
1226 start: layers.start,
1227 end: layers.end,
1228 units,
1229 });
1230 }
1231 }
1232 frontier = layers.end;
1233 }
1234 if frontier != complete.len() {
1235 return Err(ArchitectureStatePartitionError::UnassignedLayer { layer: frontier });
1236 }
1237
1238 let mut selected = Vec::new();
1239 for rule in plan.rules() {
1240 let layers = rule.layers();
1241 match rule.placement() {
1242 ArchitectureStatePlacement::GroupUnits { group } => {
1243 if let Some(owned) = self
1244 .groups
1245 .iter()
1246 .find(|owned| owned.group_index() == group)
1247 {
1248 let units = owned.global_units();
1249 selected.push(layers.start + units.start..layers.start + units.end);
1250 }
1251 }
1252 ArchitectureStatePlacement::OutputOwner if self.ownership.owns_output() => {
1253 selected.push(layers);
1254 }
1255 ArchitectureStatePlacement::OutputOwner => {}
1256 }
1257 }
1258 if selected.is_empty() {
1259 return Ok(None);
1260 }
1261 selected.sort_by_key(|layers| layers.start);
1262 let start = selected[0].start;
1263 let mut end = selected[0].end;
1264 for layers in selected.iter().skip(1) {
1265 if layers.start != end {
1266 return Err(ArchitectureStatePartitionError::DiscontiguousSelection {
1267 frontier: end,
1268 start: layers.start,
1269 });
1270 }
1271 end = layers.end;
1272 }
1273 let layout = complete
1274 .slice(start..end)
1275 .map_err(|error| ArchitectureStatePartitionError::InvalidLayout(error.to_string()))?;
1276 PartitionState::new(layout, start)
1277 .map(Some)
1278 .map_err(|error| ArchitectureStatePartitionError::InvalidLayout(error.to_string()))
1279 }
1280
1281 pub const fn local_geometry(&self) -> &G {
1283 &self.local_geometry
1284 }
1285
1286 pub const fn boundary_schema(&self) -> &A {
1288 &self.boundary_schema
1289 }
1290
1291 pub fn boundary_schema_mut(&mut self) -> &mut A {
1293 &mut self.boundary_schema
1294 }
1295
1296 pub fn parameter_bindings(&self) -> &[OwnedParameterGroupSpec] {
1298 &self.parameter_bindings
1299 }
1300
1301 pub fn parameter_bindings_for_owner<'a>(
1303 &'a self,
1304 owner: &'a ParameterGroupOwner,
1305 ) -> impl Iterator<Item = &'a ParameterGroupSpec> + 'a {
1306 self.parameter_bindings
1307 .iter()
1308 .filter(move |binding| binding.owner() == owner)
1309 .map(OwnedParameterGroupSpec::group)
1310 }
1311
1312 pub fn validate_architecture<B, S, M>(
1319 &self,
1320 architecture: &M,
1321 ) -> Result<(), ArchitecturePartitionError>
1322 where
1323 B: eredu_nn::NeuralBackend,
1324 S: crate::RuntimeState<B>,
1325 M: crate::LayeredArchitecture<B, S>,
1326 M::Error: std::fmt::Display,
1327 {
1328 let (graph, unit_layout) = canonical_architecture_layout::<B, S, M>(architecture)?;
1329 if graph != self.graph {
1330 return Err(ArchitecturePartitionError::ArchitectureGraphMismatch);
1331 }
1332 if unit_layout != self.unit_layout {
1333 return Err(ArchitecturePartitionError::ArchitectureUnitLayoutMismatch);
1334 }
1335 Ok(())
1336 }
1337}
1338
1339#[derive(Debug, Clone)]
1345pub struct LayeredPartitionDriver {
1346 group: usize,
1347 range: Range<usize>,
1348 state_layout: StateLayout,
1349 owns_input: bool,
1350 owns_output: bool,
1351}
1352
1353impl LayeredPartitionDriver {
1354 pub fn new<G, A>(
1356 partition: &ArchitecturePartition<G, A>,
1357 group_index: usize,
1358 storage_range: Range<usize>,
1359 ) -> Result<Self, LayeredPartitionError> {
1360 let group = partition
1361 .groups()
1362 .iter()
1363 .find(|group| group.group_index() == group_index)
1364 .ok_or(LayeredPartitionError::GroupNotOwned { group: group_index })?;
1365 let range = group.global_units();
1366 if storage_range != range {
1367 return Err(LayeredPartitionError::StorageRange {
1368 storage: storage_range,
1369 partition: range,
1370 });
1371 }
1372 let state = partition
1373 .state()
1374 .ok_or(LayeredPartitionError::MissingState)?;
1375 if state.global_layers().start > range.start || state.global_layers().end < range.end {
1376 return Err(LayeredPartitionError::StateRange {
1377 state: state.global_layers(),
1378 partition: range,
1379 });
1380 }
1381 Ok(Self {
1382 group: group.group_index(),
1383 range,
1384 state_layout: state.layout().clone(),
1385 owns_input: partition.ownership().owns_input(),
1386 owns_output: partition.ownership().owns_output(),
1387 })
1388 }
1389
1390 pub fn range(&self) -> Range<usize> {
1392 self.range.clone()
1393 }
1394
1395 pub const fn group_index(&self) -> usize {
1397 self.group
1398 }
1399
1400 pub const fn state_layout(&self) -> &StateLayout {
1402 &self.state_layout
1403 }
1404
1405 pub fn input<'a, T, A>(
1407 &self,
1408 input: LayeredPartitionInput<'a, T, A>,
1409 ) -> Result<LayeredPartitionInput<'a, T, A>, LayeredPartitionError> {
1410 match (&input, self.owns_input) {
1411 (LayeredPartitionInput::Tokens(_), true)
1412 | (LayeredPartitionInput::Hidden { .. }, _) => Ok(input),
1413 (LayeredPartitionInput::Tokens(_), false) => {
1414 Err(LayeredPartitionError::TokensOnNonInputOwner)
1415 }
1416 }
1417 }
1418
1419 pub fn exchange_boundary<B>(
1425 &self,
1426 value: B::Tensor,
1427 group: &B::Group,
1428 executor: &B::Executor,
1429 ) -> Result<B::Tensor, B::CollectiveError>
1430 where
1431 B: crate::CollectiveBackend,
1432 {
1433 B::all_to_all(value, group, executor)
1434 }
1435
1436 #[allow(clippy::too_many_arguments)]
1438 pub fn begin<'a, B, S, M>(
1439 &self,
1440 architecture: &mut M,
1441 input: LayeredPartitionInput<
1442 'a,
1443 B::Tensor,
1444 <M::Boundary as ArchitectureBoundary>::Boundary<B::Tensor>,
1445 >,
1446 mask: Option<&B::Tensor>,
1447 state: &mut S,
1448 parallel: Option<&B::ParallelContext>,
1449 context: &<B::Tensor as eredu_nn::Tensor>::Context,
1450 ) -> Result<LayeredForwardState<B::Tensor, M::ForwardContext>, M::Error>
1451 where
1452 B: eredu_nn::NeuralBackend,
1453 S: RuntimeState<B>,
1454 M: PartitionedLayeredArchitecture<B, S>,
1455 {
1456 let mut forward = match parallel {
1457 Some(parallel) => architecture.begin_partition_parallel(
1458 input,
1459 mask,
1460 state,
1461 &self.state_layout,
1462 self.range.start,
1463 parallel,
1464 context,
1465 ),
1466 None => architecture.begin_partition(
1467 input,
1468 mask,
1469 state,
1470 &self.state_layout,
1471 self.range.start,
1472 context,
1473 ),
1474 }?;
1475 forward.hidden = architecture.enter_partition_group(
1476 self.group,
1477 &forward.hidden,
1478 state,
1479 &mut forward.context,
1480 parallel,
1481 context,
1482 )?;
1483 Ok(forward)
1484 }
1485
1486 #[allow(
1488 clippy::too_many_arguments,
1489 clippy::type_complexity,
1490 reason = "the signature exposes the backend and architecture boundary types explicitly"
1491 )]
1492 pub fn finish<B, S, M>(
1493 &self,
1494 architecture: &mut M,
1495 hidden: &B::Tensor,
1496 state: &mut S,
1497 forward: &mut M::ForwardContext,
1498 parallel: Option<&B::ParallelContext>,
1499 context: &<B::Tensor as eredu_nn::Tensor>::Context,
1500 ) -> Result<
1501 LayeredPartitionOutput<
1502 B::Tensor,
1503 <M::Boundary as ArchitectureBoundary>::Boundary<B::Tensor>,
1504 >,
1505 M::Error,
1506 >
1507 where
1508 B: eredu_nn::NeuralBackend,
1509 S: RuntimeState<B>,
1510 M: PartitionedLayeredArchitecture<B, S>,
1511 {
1512 let hidden = architecture
1513 .leave_partition_group(self.group, hidden, state, forward, parallel, context)?;
1514 architecture.finish_partition(&hidden, state, forward, self.owns_output, parallel, context)
1515 }
1516}
1517
1518#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1520#[non_exhaustive]
1521pub enum LayeredPartitionError {
1522 #[error("layered partition does not own execution group {group}")]
1524 GroupNotOwned {
1525 group: usize,
1527 },
1528 #[error("partition storage range {storage:?} disagrees with canonical range {partition:?}")]
1530 StorageRange {
1531 storage: Range<usize>,
1533 partition: Range<usize>,
1535 },
1536 #[error("layered partition has no runtime state")]
1538 MissingState,
1539 #[error("partition state range {state:?} disagrees with canonical range {partition:?}")]
1541 StateRange {
1542 state: Range<usize>,
1544 partition: Range<usize>,
1546 },
1547 #[error("non-input partition received token ids")]
1549 TokensOnNonInputOwner,
1550}
1551
1552fn canonical_architecture_layout<B, S, M>(
1553 architecture: &M,
1554) -> Result<(ExecutionGraph, ExecutionUnitLayout), ArchitecturePartitionError>
1555where
1556 B: eredu_nn::NeuralBackend,
1557 S: crate::RuntimeState<B>,
1558 M: crate::LayeredArchitecture<B, S>,
1559 M::Error: std::fmt::Display,
1560{
1561 let graph = architecture
1562 .execution_graph()
1563 .map_err(|error| ArchitecturePartitionError::ArchitectureTopology(error.to_string()))?;
1564 let primary = architecture.primary_execution_group();
1565 let primary_index = graph.group_index(primary).ok_or_else(|| {
1566 ArchitecturePartitionError::ArchitectureTopology(format!(
1567 "primary execution group {primary:?} is not present in the canonical graph"
1568 ))
1569 })?;
1570 let primary_transport = architecture.group_transport(primary_index);
1571 if primary_transport.kind != crate::ArchitectureGroupKind::Decoder
1572 || primary_transport.placement != crate::ArchitectureGroupPlacement::Pipeline
1573 {
1574 return Err(ArchitecturePartitionError::ArchitectureTopology(format!(
1575 "primary execution group {primary:?} must be a pipeline decoder"
1576 )));
1577 }
1578 let mut declared_groups = BTreeSet::from([primary.to_owned()]);
1579 for prediction in architecture.prediction_execution_groups() {
1580 let prediction_index = graph.group_index(&prediction).ok_or_else(|| {
1581 ArchitecturePartitionError::ArchitectureTopology(format!(
1582 "prediction execution group {prediction:?} is not present in the canonical graph"
1583 ))
1584 })?;
1585 let prediction_transport = architecture.group_transport(prediction_index);
1586 if prediction_transport.kind != crate::ArchitectureGroupKind::Prediction
1587 || prediction_transport.placement != crate::ArchitectureGroupPlacement::OutputOwner
1588 {
1589 return Err(ArchitecturePartitionError::ArchitectureTopology(format!(
1590 "prediction execution group {prediction:?} must be an output-owner prediction"
1591 )));
1592 }
1593 if !declared_groups.insert(prediction.clone()) {
1594 return Err(ArchitecturePartitionError::ArchitectureTopology(format!(
1595 "execution group {prediction:?} is declared as a primary or prediction group more than once"
1596 )));
1597 }
1598 }
1599 let mut counts = Vec::with_capacity(graph.groups().len());
1600 let mut paths = BTreeSet::new();
1601 for group in 0..graph.groups().len() {
1602 let count = architecture
1603 .group_unit_count(group)
1604 .map_err(|error| ArchitecturePartitionError::ArchitectureTopology(error.to_string()))?;
1605 counts.push(count);
1606 for index in 0..count {
1607 let path = architecture.unit_path(group, index).map_err(|error| {
1608 ArchitecturePartitionError::ArchitectureTopology(error.to_string())
1609 })?;
1610 if path.trim().is_empty() {
1611 return Err(ArchitecturePartitionError::EmptyArchitectureUnitPath { group, index });
1612 }
1613 if !paths.insert(path.clone()) {
1614 return Err(ArchitecturePartitionError::DuplicateArchitectureUnitPath(
1615 path,
1616 ));
1617 }
1618 }
1619 }
1620 let unit_layout = ExecutionUnitLayout::new(&graph, counts)
1621 .map_err(|error| ArchitecturePartitionError::ArchitectureTopology(error.to_string()))?;
1622 Ok((graph, unit_layout))
1623}
1624
1625fn validate_canonical_layout(
1626 graph: &ExecutionGraph,
1627 layout: &ExecutionUnitLayout,
1628) -> Result<(), ArchitecturePartitionError> {
1629 if graph.groups().len() != layout.group_count() {
1630 return Err(ArchitecturePartitionError::LayoutGroupCountMismatch {
1631 graph: graph.groups().len(),
1632 layout: layout.group_count(),
1633 });
1634 }
1635 for (index, group) in graph.groups().iter().enumerate() {
1636 let layout_group = layout
1637 .group_id(index)
1638 .expect("matching group counts provide every layout identity");
1639 if layout_group.as_str() != group.id() {
1640 return Err(ArchitecturePartitionError::LayoutGroupMismatch {
1641 index,
1642 graph: group.id().to_owned(),
1643 layout: layout_group.as_str().to_owned(),
1644 });
1645 }
1646 }
1647 Ok(())
1648}
1649
1650#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1652#[non_exhaustive]
1653pub enum ArchitecturePartitionError {
1654 #[error("invalid architecture partition boundary: {0}")]
1656 InvalidBoundary(#[from] ArchitectureBoundaryError),
1657 #[error("neutral architecture topology is invalid: {0}")]
1660 ArchitectureTopology(String),
1661 #[error("neutral architecture state is invalid: {0}")]
1663 ArchitectureState(String),
1664 #[error("architecture partition owns no mutable state")]
1666 MissingArchitectureState,
1667 #[error("architecture prompt-cache identity is invalid: {0}")]
1669 PromptCacheIdentity(String),
1670 #[error("neutral architecture unit {group}:{index} has an empty path")]
1672 EmptyArchitectureUnitPath {
1673 group: usize,
1675 index: usize,
1677 },
1678 #[error("neutral architecture repeats unit path {0:?}")]
1680 DuplicateArchitectureUnitPath(String),
1681 #[error("architecture partition dependency graph differs from the neutral architecture")]
1683 ArchitectureGraphMismatch,
1684 #[error("architecture partition unit layout differs from the neutral architecture")]
1686 ArchitectureUnitLayoutMismatch,
1687 #[error("execution graph contains {graph} groups but its unit layout contains {layout}")]
1689 LayoutGroupCountMismatch {
1690 graph: usize,
1692 layout: usize,
1694 },
1695 #[error("execution group {index} is {graph:?} in the graph but {layout:?} in the unit layout")]
1697 LayoutGroupMismatch {
1698 index: usize,
1700 graph: String,
1702 layout: String,
1704 },
1705 #[error("architecture partition names unknown execution group {0:?}")]
1707 UnknownGroup(String),
1708 #[error("architecture partition repeats execution group {0:?}")]
1710 DuplicateGroup(String),
1711 #[error("architecture partition declares an empty unit range for group {group:?}")]
1713 EmptyGroupRange {
1714 group: String,
1716 },
1717 #[error(
1719 "architecture partition range {start}..{end} for group {group:?} exceeds {available} units"
1720 )]
1721 GroupRangeOutOfBounds {
1722 group: String,
1724 start: usize,
1726 end: usize,
1728 available: usize,
1730 },
1731 #[error("architecture partition static role must not be empty")]
1733 EmptyStaticRole,
1734 #[error("architecture partition repeats static role {0:?}")]
1736 DuplicateStaticRole(String),
1737 #[error("state layer offset {offset} plus {layers} local layers overflowed usize")]
1739 StateOffsetOverflow {
1740 offset: usize,
1742 layers: usize,
1744 },
1745 #[error("architecture partition repeats parameter target {0:?}")]
1747 DuplicateParameterTarget(String),
1748 #[error("architecture partition includes non-local parameter owner {0:?}")]
1750 NonLocalParameterOwner(ParameterGroupOwner),
1751}
1752
1753#[cfg(test)]
1754mod tests {
1755 use super::*;
1756 use crate::{MemberSharding, ParameterMemberSpec, ParameterRole};
1757 use eredu_core::{cache::LayerCachePolicy, LayerSchedule};
1758
1759 #[derive(Debug, Clone, Eq, PartialEq)]
1760 struct Geometry(&'static str);
1761
1762 #[derive(Debug, Clone, Eq, PartialEq)]
1763 struct Boundary {
1764 route: usize,
1765 }
1766
1767 #[derive(Debug, Clone, Eq, PartialEq)]
1768 struct PairBoundary<T> {
1769 tokens: T,
1770 embedded: T,
1771 }
1772
1773 #[derive(Debug, Clone, Copy)]
1774 struct PairBoundarySchema;
1775
1776 impl ArchitectureBoundary for PairBoundarySchema {
1777 type Boundary<T> = PairBoundary<T>;
1778
1779 const IDENTITY: &'static str = "fixture.target";
1780
1781 fn primary_tensor_spec(&self) -> BoundaryTensorSpec {
1782 BoundaryTensorSpec::primary_activation(8)
1783 }
1784
1785 fn auxiliary_tensor_specs(&self) -> Vec<BoundaryTensorSpec> {
1786 vec![
1787 BoundaryTensorSpec::new(
1788 "tokens",
1789 [
1790 BoundaryTensorDimension::Batch,
1791 BoundaryTensorDimension::Sequence,
1792 ],
1793 BoundaryTensorDtype::Uint32,
1794 ),
1795 BoundaryTensorSpec::new(
1796 "embedded",
1797 [
1798 BoundaryTensorDimension::Batch,
1799 BoundaryTensorDimension::Sequence,
1800 BoundaryTensorDimension::Fixed(16),
1801 ],
1802 BoundaryTensorDtype::Activation,
1803 ),
1804 ]
1805 }
1806
1807 fn encode<T>(
1808 &self,
1809 boundary: Self::Boundary<T>,
1810 ) -> Result<Vec<T>, ArchitectureBoundaryError> {
1811 Ok(vec![boundary.tokens, boundary.embedded])
1812 }
1813
1814 fn decode<T>(
1815 &self,
1816 mut tensors: Vec<T>,
1817 ) -> Result<Self::Boundary<T>, ArchitectureBoundaryError> {
1818 validate_boundary_tensor_count(self, &tensors)?;
1819 let embedded = tensors.pop().expect("validated embedded tensor");
1820 let tokens = tensors.pop().expect("validated token tensor");
1821 Ok(PairBoundary { tokens, embedded })
1822 }
1823 }
1824
1825 fn graph() -> ExecutionGraph {
1826 ExecutionGraph::chain(["primary", "prediction"]).unwrap()
1827 }
1828
1829 fn layout(graph: &ExecutionGraph) -> ExecutionUnitLayout {
1830 ExecutionUnitLayout::new(graph, [4, 3]).unwrap()
1831 }
1832
1833 fn state_layout(layers: usize) -> StateLayout {
1834 StateLayout::new(
1835 LayerSchedule::new(layers, vec![LayerCachePolicy::NoState; layers]).unwrap(),
1836 )
1837 .unwrap()
1838 }
1839
1840 fn parameter(logical: &str, target: &str) -> ParameterGroupSpec {
1841 ParameterGroupSpec::new(
1842 logical,
1843 ParameterRole::Replicated,
1844 [ParameterMemberSpec::new(
1845 target,
1846 vec![2, 2],
1847 MemberSharding::Replicated,
1848 )],
1849 )
1850 .unwrap()
1851 }
1852
1853 fn valid_partition() -> ArchitecturePartition<Geometry, Boundary> {
1854 let graph = graph();
1855 let layout = layout(&graph);
1856 ArchitecturePartition::new(
1857 graph,
1858 layout,
1859 [("prediction", 0..2), ("primary", 1..4)],
1860 PartitionOwnership::new(true, false, ["embedding", "normalization"]).unwrap(),
1861 Some(PartitionState::new(state_layout(2), 7).unwrap()),
1862 Geometry("local"),
1863 Boundary { route: 3 },
1864 [
1865 OwnedParameterGroupSpec::new(
1866 ParameterGroupOwner::static_role("embedding"),
1867 parameter("model.embed_tokens", "model.embed_tokens.weight"),
1868 ),
1869 OwnedParameterGroupSpec::new(
1870 ParameterGroupOwner::execution_unit(
1871 ExecutionGroupId::new("primary").unwrap(),
1872 1,
1873 ),
1874 parameter("model.layers.1", "model.layers.1.weight"),
1875 ),
1876 ],
1877 )
1878 .unwrap()
1879 }
1880
1881 fn state_plan_partition(
1882 primary: Range<usize>,
1883 ownership: PartitionOwnership,
1884 ) -> ArchitecturePartition<(), ()> {
1885 let graph = graph();
1886 ArchitecturePartition::new(
1887 graph.clone(),
1888 layout(&graph),
1889 [("primary", primary)],
1890 ownership,
1891 None,
1892 (),
1893 (),
1894 [],
1895 )
1896 .unwrap()
1897 }
1898
1899 #[test]
1900 fn architecture_state_plan_attaches_declared_tail_to_output_owner() {
1901 let complete = state_layout(6);
1902 let plan = ArchitectureStatePartitionPlan::new([
1903 crate::ArchitectureStatePartitionRule::group_units(0, 0..4),
1904 crate::ArchitectureStatePartitionRule::output_owner(4..6),
1905 ]);
1906 let interior = state_plan_partition(
1907 1..3,
1908 PartitionOwnership::new(false, false, std::iter::empty::<&str>()).unwrap(),
1909 );
1910 let output = state_plan_partition(
1911 3..4,
1912 PartitionOwnership::new(false, true, std::iter::empty::<&str>()).unwrap(),
1913 );
1914
1915 assert_eq!(
1916 interior
1917 .resolve_state_partition(&complete, &plan)
1918 .unwrap()
1919 .unwrap()
1920 .global_layers(),
1921 1..3
1922 );
1923 assert_eq!(
1924 output
1925 .resolve_state_partition(&complete, &plan)
1926 .unwrap()
1927 .unwrap()
1928 .global_layers(),
1929 3..6
1930 );
1931 }
1932
1933 #[test]
1934 fn architecture_state_plan_rejects_noncontiguous_local_state() {
1935 let complete = state_layout(6);
1936 let plan = ArchitectureStatePartitionPlan::new([
1937 crate::ArchitectureStatePartitionRule::output_owner(0..2),
1938 crate::ArchitectureStatePartitionRule::group_units(0, 2..6),
1939 ]);
1940 let output = state_plan_partition(
1941 3..4,
1942 PartitionOwnership::new(false, true, std::iter::empty::<&str>()).unwrap(),
1943 );
1944
1945 assert_eq!(
1946 output.resolve_state_partition(&complete, &plan),
1947 Err(ArchitectureStatePartitionError::DiscontiguousSelection {
1948 frontier: 2,
1949 start: 5,
1950 })
1951 );
1952 }
1953
1954 fn parameter_description(
1955 expected: Vec<ParameterGroupSpec>,
1956 groups: Vec<OwnedParameterGroupSpec>,
1957 ) -> Result<ArchitectureParameterDescription, ArchitectureParameterError> {
1958 let graph = graph();
1959 ArchitectureParameterDescription::new(&graph, &layout(&graph), expected, groups)
1960 }
1961
1962 #[test]
1963 fn parameter_description_selects_static_roles_and_canonical_units() {
1964 let embedding = parameter("embedding", "model.embed_tokens.weight");
1965 let layer = parameter("layer", "model.layers.1.weight");
1966 let description = parameter_description(
1967 vec![embedding.clone(), layer.clone()],
1968 vec![
1969 OwnedParameterGroupSpec::new(
1970 ParameterGroupOwner::static_role("embedding"),
1971 embedding,
1972 ),
1973 OwnedParameterGroupSpec::new(
1974 ParameterGroupOwner::execution_unit(
1975 ExecutionGroupId::new("primary").unwrap(),
1976 1,
1977 ),
1978 layer,
1979 ),
1980 ],
1981 )
1982 .unwrap();
1983 let partition = valid_partition();
1984 assert_eq!(description.graph(), partition.graph());
1985 assert_eq!(description.unit_layout(), partition.unit_layout());
1986 let selected = description.select_owned(&partition);
1987 assert_eq!(selected.len(), 2);
1988 assert_eq!(selected[0].logical_name(), "embedding");
1989 assert_eq!(selected[1].logical_name(), "layer");
1990 assert_eq!(
1991 selected[0].owner(),
1992 &ParameterGroupOwner::static_role("embedding")
1993 );
1994 assert_eq!(
1995 selected[1].owner(),
1996 &ParameterGroupOwner::execution_unit(ExecutionGroupId::new("primary").unwrap(), 1,)
1997 );
1998 }
1999
2000 #[test]
2001 fn parameter_description_selects_every_owned_target_for_a_role() {
2002 let expert = ParameterGroupSpec::new(
2003 "model.layers.1.expert_intermediate",
2004 ParameterRole::ExpertIntermediate,
2005 [
2006 ParameterMemberSpec::new(
2007 "model.layers.1.moe.packed.weight",
2008 vec![4, 2],
2009 MemberSharding::Replicated,
2010 ),
2011 ParameterMemberSpec::new(
2012 "model.layers.1.moe.packed.scales",
2013 vec![4, 1],
2014 MemberSharding::Replicated,
2015 ),
2016 ParameterMemberSpec::new(
2017 "model.layers.1.moe.alias.biases",
2018 vec![4, 1],
2019 MemberSharding::Replicated,
2020 ),
2021 ],
2022 )
2023 .unwrap();
2024 let replicated = parameter("router", "model.layers.1.moe.router.weight");
2025 let owner =
2026 ParameterGroupOwner::execution_unit(ExecutionGroupId::new("primary").unwrap(), 1);
2027 let description = parameter_description(
2028 vec![expert.clone(), replicated.clone()],
2029 vec![
2030 OwnedParameterGroupSpec::new(owner.clone(), expert),
2031 OwnedParameterGroupSpec::new(owner, replicated),
2032 ],
2033 )
2034 .unwrap();
2035
2036 assert_eq!(
2037 description.targets_for_role(ParameterRole::ExpertIntermediate),
2038 BTreeSet::from([
2039 "model.layers.1.moe.alias.biases".to_owned(),
2040 "model.layers.1.moe.packed.scales".to_owned(),
2041 "model.layers.1.moe.packed.weight".to_owned(),
2042 ])
2043 );
2044 }
2045
2046 #[test]
2047 fn parameter_description_selects_shared_static_owner_by_any_consumer() {
2048 let embedding = parameter("embedding", "model.embed_tokens.weight");
2049 let description = parameter_description(
2050 vec![embedding.clone()],
2051 vec![OwnedParameterGroupSpec::new(
2052 ParameterGroupOwner::static_any_of(["output", "embedding"]),
2053 embedding,
2054 )],
2055 )
2056 .unwrap();
2057 assert_eq!(description.select_owned(&valid_partition()).len(), 1);
2058
2059 let duplicate = parameter("embedding", "model.embed_tokens.weight");
2060 assert_eq!(
2061 parameter_description(
2062 vec![duplicate.clone()],
2063 vec![OwnedParameterGroupSpec::new(
2064 ParameterGroupOwner::static_any_of(["embedding", "embedding"]),
2065 duplicate,
2066 )],
2067 )
2068 .unwrap_err(),
2069 ArchitectureParameterError::DuplicateStaticRole,
2070 );
2071 }
2072
2073 #[test]
2074 fn partition_rejects_parameter_owner_outside_local_unit_ranges() {
2075 let graph = graph();
2076 let error = ArchitecturePartition::new(
2077 graph.clone(),
2078 layout(&graph),
2079 [("primary", 1..4)],
2080 PartitionOwnership::new(false, false, ["embedding"]).unwrap(),
2081 None,
2082 (),
2083 (),
2084 [OwnedParameterGroupSpec::new(
2085 ParameterGroupOwner::execution_unit(
2086 ExecutionGroupId::new("prediction").unwrap(),
2087 0,
2088 ),
2089 parameter("prediction", "prediction.weight"),
2090 )],
2091 )
2092 .unwrap_err();
2093 assert!(matches!(
2094 error,
2095 ArchitecturePartitionError::NonLocalParameterOwner(
2096 ParameterGroupOwner::ExecutionUnit { .. }
2097 )
2098 ));
2099 }
2100
2101 #[test]
2102 fn parameter_description_rejects_missing_duplicate_and_out_of_range_ownership() {
2103 let embedding = parameter("embedding", "model.embed_tokens.weight");
2104 let layer = parameter("layer", "model.layers.1.weight");
2105 assert_eq!(
2106 parameter_description(
2107 vec![embedding.clone(), layer.clone()],
2108 vec![OwnedParameterGroupSpec::new(
2109 ParameterGroupOwner::static_role("embedding"),
2110 embedding.clone(),
2111 )],
2112 )
2113 .unwrap_err(),
2114 ArchitectureParameterError::MissingOwnership("model.layers.1.weight".into())
2115 );
2116 assert!(matches!(
2117 parameter_description(
2118 vec![embedding.clone()],
2119 vec![
2120 OwnedParameterGroupSpec::new(
2121 ParameterGroupOwner::static_role("embedding"),
2122 embedding.clone(),
2123 ),
2124 OwnedParameterGroupSpec::new(
2125 ParameterGroupOwner::static_role("output"),
2126 embedding.clone(),
2127 ),
2128 ],
2129 )
2130 .unwrap_err(),
2131 ArchitectureParameterError::DuplicateOwnership { .. }
2132 ));
2133 assert_eq!(
2134 parameter_description(
2135 vec![layer.clone()],
2136 vec![OwnedParameterGroupSpec::new(
2137 ParameterGroupOwner::execution_unit(
2138 ExecutionGroupId::new("prediction").unwrap(),
2139 3,
2140 ),
2141 layer,
2142 )],
2143 )
2144 .unwrap_err(),
2145 ArchitectureParameterError::UnitOutOfRange {
2146 group: "prediction".into(),
2147 global_unit: 3,
2148 available: 3,
2149 }
2150 );
2151 }
2152
2153 #[test]
2154 fn retains_canonical_topology_ownership_and_typed_family_values() {
2155 let mut partition = valid_partition();
2156 assert_eq!(partition.graph().groups().len(), 2);
2157 assert_eq!(partition.unit_layout().len(), 7);
2158 assert_eq!(partition.groups()[0].group().as_str(), "primary");
2159 assert_eq!(partition.groups()[0].group_index(), 0);
2160 assert_eq!(partition.groups()[0].global_units(), 1..4);
2161 assert!(partition.owns_unit("primary", 3));
2162 assert!(!partition.owns_unit("primary", 0));
2163 assert!(partition.ownership().owns_input());
2164 assert!(!partition.ownership().owns_output());
2165 assert!(partition.ownership().owns_static_role("embedding"));
2166 assert_eq!(
2167 partition
2168 .units()
2169 .map(|unit| (unit.group(), unit.index()))
2170 .collect::<Vec<_>>(),
2171 [(0, 1), (0, 2), (0, 3), (1, 0), (1, 1)]
2172 );
2173 assert_eq!(partition.state().unwrap().global_layers(), 7..9);
2174 assert_eq!(partition.local_geometry(), &Geometry("local"));
2175 partition.boundary_schema_mut().route = 5;
2176 assert_eq!(partition.boundary_schema().route, 5);
2177 assert_eq!(partition.parameter_bindings().len(), 2);
2178 }
2179
2180 #[test]
2181 fn typed_boundary_owns_roles_order_and_atomic_cardinality_validation() {
2182 let boundary = PairBoundary {
2183 tokens: 3,
2184 embedded: 7,
2185 };
2186 let schema = PairBoundarySchema;
2187 let tensors = schema.encode(boundary).unwrap();
2188 assert_eq!(tensors, [3, 7]);
2189 assert_eq!(
2190 schema.decode(tensors).unwrap(),
2191 PairBoundary {
2192 tokens: 3,
2193 embedded: 7
2194 }
2195 );
2196 let resolved = schema.wire_schema().unwrap().resolve(2, 3).unwrap();
2197 assert_eq!(resolved.primary().shape(), [2, 3, 8]);
2198 assert_eq!(resolved.primary().dtype(), BoundaryTensorDtype::Activation);
2199 assert_eq!(resolved.auxiliary()[0].shape(), [2, 3]);
2200 assert_eq!(resolved.auxiliary()[0].dtype(), BoundaryTensorDtype::Uint32);
2201 assert_eq!(resolved.auxiliary()[1].shape(), [2, 3, 16]);
2202 assert_eq!(
2203 resolved.auxiliary()[1].dtype(),
2204 BoundaryTensorDtype::Activation
2205 );
2206 assert_eq!(
2207 schema.decode(vec![3]).unwrap_err(),
2208 ArchitectureBoundaryError::TensorCount {
2209 boundary: "fixture.target",
2210 expected: 2,
2211 actual: 1,
2212 }
2213 );
2214 }
2215
2216 #[test]
2217 fn boundary_schema_rejects_role_and_geometry_drift_before_transport() {
2218 let invalid_primary = BoundaryWireSchema::new(
2219 "fixture.invalid",
2220 BoundaryTensorSpec::new(
2221 "hidden",
2222 [BoundaryTensorDimension::Fixed(8)],
2223 BoundaryTensorDtype::Uint32,
2224 ),
2225 [],
2226 )
2227 .unwrap_err();
2228 assert_eq!(
2229 invalid_primary,
2230 ArchitectureBoundaryError::InvalidPrimaryDtype {
2231 boundary: "fixture.invalid",
2232 }
2233 );
2234
2235 let duplicate = BoundaryWireSchema::new(
2236 "fixture.invalid",
2237 BoundaryTensorSpec::primary_activation(8),
2238 [
2239 BoundaryTensorSpec::new(
2240 "state",
2241 [BoundaryTensorDimension::Fixed(1)],
2242 BoundaryTensorDtype::Activation,
2243 ),
2244 BoundaryTensorSpec::new(
2245 "state",
2246 [BoundaryTensorDimension::Fixed(2)],
2247 BoundaryTensorDtype::Activation,
2248 ),
2249 ],
2250 )
2251 .unwrap_err();
2252 assert_eq!(
2253 duplicate,
2254 ArchitectureBoundaryError::DuplicateTensorRole {
2255 boundary: "fixture.invalid",
2256 role: "state".into(),
2257 }
2258 );
2259
2260 let invalid = BoundaryWireSchema::new(
2261 "fixture.invalid",
2262 BoundaryTensorSpec::primary_activation(8),
2263 [BoundaryTensorSpec::new(
2264 "state",
2265 [BoundaryTensorDimension::Fixed(0)],
2266 BoundaryTensorDtype::Activation,
2267 )],
2268 )
2269 .unwrap_err();
2270 assert_eq!(
2271 invalid,
2272 ArchitectureBoundaryError::InvalidTensorDimension {
2273 boundary: "fixture.invalid",
2274 role: "state".into(),
2275 }
2276 );
2277 }
2278
2279 #[test]
2280 fn rejects_noncanonical_unknown_and_duplicate_groups() {
2281 let graph = graph();
2282 let mismatched_graph = ExecutionGraph::chain(["primary", "other"]).unwrap();
2283 let error = ArchitecturePartition::new(
2284 graph.clone(),
2285 layout(&mismatched_graph),
2286 [("primary", 0..1)],
2287 PartitionOwnership::new(false, false, std::iter::empty::<String>()).unwrap(),
2288 None,
2289 (),
2290 (),
2291 std::iter::empty(),
2292 )
2293 .unwrap_err();
2294 assert!(matches!(
2295 error,
2296 ArchitecturePartitionError::LayoutGroupMismatch { .. }
2297 ));
2298
2299 let error = ArchitecturePartition::new(
2300 graph.clone(),
2301 layout(&graph),
2302 [("missing", 0..1)],
2303 PartitionOwnership::new(false, false, std::iter::empty::<String>()).unwrap(),
2304 None,
2305 (),
2306 (),
2307 std::iter::empty(),
2308 )
2309 .unwrap_err();
2310 assert_eq!(
2311 error,
2312 ArchitecturePartitionError::UnknownGroup("missing".into())
2313 );
2314
2315 let error = ArchitecturePartition::new(
2316 graph.clone(),
2317 layout(&graph),
2318 [("primary", 0..1), ("primary", 1..2)],
2319 PartitionOwnership::new(false, false, std::iter::empty::<String>()).unwrap(),
2320 None,
2321 (),
2322 (),
2323 std::iter::empty(),
2324 )
2325 .unwrap_err();
2326 assert_eq!(
2327 error,
2328 ArchitecturePartitionError::DuplicateGroup("primary".into())
2329 );
2330 }
2331
2332 #[test]
2333 fn rejects_empty_and_out_of_bounds_group_ranges() {
2334 let graph = graph();
2335 let error = ArchitecturePartition::new(
2336 graph.clone(),
2337 layout(&graph),
2338 [("primary", 2..2)],
2339 PartitionOwnership::new(false, false, std::iter::empty::<String>()).unwrap(),
2340 None,
2341 (),
2342 (),
2343 std::iter::empty(),
2344 )
2345 .unwrap_err();
2346 assert!(matches!(
2347 error,
2348 ArchitecturePartitionError::EmptyGroupRange { .. }
2349 ));
2350
2351 let error = ArchitecturePartition::new(
2352 graph.clone(),
2353 layout(&graph),
2354 [("prediction", 1..4)],
2355 PartitionOwnership::new(false, false, std::iter::empty::<String>()).unwrap(),
2356 None,
2357 (),
2358 (),
2359 std::iter::empty(),
2360 )
2361 .unwrap_err();
2362 assert!(matches!(
2363 error,
2364 ArchitecturePartitionError::GroupRangeOutOfBounds { .. }
2365 ));
2366 }
2367
2368 #[test]
2369 fn rejects_state_offset_overflow() {
2370 assert_eq!(
2371 PartitionState::new(state_layout(2), usize::MAX).unwrap_err(),
2372 ArchitecturePartitionError::StateOffsetOverflow {
2373 offset: usize::MAX,
2374 layers: 2,
2375 }
2376 );
2377 }
2378
2379 #[test]
2380 fn rejects_empty_static_roles_and_duplicate_parameter_targets() {
2381 assert_eq!(
2382 PartitionOwnership::new(false, false, [" "]).unwrap_err(),
2383 ArchitecturePartitionError::EmptyStaticRole
2384 );
2385
2386 let graph = graph();
2387 let error = ArchitecturePartition::new(
2388 graph.clone(),
2389 layout(&graph),
2390 [("primary", 0..1)],
2391 PartitionOwnership::new(false, false, ["embedding", "normalization"]).unwrap(),
2392 None,
2393 (),
2394 (),
2395 [
2396 OwnedParameterGroupSpec::new(
2397 ParameterGroupOwner::static_role("embedding"),
2398 parameter("first", "shared.weight"),
2399 ),
2400 OwnedParameterGroupSpec::new(
2401 ParameterGroupOwner::static_role("normalization"),
2402 parameter("second", "shared.weight"),
2403 ),
2404 ],
2405 )
2406 .unwrap_err();
2407 assert_eq!(
2408 error,
2409 ArchitecturePartitionError::DuplicateParameterTarget("shared.weight".into())
2410 );
2411 }
2412
2413 fn layered_partition(
2414 storage_state: Range<usize>,
2415 owns_input: bool,
2416 ) -> ArchitecturePartition<(), ()> {
2417 let graph = ExecutionGraph::chain(["decoder"]).unwrap();
2418 let layout = ExecutionUnitLayout::new(&graph, [4]).unwrap();
2419 ArchitecturePartition::new(
2420 graph,
2421 layout,
2422 [("decoder", 1..3)],
2423 PartitionOwnership::new(owns_input, false, std::iter::empty::<String>()).unwrap(),
2424 Some(
2425 PartitionState::new(state_layout(storage_state.len()), storage_state.start)
2426 .unwrap(),
2427 ),
2428 (),
2429 (),
2430 std::iter::empty(),
2431 )
2432 .unwrap()
2433 }
2434
2435 #[test]
2436 fn layered_driver_rejects_storage_and_state_range_drift() {
2437 let partition = layered_partition(1..3, true);
2438 assert!(LayeredPartitionDriver::new(&partition, 0, 1..3).is_ok());
2439 assert_eq!(
2440 LayeredPartitionDriver::new(&partition, 0, 0..2).unwrap_err(),
2441 LayeredPartitionError::StorageRange {
2442 storage: 0..2,
2443 partition: 1..3,
2444 }
2445 );
2446
2447 let partition = layered_partition(0..2, true);
2448 assert_eq!(
2449 LayeredPartitionDriver::new(&partition, 0, 1..3).unwrap_err(),
2450 LayeredPartitionError::StateRange {
2451 state: 0..2,
2452 partition: 1..3,
2453 }
2454 );
2455 }
2456
2457 #[test]
2458 fn layered_driver_restricts_tokens_but_accepts_architecture_prepared_hidden() {
2459 let input_owner =
2460 LayeredPartitionDriver::new(&layered_partition(1..3, true), 0, 1..3).unwrap();
2461 assert!(matches!(
2462 input_owner.input(LayeredPartitionInput::<i32, NoAuxiliaryBoundary>::Tokens(
2463 &7
2464 )),
2465 Ok(LayeredPartitionInput::Tokens(7))
2466 ));
2467 assert!(matches!(
2468 input_owner.input(LayeredPartitionInput::Hidden {
2469 hidden: 7,
2470 auxiliary: NoAuxiliaryBoundary,
2471 }),
2472 Ok(LayeredPartitionInput::Hidden {
2473 hidden: 7,
2474 auxiliary: NoAuxiliaryBoundary,
2475 })
2476 ));
2477
2478 let hidden_owner =
2479 LayeredPartitionDriver::new(&layered_partition(1..3, false), 0, 1..3).unwrap();
2480 assert_eq!(
2481 hidden_owner
2482 .input(LayeredPartitionInput::<i32, NoAuxiliaryBoundary>::Tokens(
2483 &7
2484 ))
2485 .unwrap_err(),
2486 LayeredPartitionError::TokensOnNonInputOwner
2487 );
2488 assert!(matches!(
2489 hidden_owner.input(LayeredPartitionInput::Hidden {
2490 hidden: 7,
2491 auxiliary: NoAuxiliaryBoundary,
2492 }),
2493 Ok(LayeredPartitionInput::Hidden {
2494 hidden: 7,
2495 auxiliary: NoAuxiliaryBoundary,
2496 })
2497 ));
2498 }
2499}