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