1use std::collections::HashSet;
7use std::hash::{Hash, Hasher};
8#[cfg(test)]
9use std::num::NonZeroU64;
10use std::sync::Arc;
11
12use super::snapshot::ExecutableEngineSnapshot;
13use super::{
14 FrozenTransferRegistry, RegistrationIdentity, ResolvedTransferEndpoint, ResolvedTransferRoute,
15 RuntimeEpoch, RuntimeId,
16};
17use crate::exec::ExecProgram;
18use crate::{EngineId, StorageClass, TransferEndpoint};
19
20#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
38pub struct EventDomainId {
39 runtime_id: RuntimeId,
40 epoch: RuntimeEpoch,
41 registration_identity: RegistrationIdentity,
42}
43
44impl EventDomainId {
45 pub(crate) const fn new(
46 runtime_id: RuntimeId,
47 epoch: RuntimeEpoch,
48 registration_identity: RegistrationIdentity,
49 ) -> Self {
50 Self {
51 runtime_id,
52 epoch,
53 registration_identity,
54 }
55 }
56
57 #[must_use]
69 pub const fn runtime_id(self) -> RuntimeId {
70 self.runtime_id
71 }
72
73 #[must_use]
85 pub const fn epoch(self) -> RuntimeEpoch {
86 self.epoch
87 }
88
89 #[must_use]
101 pub const fn registration_identity(self) -> RegistrationIdentity {
102 self.registration_identity
103 }
104
105 #[cfg(test)]
106 pub(crate) const fn runtime_created_for_test(
107 runtime_id: RuntimeId,
108 epoch: RuntimeEpoch,
109 registration_identity: RegistrationIdentity,
110 ) -> Self {
111 Self::new(runtime_id, epoch, registration_identity)
112 }
113}
114
115#[derive(Clone, Debug)]
116pub(crate) struct ExecutionLocation {
117 resolved_endpoint: ResolvedTransferEndpoint,
118 witness: Arc<ExecutableEngineSnapshot>,
119}
120
121impl ExecutionLocation {
122 pub(super) fn from_witness(
123 witness: Arc<ExecutableEngineSnapshot>,
124 storage_class: StorageClass,
125 ) -> Self {
126 Self {
127 resolved_endpoint: ResolvedTransferEndpoint::new(
128 TransferEndpoint::new(witness.engine_id().clone(), storage_class),
129 witness.provider_device_identity().clone(),
130 witness.event_domain_id(),
131 ),
132 witness,
133 }
134 }
135
136 #[cfg(test)]
137 pub(crate) fn new(
138 engine_id: EngineId,
139 provider_device_identity: super::ProviderDeviceIdentity,
140 event_domain_id: EventDomainId,
141 storage_class: StorageClass,
142 ) -> Self {
143 Self::from_witness(
144 super::snapshot::ExecutableEngineSnapshot::for_test(
145 engine_id,
146 provider_device_identity,
147 event_domain_id,
148 storage_class.clone(),
149 ),
150 storage_class,
151 )
152 }
153
154 pub(crate) fn engine_id(&self) -> &EngineId {
155 self.resolved_endpoint.logical().engine_id()
156 }
157
158 pub(crate) fn endpoint(&self) -> &TransferEndpoint {
159 self.resolved_endpoint.logical()
160 }
161
162 pub(crate) fn event_domain_id(&self) -> EventDomainId {
163 self.resolved_endpoint.event_domain_id()
164 }
165
166 pub(crate) fn provider_device_identity(&self) -> &super::ProviderDeviceIdentity {
167 self.resolved_endpoint.provider_device_identity()
168 }
169
170 pub(crate) fn resolved_endpoint(&self) -> &ResolvedTransferEndpoint {
171 &self.resolved_endpoint
172 }
173
174 pub(crate) fn storage_class(&self) -> &StorageClass {
175 self.resolved_endpoint.logical().storage_class()
176 }
177
178 pub(super) fn witness(&self) -> &Arc<ExecutableEngineSnapshot> {
179 &self.witness
180 }
181
182 #[cfg(test)]
183 fn for_test(
184 domain: EventDomainId,
185 provider_device_identity: super::ProviderDeviceIdentity,
186 ) -> Self {
187 Self::new(
188 EngineId::new("tenferro-test.schedule-engine").expect("test engine id"),
189 provider_device_identity,
190 domain,
191 StorageClass::new("tenferro-test.schedule-storage").expect("test storage class"),
192 )
193 }
194}
195
196impl PartialEq for ExecutionLocation {
197 fn eq(&self, other: &Self) -> bool {
198 self.resolved_endpoint == other.resolved_endpoint
199 }
200}
201
202impl Eq for ExecutionLocation {}
203
204impl Hash for ExecutionLocation {
205 fn hash<H: Hasher>(&self, state: &mut H) {
206 self.resolved_endpoint.hash(state);
207 }
208}
209
210#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
211pub(crate) struct EventSlotId(u32);
212
213impl EventSlotId {
214 pub(crate) const fn new(value: u32) -> Self {
215 Self(value)
216 }
217}
218
219#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
220pub(crate) struct EventDependency {
221 domain: EventDomainId,
222 slot: EventSlotId,
223 generation: u64,
224}
225
226impl EventDependency {
227 pub(crate) fn new(domain: EventDomainId, slot: EventSlotId, generation: u64) -> Self {
228 Self {
229 domain,
230 slot,
231 generation,
232 }
233 }
234
235 pub(crate) fn domain(&self) -> EventDomainId {
236 self.domain
237 }
238
239 pub(crate) fn from_completion(completion: EventCompletion) -> Self {
240 Self::new(completion.domain, completion.slot, completion.generation)
241 }
242}
243
244#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
245pub(crate) struct EventCompletion {
246 domain: EventDomainId,
247 slot: EventSlotId,
248 generation: u64,
249}
250
251impl EventCompletion {
252 pub(crate) fn new(domain: EventDomainId, slot: EventSlotId, generation: u64) -> Self {
253 Self {
254 domain,
255 slot,
256 generation,
257 }
258 }
259
260 pub(crate) fn domain(&self) -> EventDomainId {
261 self.domain
262 }
263}
264
265#[derive(Clone, Debug)]
266pub(crate) struct ScheduledOperation {
267 instruction_index: usize,
268 location: ExecutionLocation,
269 input_values: Box<[usize]>,
270 output_values: Box<[usize]>,
271 dependencies: Box<[EventDependency]>,
272 completion: EventCompletion,
273}
274
275impl ScheduledOperation {
276 pub(crate) fn new(
277 instruction_index: usize,
278 location: ExecutionLocation,
279 input_values: impl Into<Box<[usize]>>,
280 output_values: impl Into<Box<[usize]>>,
281 dependencies: impl Into<Box<[EventDependency]>>,
282 completion: EventCompletion,
283 ) -> Self {
284 Self {
285 instruction_index,
286 location,
287 input_values: input_values.into(),
288 output_values: output_values.into(),
289 dependencies: dependencies.into(),
290 completion,
291 }
292 }
293
294 #[cfg(test)]
295 pub(crate) fn for_test(
296 domain: EventDomainId,
297 provider_device_identity: super::ProviderDeviceIdentity,
298 ) -> Self {
299 Self::new(
300 0,
301 ExecutionLocation::for_test(domain, provider_device_identity),
302 [],
303 [],
304 [],
305 EventCompletion::new(domain, EventSlotId::new(0), 0),
306 )
307 }
308
309 pub(crate) fn instruction_index(&self) -> usize {
310 self.instruction_index
311 }
312
313 pub(crate) fn location(&self) -> &ExecutionLocation {
314 &self.location
315 }
316
317 pub(crate) fn dependencies(&self) -> &[EventDependency] {
318 &self.dependencies
319 }
320
321 pub(crate) fn completion(&self) -> EventCompletion {
322 self.completion
323 }
324
325 fn retained_bytes(&self) -> Option<usize> {
326 checked_sum([
327 self.input_values
328 .len()
329 .checked_mul(std::mem::size_of::<usize>())?,
330 self.output_values
331 .len()
332 .checked_mul(std::mem::size_of::<usize>())?,
333 self.dependencies
334 .len()
335 .checked_mul(std::mem::size_of::<EventDependency>())?,
336 ])
337 }
338}
339
340#[derive(Clone)]
341pub(crate) struct ScheduledTransfer {
342 value_slot: usize,
343 source_location: ExecutionLocation,
344 destination_location: ExecutionLocation,
345 provider: Arc<dyn super::TransferProvider>,
346 dependencies: Box<[EventDependency]>,
347 completion: EventCompletion,
348}
349
350impl ScheduledTransfer {
351 pub(crate) fn with_provider(
352 value_slot: usize,
353 source_location: ExecutionLocation,
354 destination_location: ExecutionLocation,
355 provider: Arc<dyn super::TransferProvider>,
356 dependencies: impl Into<Box<[EventDependency]>>,
357 completion: EventCompletion,
358 ) -> Self {
359 Self {
360 value_slot,
361 source_location,
362 destination_location,
363 provider,
364 dependencies: dependencies.into(),
365 completion,
366 }
367 }
368
369 #[cfg(test)]
370 pub(crate) fn new(
371 value_slot: usize,
372 source_location: ExecutionLocation,
373 destination_location: ExecutionLocation,
374 dependencies: impl Into<Box<[EventDependency]>>,
375 completion: EventCompletion,
376 ) -> Self {
377 Self::with_provider(
378 value_slot,
379 source_location,
380 destination_location,
381 Arc::new(TestScheduledTransferProvider),
382 dependencies,
383 completion,
384 )
385 }
386
387 #[cfg(test)]
388 pub(crate) fn for_test(
389 source_event_domain: EventDomainId,
390 destination_event_domain: EventDomainId,
391 source_provider_device_identity: super::ProviderDeviceIdentity,
392 destination_provider_device_identity: super::ProviderDeviceIdentity,
393 ) -> Self {
394 let source_location =
395 ExecutionLocation::for_test(source_event_domain, source_provider_device_identity);
396 let destination_location = ExecutionLocation::for_test(
397 destination_event_domain,
398 destination_provider_device_identity,
399 );
400 Self::new(
401 0,
402 source_location,
403 destination_location,
404 [EventDependency::new(
405 source_event_domain,
406 EventSlotId::new(0),
407 0,
408 )],
409 EventCompletion::new(destination_event_domain, EventSlotId::new(0), 0),
410 )
411 }
412
413 pub(crate) fn source_event_domain(&self) -> EventDomainId {
414 self.source_location.event_domain_id()
415 }
416
417 #[cfg(test)]
418 pub(crate) fn destination_event_domain(&self) -> EventDomainId {
419 self.destination_location.event_domain_id()
420 }
421
422 pub(crate) fn value_slot(&self) -> usize {
423 self.value_slot
424 }
425
426 pub(crate) fn source_location(&self) -> &ExecutionLocation {
427 &self.source_location
428 }
429
430 pub(crate) fn destination_location(&self) -> &ExecutionLocation {
431 &self.destination_location
432 }
433
434 pub(crate) fn provider(&self) -> &Arc<dyn super::TransferProvider> {
435 &self.provider
436 }
437
438 pub(crate) fn dependencies(&self) -> &[EventDependency] {
439 &self.dependencies
440 }
441
442 pub(crate) fn completion(&self) -> EventCompletion {
443 self.completion
444 }
445
446 fn retained_bytes(&self) -> Option<usize> {
447 self.dependencies
448 .len()
449 .checked_mul(std::mem::size_of::<EventDependency>())
450 }
451}
452
453impl std::fmt::Debug for ScheduledTransfer {
454 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 formatter
456 .debug_struct("ScheduledTransfer")
457 .field("value_slot", &self.value_slot)
458 .field("source_location", &self.source_location)
459 .field("destination_location", &self.destination_location)
460 .field("dependencies", &self.dependencies)
461 .field("completion", &self.completion)
462 .finish_non_exhaustive()
463 }
464}
465
466#[derive(Clone, Debug)]
467pub(crate) struct ScheduledCollective {
468 location: ExecutionLocation,
469 dependencies: Box<[EventDependency]>,
470 completion: EventCompletion,
471}
472
473impl ScheduledCollective {
474 #[cfg(test)]
475 pub(crate) fn unsupported_for_test() -> Self {
476 let domain = EventDomainId::runtime_created_for_test(
477 RuntimeId::from_nonzero(NonZeroU64::new(1).expect("runtime id")),
478 RuntimeEpoch::from_nonzero(NonZeroU64::new(1).expect("runtime epoch")),
479 RegistrationIdentity::new(
480 NonZeroU64::new(1).expect("registration issuer"),
481 NonZeroU64::new(1).expect("registration ordinal"),
482 ),
483 );
484 let location = ExecutionLocation::new(
485 EngineId::new("tenferro-test.schedule-engine").expect("test engine id"),
486 super::ProviderDeviceIdentity::new(
487 super::ProviderId::new("tenferro.test.schedule").expect("test provider id"),
488 "collective",
489 )
490 .expect("test provider target"),
491 domain,
492 StorageClass::new("tenferro-test.schedule-storage").expect("test storage class"),
493 );
494 Self {
495 location,
496 dependencies: Box::new([]),
497 completion: EventCompletion::new(domain, EventSlotId::new(0), 0),
498 }
499 }
500
501 pub(crate) fn location(&self) -> &ExecutionLocation {
502 &self.location
503 }
504
505 pub(crate) fn completion(&self) -> EventCompletion {
506 self.completion
507 }
508
509 fn dependencies(&self) -> &[EventDependency] {
510 &self.dependencies
511 }
512
513 fn retained_bytes(&self) -> Option<usize> {
514 self.dependencies
515 .len()
516 .checked_mul(std::mem::size_of::<EventDependency>())
517 }
518}
519
520#[derive(Clone, Debug)]
521pub(crate) struct ScheduledBarrier {
522 location: ExecutionLocation,
523 dependencies: Box<[EventDependency]>,
524 completion: EventCompletion,
525}
526
527impl ScheduledBarrier {
528 pub(crate) fn location(&self) -> &ExecutionLocation {
529 &self.location
530 }
531
532 fn dependencies(&self) -> &[EventDependency] {
533 &self.dependencies
534 }
535
536 pub(crate) fn completion(&self) -> EventCompletion {
537 self.completion
538 }
539
540 fn retained_bytes(&self) -> Option<usize> {
541 self.dependencies
542 .len()
543 .checked_mul(std::mem::size_of::<EventDependency>())
544 }
545}
546
547#[derive(Clone, Debug)]
548pub(crate) enum ScheduledNode {
549 Operation(ScheduledOperation),
550 Transfer(ScheduledTransfer),
551 #[allow(
554 dead_code,
555 reason = "collective scheduling remains representation-only in this scoped change"
556 )]
557 Collective(ScheduledCollective),
558 #[allow(
561 dead_code,
562 reason = "barrier construction is deferred until scheduling policy emits explicit barriers"
563 )]
564 Barrier(ScheduledBarrier),
565}
566
567impl ScheduledNode {
568 pub(crate) fn completion(&self) -> EventCompletion {
569 match self {
570 Self::Operation(node) => node.completion(),
571 Self::Transfer(node) => node.completion(),
572 Self::Collective(node) => node.completion(),
573 Self::Barrier(node) => node.completion(),
574 }
575 }
576
577 pub(crate) fn dependencies(&self) -> &[EventDependency] {
578 match self {
579 Self::Operation(node) => node.dependencies(),
580 Self::Transfer(node) => node.dependencies(),
581 Self::Collective(node) => node.dependencies(),
582 Self::Barrier(node) => node.dependencies(),
583 }
584 }
585
586 pub(super) fn event_domain_witness(&self) -> &Arc<ExecutableEngineSnapshot> {
587 match self {
588 Self::Operation(node) => node.location().witness(),
589 Self::Transfer(node) => node.destination_location().witness(),
590 Self::Collective(node) => node.location().witness(),
591 Self::Barrier(node) => node.location().witness(),
592 }
593 }
594
595 fn retained_bytes(&self) -> Option<usize> {
596 match self {
597 Self::Operation(node) => node.retained_bytes(),
598 Self::Transfer(node) => node.retained_bytes(),
599 Self::Collective(node) => node.retained_bytes(),
600 Self::Barrier(node) => node.retained_bytes(),
601 }
602 }
603}
604
605#[derive(Clone, Copy, Debug, Eq, PartialEq)]
606pub(crate) enum ScheduledNodeKind {
607 Collective,
608}
609
610#[derive(Debug, thiserror::Error)]
611#[error("scheduled node {node_index} of kind {node_kind:?} is unsupported during execution")]
612pub(crate) struct UnsupportedScheduledNodeError {
613 pub(crate) node_index: usize,
614 pub(crate) node_kind: ScheduledNodeKind,
615}
616
617#[derive(Clone, Debug)]
618pub(crate) struct ScheduledGraph {
619 nodes: Box<[ScheduledNode]>,
620 input_slots: Box<[usize]>,
621 output_slots: Box<[usize]>,
622 value_count: usize,
623 root_location: ExecutionLocation,
624 input_locations: Box<[ExecutionLocation]>,
625 operation_locations: Box<[ExecutionLocation]>,
626 facts: ScheduleFacts,
628}
629
630#[derive(Clone, Copy, Debug, Default)]
632struct ScheduleFacts {
633 coalescing_eligible: bool,
637 flat: bool,
641 unsupported_node: Option<usize>,
643}
644
645impl ScheduleFacts {
646 fn of(
647 nodes: &[ScheduledNode],
648 root_location: &ExecutionLocation,
649 input_locations: &[ExecutionLocation],
650 operation_locations: &[ExecutionLocation],
651 ) -> Self {
652 let has_transfer = nodes
653 .iter()
654 .any(|node| matches!(node, ScheduledNode::Transfer(_)));
655 let domain = root_location.event_domain_id();
656 let same_domain = |location: &ExecutionLocation| location.event_domain_id() == domain;
657 let coalescing_eligible = !has_transfer
658 && input_locations.iter().all(same_domain)
659 && operation_locations.iter().all(same_domain)
660 && nodes.iter().all(|node| {
661 node.completion().domain() == domain
662 && node
663 .dependencies()
664 .iter()
665 .all(|dependency| dependency.domain() == domain)
666 });
667 let flat = !has_transfer
668 && input_locations
669 .iter()
670 .chain(operation_locations)
671 .all(|location| location == root_location);
672 let unsupported_node = nodes
673 .iter()
674 .position(|node| matches!(node, ScheduledNode::Collective(_)));
675 Self {
676 coalescing_eligible,
677 flat,
678 unsupported_node,
679 }
680 }
681}
682
683impl ScheduledGraph {
684 pub(crate) fn from_exec_program(
685 program: &ExecProgram,
686 root_location: ExecutionLocation,
687 input_locations: &[ExecutionLocation],
688 operation_locations: &[ExecutionLocation],
689 transfer_registry: &FrozenTransferRegistry,
690 ) -> Result<Self, ScheduleBuildError> {
691 let mut nodes = Vec::with_capacity(program.instructions.len());
692 let mut available = vec![Vec::<AvailableValue>::new(); program.n_slots];
693 if input_locations.len() != program.input_slots.len() {
694 return Err(ScheduleBuildError::InputLocationCountMismatch {
695 expected: program.input_slots.len(),
696 actual: input_locations.len(),
697 });
698 }
699 for (&slot, location) in program.input_slots.iter().zip(input_locations) {
700 let values =
701 available
702 .get_mut(slot)
703 .ok_or(ScheduleBuildError::ValueSlotOutOfBounds {
704 slot,
705 value_count: program.n_slots,
706 })?;
707 values.push(AvailableValue {
708 location: location.clone(),
709 completion: None,
710 });
711 }
712
713 for (instruction_index, instruction) in program.instructions.iter().enumerate() {
714 let location = match instruction.semantic_operation_index {
715 Some(operation_index) => operation_locations.get(operation_index).cloned().ok_or(
716 ScheduleBuildError::MissingOperationLocation {
717 instruction_index,
718 operation_index,
719 },
720 )?,
721 None => root_location.clone(),
722 };
723
724 for &slot in &instruction.input_slots {
725 let values =
726 available
727 .get(slot)
728 .ok_or(ScheduleBuildError::ValueSlotOutOfBounds {
729 slot,
730 value_count: program.n_slots,
731 })?;
732 if values.iter().any(|value| value.location == location) {
733 continue;
734 }
735 let source = values
736 .iter()
737 .find_map(|value| {
738 let route = ResolvedTransferRoute::new(
739 value.location.resolved_endpoint().clone(),
740 location.resolved_endpoint().clone(),
741 );
742 transfer_registry
743 .get(&route)
744 .map(|provider| (value.clone(), Arc::clone(provider)))
745 })
746 .ok_or_else(|| {
747 if values.is_empty() {
748 ScheduleBuildError::ValueUnavailable {
749 instruction_index,
750 slot,
751 }
752 } else {
753 ScheduleBuildError::MissingTransferProvider {
754 instruction_index,
755 slot,
756 destination_endpoint: location.endpoint().clone(),
757 available_source_endpoints: values
758 .iter()
759 .map(|value| value.location.endpoint().clone())
760 .collect(),
761 }
762 }
763 })?;
764 let completion = event_completion(&nodes, location.event_domain_id())?;
765 let dependencies = source
766 .0
767 .completion
768 .map(EventDependency::from_completion)
769 .into_iter()
770 .collect::<Vec<_>>();
771 nodes.push(ScheduledNode::Transfer(ScheduledTransfer::with_provider(
772 slot,
773 source.0.location,
774 location.clone(),
775 source.1,
776 dependencies,
777 completion,
778 )));
779 available[slot].push(AvailableValue {
780 location: location.clone(),
781 completion: Some(completion),
782 });
783 }
784
785 let dependencies = operation_dependencies(
786 &available,
787 instruction_index,
788 &instruction.input_slots,
789 &location,
790 )?;
791 let completion = event_completion(&nodes, location.event_domain_id())?;
792 nodes.push(ScheduledNode::Operation(ScheduledOperation::new(
793 instruction_index,
794 location.clone(),
795 instruction.input_slots.clone(),
796 instruction.output_slots.clone(),
797 dependencies,
798 completion,
799 )));
800
801 for (input_index, &slot) in instruction.input_slots.iter().enumerate() {
802 if instruction
803 .last_use
804 .get(input_index)
805 .copied()
806 .unwrap_or(false)
807 {
808 available[slot].clear();
809 }
810 }
811 for &slot in &instruction.output_slots {
812 let values =
813 available
814 .get_mut(slot)
815 .ok_or(ScheduleBuildError::ValueSlotOutOfBounds {
816 slot,
817 value_count: program.n_slots,
818 })?;
819 values.clear();
820 values.push(AvailableValue {
821 location: location.clone(),
822 completion: Some(completion),
823 });
824 }
825 }
826
827 let facts = ScheduleFacts::of(&nodes, &root_location, input_locations, operation_locations);
828 let graph = Self {
829 nodes: nodes.into_boxed_slice(),
830 input_slots: program.input_slots.clone().into_boxed_slice(),
831 output_slots: program.output_slots.clone().into_boxed_slice(),
832 value_count: program.n_slots,
833 root_location,
834 input_locations: input_locations.to_vec().into_boxed_slice(),
835 operation_locations: operation_locations.to_vec().into_boxed_slice(),
836 facts,
837 };
838 graph
839 .validate()
840 .map_err(|source| ScheduleBuildError::InvalidSchedule { source })?;
841 Ok(graph)
842 }
843
844 #[cfg(test)]
845 pub(crate) fn for_test(nodes: Vec<ScheduledNode>) -> Self {
846 let value_count = nodes.len();
847 let root_location = nodes
848 .iter()
849 .find_map(|node| match node {
850 ScheduledNode::Operation(operation) => Some(operation.location().clone()),
851 ScheduledNode::Transfer(transfer) => Some(transfer.destination_location().clone()),
852 ScheduledNode::Collective(_) | ScheduledNode::Barrier(_) => None,
853 })
854 .unwrap_or_else(|| {
855 ExecutionLocation::new(
856 EngineId::new("tenferro-test.schedule-fallback-engine")
857 .expect("test engine id"),
858 super::ProviderDeviceIdentity::new(
859 super::ProviderId::new("tenferro.test.schedule").expect("test provider id"),
860 "fallback",
861 )
862 .expect("test provider target"),
863 EventDomainId::runtime_created_for_test(
864 RuntimeId::from_nonzero(NonZeroU64::new(1).expect("runtime id")),
865 RuntimeEpoch::from_nonzero(NonZeroU64::new(1).expect("runtime epoch")),
866 RegistrationIdentity::new(
867 NonZeroU64::new(1).expect("registration issuer"),
868 NonZeroU64::new(1).expect("registration ordinal"),
869 ),
870 ),
871 StorageClass::new("tenferro-test.schedule-fallback-storage")
872 .expect("test storage class"),
873 )
874 });
875 let facts = ScheduleFacts::of(&nodes, &root_location, &[], &[]);
876 Self {
877 nodes: nodes.into_boxed_slice(),
878 input_slots: Box::new([]),
879 output_slots: Box::new([]),
880 value_count,
881 root_location,
882 input_locations: Box::new([]),
883 operation_locations: Box::new([]),
884 facts,
885 }
886 }
887
888 pub(crate) fn validate(&self) -> Result<(), ScheduleValidationError> {
889 let mut known_completions = HashSet::with_capacity(self.nodes.len());
890 for (index, node) in self.nodes.iter().enumerate() {
891 match node {
892 ScheduledNode::Operation(operation) => {
893 if operation.completion().domain() != operation.location().event_domain_id() {
894 return Err(ScheduleValidationError::CompletionEventDomainMismatch {
895 index,
896 });
897 }
898 }
899 ScheduledNode::Transfer(transfer) => {
900 if transfer.source_location == transfer.destination_location {
901 return Err(ScheduleValidationError::TransferSameLocation { index });
902 }
903 if transfer.completion().domain()
904 != transfer.destination_location().event_domain_id()
905 {
906 return Err(ScheduleValidationError::CompletionEventDomainMismatch {
907 index,
908 });
909 }
910 if transfer.dependencies().iter().any(|dependency| {
911 dependency.domain() != transfer.source_location().event_domain_id()
912 && dependency.domain()
913 != transfer.destination_location().event_domain_id()
914 }) {
915 return Err(ScheduleValidationError::DependencyEventDomainMismatch {
916 index,
917 });
918 }
919 }
920 ScheduledNode::Collective(collective) => {
921 if collective.completion().domain() != collective.location().event_domain_id() {
922 return Err(ScheduleValidationError::CompletionEventDomainMismatch {
923 index,
924 });
925 }
926 }
927 ScheduledNode::Barrier(barrier) => {
928 if barrier.completion().domain() != barrier.location().event_domain_id() {
929 return Err(ScheduleValidationError::CompletionEventDomainMismatch {
930 index,
931 });
932 }
933 }
934 }
935 if node
936 .dependencies()
937 .iter()
938 .any(|dependency| !known_completions.contains(dependency))
939 {
940 return Err(ScheduleValidationError::DependencyNotPriorCompletion { index });
941 }
942 if !known_completions.insert(EventDependency::from_completion(node.completion())) {
943 return Err(ScheduleValidationError::DuplicateCompletion { index });
944 }
945 }
946 Ok(())
947 }
948
949 #[cfg(test)]
950 pub(crate) fn contains_collective(&self) -> bool {
951 self.nodes
952 .iter()
953 .any(|node| matches!(node, ScheduledNode::Collective(_)))
954 }
955
956 pub(crate) fn nodes(&self) -> &[ScheduledNode] {
957 &self.nodes
958 }
959
960 pub(crate) fn root_location(&self) -> &ExecutionLocation {
961 &self.root_location
962 }
963
964 pub(crate) fn input_locations(&self) -> &[ExecutionLocation] {
965 &self.input_locations
966 }
967
968 pub(crate) fn operation_locations(&self) -> &[ExecutionLocation] {
969 &self.operation_locations
970 }
971
972 pub(crate) fn preflight(&self) -> Result<(), UnsupportedScheduledNodeError> {
976 if let Some(index) = self.facts.unsupported_node {
977 return Err(UnsupportedScheduledNodeError {
978 node_index: index,
979 node_kind: ScheduledNodeKind::Collective,
980 });
981 }
982 Ok(())
983 }
984
985 pub(crate) fn coalescing_eligible(&self) -> bool {
990 self.facts.coalescing_eligible
991 }
992
993 pub(crate) fn flat_location(&self) -> Option<&ExecutionLocation> {
997 self.facts.flat.then_some(&self.root_location)
998 }
999
1000 pub(crate) fn retained_bytes(&self) -> Option<usize> {
1001 let node_payload_bytes = self
1002 .nodes
1003 .iter()
1004 .try_fold(0usize, |sum, node| sum.checked_add(node.retained_bytes()?))?;
1005 checked_sum([
1006 std::mem::size_of::<ScheduledGraph>(),
1007 self.nodes
1008 .len()
1009 .checked_mul(std::mem::size_of::<ScheduledNode>())?,
1010 node_payload_bytes,
1011 self.input_slots
1012 .len()
1013 .checked_mul(std::mem::size_of::<usize>())?,
1014 self.output_slots
1015 .len()
1016 .checked_mul(std::mem::size_of::<usize>())?,
1017 self.value_count.checked_mul(std::mem::size_of::<usize>())?,
1018 self.input_locations
1019 .len()
1020 .checked_mul(std::mem::size_of::<ExecutionLocation>())?,
1021 self.operation_locations
1022 .len()
1023 .checked_mul(std::mem::size_of::<ExecutionLocation>())?,
1024 ])
1025 }
1026
1027 #[cfg(test)]
1028 pub(crate) fn transfers_for_test(&self) -> impl Iterator<Item = &ScheduledTransfer> {
1029 self.nodes.iter().filter_map(|node| match node {
1030 ScheduledNode::Transfer(transfer) => Some(transfer),
1031 _ => None,
1032 })
1033 }
1034
1035 #[cfg(test)]
1036 pub(crate) fn nodes_for_test(&self) -> &[ScheduledNode] {
1037 &self.nodes
1038 }
1039}
1040
1041#[derive(Debug, thiserror::Error)]
1042pub(crate) enum ScheduleValidationError {
1043 #[error("transfer node {index} uses the same source and destination location")]
1044 TransferSameLocation { index: usize },
1045 #[error("schedule node {index} completion uses the wrong event domain")]
1046 CompletionEventDomainMismatch { index: usize },
1047 #[error("schedule node {index} dependency uses the wrong event domain")]
1048 DependencyEventDomainMismatch { index: usize },
1049 #[error("schedule node {index} dependency does not refer to a prior completion")]
1050 DependencyNotPriorCompletion { index: usize },
1051 #[error("schedule node {index} reuses a prior completion identity")]
1052 DuplicateCompletion { index: usize },
1053}
1054
1055#[derive(Debug, thiserror::Error)]
1056pub(crate) enum ScheduleBuildError {
1057 #[error("schedule construction produced an invalid immutable schedule")]
1058 InvalidSchedule {
1059 #[source]
1060 source: ScheduleValidationError,
1061 },
1062 #[error("schedule has {actual} input locations for {expected} program inputs")]
1063 InputLocationCountMismatch { expected: usize, actual: usize },
1064 #[error(
1065 "instruction {instruction_index} references semantic operation {operation_index}, \
1066 but that operation has no execution location"
1067 )]
1068 MissingOperationLocation {
1069 instruction_index: usize,
1070 operation_index: usize,
1071 },
1072 #[error("instruction {instruction_index} requires unavailable value slot {slot}")]
1073 ValueUnavailable {
1074 instruction_index: usize,
1075 slot: usize,
1076 },
1077 #[error(
1078 "instruction {instruction_index} has no direct transfer provider for value slot {slot} \
1079 from {available_source_endpoints:?} to {destination_endpoint:?}"
1080 )]
1081 MissingTransferProvider {
1082 instruction_index: usize,
1083 slot: usize,
1084 destination_endpoint: TransferEndpoint,
1085 available_source_endpoints: Vec<TransferEndpoint>,
1086 },
1087 #[error("value slot {slot} is outside schedule value count {value_count}")]
1088 ValueSlotOutOfBounds { slot: usize, value_count: usize },
1089 #[error("scheduled node count exceeds the event-slot identity space")]
1090 EventSlotExhausted,
1091 #[error("the staged program is invalid after metadata view folding")]
1092 InvalidStaging {
1093 #[source]
1094 source: crate::Error,
1095 },
1096}
1097
1098#[cfg(test)]
1099#[derive(Debug)]
1100struct TestScheduledTransferProvider;
1101
1102#[cfg(test)]
1103impl super::TransferProvider for TestScheduledTransferProvider {
1104 fn transfer_blocking(
1105 &self,
1106 _request: super::TransferRequest<'_>,
1107 ) -> crate::Result<tenferro_tensor::Tensor> {
1108 Err(crate::Error::Internal(
1109 "scheduled test transfer provider".into(),
1110 ))
1111 }
1112}
1113
1114fn checked_sum(values: impl IntoIterator<Item = usize>) -> Option<usize> {
1115 values
1116 .into_iter()
1117 .try_fold(0usize, |sum, value| sum.checked_add(value))
1118}
1119
1120#[derive(Clone)]
1121struct AvailableValue {
1122 location: ExecutionLocation,
1123 completion: Option<EventCompletion>,
1124}
1125
1126fn operation_dependencies(
1127 available: &[Vec<AvailableValue>],
1128 instruction_index: usize,
1129 input_slots: &[usize],
1130 location: &ExecutionLocation,
1131) -> Result<Vec<EventDependency>, ScheduleBuildError> {
1132 let mut dependencies = Vec::with_capacity(input_slots.len());
1133 let mut seen = HashSet::with_capacity(input_slots.len());
1134 for &slot in input_slots {
1135 let values = available
1136 .get(slot)
1137 .ok_or(ScheduleBuildError::ValueSlotOutOfBounds {
1138 slot,
1139 value_count: available.len(),
1140 })?;
1141 let value = values
1142 .iter()
1143 .find(|value| &value.location == location)
1144 .ok_or(ScheduleBuildError::ValueUnavailable {
1145 instruction_index,
1146 slot,
1147 })?;
1148 if let Some(completion) = value.completion {
1149 let dependency = EventDependency::from_completion(completion);
1150 if seen.insert(dependency) {
1151 dependencies.push(dependency);
1152 }
1153 }
1154 }
1155 Ok(dependencies)
1156}
1157
1158fn event_completion(
1159 nodes: &[ScheduledNode],
1160 domain: EventDomainId,
1161) -> Result<EventCompletion, ScheduleBuildError> {
1162 let slot = u32::try_from(nodes.len()).map_err(|_| ScheduleBuildError::EventSlotExhausted)?;
1163 Ok(EventCompletion::new(domain, EventSlotId::new(slot), 0))
1164}