1use num_complex::{Complex32, Complex64};
7use std::collections::{HashMap, HashSet};
8use std::error::Error as StdError;
9use std::fmt;
10use std::panic::{catch_unwind, AssertUnwindSafe};
11use std::sync::{Arc, Condvar, Mutex, MutexGuard};
12use std::thread::{self, ThreadId};
13
14use smallvec::SmallVec;
15use tenferro_ops::shape_extent::ShapeExtent;
16use tenferro_tensor::{
17 AllocationGroup, DType, DescriptorSlot, GroupError, MemoryKind, Tensor, TensorBackend,
18 TensorRead, TensorValue, TensorView,
19};
20
21use crate::error::ErrorPhase;
22use crate::exec::{
23 DispatchMode, ExecInstruction, ExecProgram, ExecSlot, ExtensionExecutionDispatch,
24};
25use crate::extension_cache::{ExtensionCacheSelector, ExtensionCacheStore};
26use crate::graph::CompiledGraph;
27use crate::runtime::schedule::{
28 EventDependency, ExecutionLocation, ScheduledGraph, ScheduledNode, ScheduledNodeKind,
29 ScheduledTransfer, UnsupportedScheduledNodeError,
30};
31use crate::runtime::{
32 CacheOwnerError, CacheStats, EventDomainError, EventDomainOperation, EventDomainRun,
33 EventToken, InputSignature, PrepareError, PrepareOptions, PreparedOperationPlan, Runtime,
34 RuntimeCacheOwner, SubmissionError, TransferError, TransferProviderContractError,
35 TransferRequest,
36};
37use crate::{Error, Result};
38
39type RuntimeInputReads<'a> = SmallVec<[TensorRead<'a>; 8]>;
40type RuntimeInputShapes<'a> = SmallVec<[&'a [usize]; 8]>;
41type RuntimeShapeScratch = SmallVec<[usize; 8]>;
42
43#[derive(Clone, Copy, Debug)]
44pub(super) enum RuntimeOutputMode {
45 Tensor,
46 Value,
47}
48
49#[derive(Clone)]
55pub struct PreparedCompiledGraph {
56 runtime_id: super::RuntimeId,
57 epoch: super::RuntimeEpoch,
58 program: CompiledGraph,
59 prepared: Arc<super::preparation::PreparedProgram>,
60}
61
62impl PreparedCompiledGraph {
63 #[doc(hidden)]
66 #[must_use]
67 pub fn elementwise_region_summary(&self) -> (usize, usize) {
68 super::region::region_summary(self.prepared.root().regions())
69 }
70
71 #[doc(hidden)]
74 #[must_use]
75 pub fn elementwise_region_execution_counts(&self) -> (usize, usize) {
76 let counters = self.prepared.root().region_counters();
77 (counters.fused(), counters.fallbacks())
78 }
79
80 #[doc(hidden)]
85 #[must_use]
86 pub fn execution_submission_count(&self) -> usize {
87 self.prepared.root().region_counters().submissions()
88 }
89
90 #[doc(hidden)]
96 #[must_use]
97 pub fn execution_command_count(&self) -> usize {
98 let root = self.prepared.root();
99 super::region::command_count(root.schedule(), root.regions())
100 }
101}
102
103pub struct ExecutionHandle {
105 submission: Arc<InFlightSubmission>,
106}
107
108pub struct ExecutionInputs {
120 group: AllocationGroup,
121 bindings: Box<[DescriptorSlot]>,
122}
123
124impl ExecutionInputs {
125 pub fn new(tensors: Vec<Tensor>) -> Result<Self> {
132 let (group, bindings) = AllocationGroup::from_tensors(tensors).map_err(|error| {
133 Error::runtime_state(
134 "ExecutionInputs::new",
135 ErrorPhase::Execution,
136 error.to_string(),
137 )
138 })?;
139 Ok(Self { group, bindings })
140 }
141
142 pub(crate) fn as_reads(&self) -> Result<Vec<TensorRead<'_>>> {
143 self.group.read_views(&self.bindings).map_err(|error| {
144 Error::runtime_state(
145 "ExecutionInputs::as_reads",
146 ErrorPhase::Execution,
147 error.to_string(),
148 )
149 })
150 }
151}
152
153impl fmt::Debug for ExecutionInputs {
154 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
155 formatter
156 .debug_struct("ExecutionInputs")
157 .field("len", &self.bindings.len())
158 .finish()
159 }
160}
161
162pub struct ScopedReadInputs<'env> {
164 bindings: Box<[ScopedReadBinding<'env>]>,
165}
166
167impl<'env> ScopedReadInputs<'env> {
168 pub fn new(bindings: Vec<TensorView<'env>>) -> Self {
170 Self {
171 bindings: bindings
172 .into_iter()
173 .map(|tensor| ScopedReadBinding { tensor })
174 .collect(),
175 }
176 }
177
178 fn as_reads(&self) -> Vec<TensorRead<'env>> {
179 self.bindings
180 .iter()
181 .map(|binding| TensorRead::from_view(binding.tensor.clone()))
182 .collect()
183 }
184
185 fn has_non_host_provider(&self) -> bool {
186 self.bindings.iter().any(|binding| {
187 !matches!(
188 binding.tensor.placement().memory_kind,
189 MemoryKind::PinnedHost | MemoryKind::UnpinnedHost
190 )
191 })
192 }
193
194 pub fn len(&self) -> usize {
196 self.bindings.len()
197 }
198
199 pub fn is_empty(&self) -> bool {
201 self.bindings.is_empty()
202 }
203}
204
205pub struct ScopedReadBinding<'env> {
207 tensor: TensorView<'env>,
208}
209
210impl<'env> ScopedReadBinding<'env> {
211 pub fn new(tensor: TensorView<'env>) -> Self {
213 Self { tensor }
214 }
215
216 pub fn tensor(&self) -> &TensorView<'env> {
218 &self.tensor
219 }
220}
221
222impl fmt::Debug for ScopedReadInputs<'_> {
223 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
224 formatter
225 .debug_struct("ScopedReadInputs")
226 .field("len", &self.bindings.len())
227 .finish()
228 }
229}
230
231#[allow(clippy::large_enum_variant)]
235#[derive(Debug)]
236pub enum ScopedExecutionOutcome<'env> {
237 Completed(ScopedExecutionBundle<'env>),
239 RetiredFailed {
241 error: Error,
242 inputs: ScopedReadInputs<'env>,
243 },
244}
245
246#[derive(Clone, Debug, PartialEq, Eq)]
248pub struct OutputMetadata {
249 pub dtype: DType,
251 pub shape: Box<[usize]>,
253}
254
255pub struct ScopedExecutionBundle<'env> {
257 owned: AllocationGroup,
258 outputs: Box<[ScopedOutput<'env>]>,
259}
260
261impl fmt::Debug for ScopedExecutionBundle<'_> {
262 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
263 formatter
264 .debug_struct("ScopedExecutionBundle")
265 .field("len", &self.outputs.len())
266 .finish()
267 }
268}
269
270impl<'env> ScopedExecutionBundle<'env> {
271 pub fn output(&self, index: usize) -> std::result::Result<OutputRef<'_>, OutputAccessError> {
279 let output = self
280 .outputs
281 .get(index)
282 .ok_or(OutputAccessError::InvalidOutput { index })?;
283 match output {
284 ScopedOutput::Borrowed(view) => Ok(OutputRef::Tensor(view.clone())),
285 ScopedOutput::Owned(slot) => output_ref_from_group(&self.owned, *slot),
286 ScopedOutput::Metadata(metadata) => Ok(OutputRef::Metadata(metadata)),
287 }
288 }
289
290 #[allow(clippy::result_large_err)]
300 pub fn into_owned_output(
301 self,
302 index: usize,
303 ) -> std::result::Result<Tensor, (Self, ScopedOutputExtractError)> {
304 let slot = match self.outputs.get(index) {
305 Some(ScopedOutput::Owned(slot)) => *slot,
306 Some(ScopedOutput::Borrowed(_)) => {
307 return Err((self, ScopedOutputExtractError::BorrowedOutput))
308 }
309 Some(ScopedOutput::Metadata(_)) => {
310 return Err((self, ScopedOutputExtractError::MetadataOutput))
311 }
312 None => return Err((self, ScopedOutputExtractError::InvalidOutput { index })),
313 };
314 let Self { owned, outputs } = self;
315 match owned.into_tensor(slot) {
316 Ok(tensor) => Ok(tensor),
317 Err((owned, error)) => Err((
318 Self { owned, outputs },
319 ScopedOutputExtractError::Output(OutputExtractError::Group(error)),
320 )),
321 }
322 }
323}
324
325#[allow(clippy::large_enum_variant)]
329#[derive(Debug)]
330pub enum ScopedOutput<'env> {
331 Borrowed(TensorView<'env>),
333 Owned(DescriptorSlot),
335 Metadata(OutputMetadata),
337}
338
339#[derive(Debug, thiserror::Error)]
341pub enum ScopedOutputExtractError {
342 #[error("scoped output index {index} is outside the output set")]
343 InvalidOutput { index: usize },
344 #[error("scoped output is borrowed from the caller")]
345 BorrowedOutput,
346 #[error("scoped output contains metadata only")]
347 MetadataOutput,
348 #[error("scoped owned output cannot be extracted: {0}")]
349 Output(#[from] OutputExtractError),
350}
351
352#[derive(Debug)]
354pub struct ScopedSubmitRejected<'env> {
355 source: Box<Error>,
356 inputs: ScopedReadInputs<'env>,
357}
358
359impl<'env> ScopedSubmitRejected<'env> {
360 pub fn into_parts(self) -> (Error, ScopedReadInputs<'env>) {
362 (*self.source, self.inputs)
363 }
364}
365
366impl fmt::Display for ScopedSubmitRejected<'_> {
367 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
368 write!(
369 formatter,
370 "scoped submission rejected before admission: {}",
371 self.source
372 )
373 }
374}
375
376impl StdError for ScopedSubmitRejected<'_> {
377 fn source(&self) -> Option<&(dyn StdError + 'static)> {
378 Some(self.source.as_ref())
379 }
380}
381
382#[derive(Debug)]
384pub struct ExecutionBundle {
385 group: AllocationGroup,
386 outputs: Box<[DescriptorSlot]>,
387}
388
389#[allow(clippy::large_enum_variant)]
393#[derive(Debug)]
394pub enum OutputRef<'a> {
395 Tensor(TensorView<'a>),
396 Metadata(&'a OutputMetadata),
397}
398
399#[derive(Debug, thiserror::Error)]
400pub enum OutputAccessError {
401 #[error("execution output index {index} is outside the output set")]
402 InvalidOutput { index: usize },
403 #[error("execution output group is invalid: {0}")]
404 Group(#[from] GroupError),
405}
406
407#[derive(Debug, thiserror::Error)]
408pub enum OutputExtractError {
409 #[error("execution output index {index} is outside the output set")]
410 InvalidOutput { index: usize },
411 #[error("execution output cannot be extracted: {0}")]
412 Group(#[from] GroupError),
413}
414
415impl ExecutionBundle {
416 fn from_inputs_and_outputs(inputs: ExecutionInputs, outputs: Vec<Tensor>) -> Result<Self> {
417 let mut group = inputs.group;
418 let mut output_slots = Vec::with_capacity(outputs.len());
419 for output in outputs {
420 let slot = group.append_tensor(output).map_err(|error| {
421 Error::runtime_state(
422 "ExecutionBundle::from_inputs_and_outputs",
423 ErrorPhase::Execution,
424 error.to_string(),
425 )
426 })?;
427 output_slots.push(slot);
428 }
429 Ok(Self {
430 group,
431 outputs: output_slots.into_boxed_slice(),
432 })
433 }
434
435 #[cfg(test)]
436 pub(super) fn from_outputs(outputs: Vec<Tensor>) -> Result<Self> {
437 let (group, bindings) = AllocationGroup::from_tensors(outputs).map_err(|error| {
438 Error::runtime_state(
439 "ExecutionBundle::from_outputs",
440 ErrorPhase::Execution,
441 error.to_string(),
442 )
443 })?;
444 Ok(Self {
445 group,
446 outputs: bindings,
447 })
448 }
449
450 pub fn output(&self, index: usize) -> std::result::Result<OutputRef<'_>, OutputAccessError> {
456 let slot = *self
457 .outputs
458 .get(index)
459 .ok_or(OutputAccessError::InvalidOutput { index })?;
460 let mut views = self.group.read_views(std::slice::from_ref(&slot))?;
461 let view = views
462 .pop()
463 .ok_or(OutputAccessError::InvalidOutput { index })?;
464 match view {
465 TensorRead::View(view) => Ok(OutputRef::Tensor(view)),
466 TensorRead::Tensor(_) => Err(OutputAccessError::InvalidOutput { index }),
467 }
468 }
469
470 #[allow(clippy::result_large_err)]
479 pub fn into_output(
480 self,
481 index: usize,
482 ) -> std::result::Result<Tensor, (Self, OutputExtractError)> {
483 let slot = match self.outputs.get(index).copied() {
484 Some(slot) => slot,
485 None => return Err((self, OutputExtractError::InvalidOutput { index })),
486 };
487 let ExecutionBundle { group, outputs } = self;
488 match group.into_tensor(slot) {
489 Ok(tensor) => Ok(tensor),
490 Err((group, error)) => Err((Self { group, outputs }, OutputExtractError::Group(error))),
491 }
492 }
493}
494
495#[derive(Debug)]
497pub enum ExecutionOutcome {
498 Completed(Box<ExecutionBundle>),
500 RetiredFailed {
503 error: Error,
504 inputs: Box<ExecutionInputs>,
505 },
506 CompletionUnproven {
509 error: Error,
510 diagnostic_keys: Box<[String]>,
511 },
512}
513
514#[derive(Debug)]
530pub enum SubmitError {
531 PreAdmission {
534 source: Box<Error>,
535 inputs: Box<ExecutionInputs>,
536 },
537}
538
539impl SubmitError {
540 pub fn into_pre_admission(self) -> Option<(Error, ExecutionInputs)> {
555 match self {
556 Self::PreAdmission { source, inputs } => Some((*source, *inputs)),
557 }
558 }
559}
560
561impl fmt::Display for SubmitError {
562 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
563 match self {
564 Self::PreAdmission { source, .. } => {
565 write!(formatter, "submission rejected before admission: {source}")
566 }
567 }
568 }
569}
570
571impl StdError for SubmitError {
572 fn source(&self) -> Option<&(dyn StdError + 'static)> {
573 match self {
574 Self::PreAdmission { source, .. } => {
575 source.source().or(Some(source.as_ref()))
579 }
580 }
581 }
582}
583
584impl From<SubmitError> for Error {
585 fn from(error: SubmitError) -> Self {
586 match error {
587 SubmitError::PreAdmission { source, .. } => *source,
588 }
589 }
590}
591
592impl ExecutionHandle {
593 pub fn wait(self) -> Result<ExecutionOutcome> {
601 self.submission.wait()
602 }
603}
604
605impl fmt::Debug for ExecutionHandle {
606 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
607 formatter
608 .debug_struct("ExecutionHandle")
609 .field("pending", &self.submission.is_pending())
610 .finish_non_exhaustive()
611 }
612}
613
614pub(super) struct InFlightSubmission {
615 work: Mutex<Option<InFlightWork>>,
616 completion: Mutex<Option<Result<ExecutionOutcome>>>,
617 completed: Condvar,
618}
619
620struct AdmittedExecution {
621 prepared: PreparedCompiledGraph,
622 inputs: ExecutionInputs,
623}
624
625enum InFlightWork {
626 Admitted(Box<AdmittedExecution>),
627 #[cfg(test)]
628 Test {
629 inputs: Box<ExecutionInputs>,
630 work: Box<dyn FnOnce() -> Result<Vec<Tensor>> + Send>,
631 },
632}
633
634impl InFlightSubmission {
635 fn new(prepared: PreparedCompiledGraph, inputs: ExecutionInputs) -> Self {
636 Self {
637 work: Mutex::new(Some(InFlightWork::Admitted(Box::new(AdmittedExecution {
638 prepared,
639 inputs,
640 })))),
641 completion: Mutex::new(None),
642 completed: Condvar::new(),
643 }
644 }
645
646 #[cfg(test)]
647 pub(super) fn for_test(work: impl FnOnce() -> Result<Vec<Tensor>> + Send + 'static) -> Self {
648 Self {
649 work: Mutex::new(Some(InFlightWork::Test {
650 inputs: Box::new(ExecutionInputs::new(Vec::new()).expect("empty test inputs")),
651 work: Box::new(work),
652 })),
653 completion: Mutex::new(None),
654 completed: Condvar::new(),
655 }
656 }
657
658 fn into_unstarted_inputs(self) -> ExecutionInputs {
659 let work = match self.work.into_inner() {
660 Ok(work) => work,
661 Err(poisoned) => poisoned.into_inner(),
662 };
663 match work {
664 Some(InFlightWork::Admitted(admitted)) => admitted.inputs,
665 #[cfg(test)]
666 Some(InFlightWork::Test { inputs, .. }) => *inputs,
667 None => unreachable!("unstarted submission must still contain its owner"),
668 }
669 }
670
671 pub(super) fn run(&self) {
672 let work = match self.work.lock() {
673 Ok(mut work) => work.take(),
674 Err(poisoned) => poisoned.into_inner().take(),
675 };
676 let result = match work {
677 Some(InFlightWork::Admitted(admitted)) => run_admitted_work(admitted),
678 #[cfg(test)]
679 Some(InFlightWork::Test { work, .. }) => {
680 let result = catch_unwind(AssertUnwindSafe(work));
681 match result {
682 Ok(Ok(outputs)) => ExecutionBundle::from_outputs(outputs)
683 .map(|bundle| ExecutionOutcome::Completed(Box::new(bundle))),
684 Ok(Err(error)) => Err(error),
685 Err(payload) => Ok(ExecutionOutcome::CompletionUnproven {
686 error: Error::runtime_state(
687 "ExecutionHandle::wait",
688 ErrorPhase::Execution,
689 panic_payload_message(payload),
690 ),
691 diagnostic_keys: Box::from(["execution.retirement-unproven".to_owned()]),
692 }),
693 }
694 }
695 None => Err(Error::runtime_state(
696 "Runtime::submit",
697 ErrorPhase::Execution,
698 "in-flight submission work was already consumed",
699 )),
700 };
701 match self.completion.lock() {
702 Ok(mut completion) => {
703 *completion = Some(result);
704 }
705 Err(poisoned) => {
706 *poisoned.into_inner() = Some(Err(Error::runtime_state(
707 "ExecutionHandle::wait",
708 ErrorPhase::Execution,
709 "in-flight completion lock poisoned",
710 )));
711 }
712 }
713 self.completed.notify_all();
714 }
715
716 fn wait(&self) -> Result<ExecutionOutcome> {
717 let mut completion = self.completion.lock().map_err(|_| {
718 Error::runtime_state(
719 "ExecutionHandle::wait",
720 ErrorPhase::Execution,
721 "in-flight completion lock poisoned",
722 )
723 })?;
724 loop {
725 if let Some(result) = completion.take() {
726 return result;
727 }
728 completion = self.completed.wait(completion).map_err(|_| {
729 Error::runtime_state(
730 "ExecutionHandle::wait",
731 ErrorPhase::Execution,
732 "in-flight completion lock poisoned while waiting",
733 )
734 })?;
735 }
736 }
737
738 fn is_pending(&self) -> bool {
739 self.completion
740 .lock()
741 .map_or(true, |completion| completion.is_none())
742 }
743}
744
745fn run_admitted_work(admitted: Box<AdmittedExecution>) -> Result<ExecutionOutcome> {
746 let execution = catch_unwind(AssertUnwindSafe(|| {
747 let input_reads = admitted.inputs.as_reads()?;
748 execute_admitted(&admitted.prepared, &input_reads)
749 }));
750 match execution {
751 Ok(Ok(outputs)) => {
752 let AdmittedExecution { inputs, .. } = *admitted;
753 match ExecutionBundle::from_inputs_and_outputs(inputs, outputs) {
754 Ok(bundle) => Ok(ExecutionOutcome::Completed(Box::new(bundle))),
755 Err(error) => Ok(ExecutionOutcome::CompletionUnproven {
756 error,
757 diagnostic_keys: Box::from(["execution.bundle-build".to_owned()]),
758 }),
759 }
760 }
761 Ok(Err(error)) => {
762 let AdmittedExecution { inputs, .. } = *admitted;
763 Ok(ExecutionOutcome::RetiredFailed {
764 error,
765 inputs: Box::new(inputs),
766 })
767 }
768 Err(payload) => {
769 let error = Error::runtime_state(
770 "ExecutionHandle::wait",
771 ErrorPhase::Execution,
772 panic_payload_message(payload),
773 );
774 Box::leak(admitted);
778 Ok(ExecutionOutcome::CompletionUnproven {
779 error,
780 diagnostic_keys: Box::from(["execution.retirement-unproven".to_owned()]),
781 })
782 }
783 }
784}
785
786pub(super) trait SubmissionSpawner {
787 fn spawn(&self, submission: Arc<InFlightSubmission>) -> std::io::Result<()>;
788}
789
790pub(super) fn spawn_in_flight(
791 submission: Arc<InFlightSubmission>,
792 spawner: &dyn SubmissionSpawner,
793) -> std::result::Result<ExecutionHandle, Box<(ExecutionInputs, Error)>> {
794 if let Err(source) = spawner.spawn(Arc::clone(&submission)) {
795 let submission = match Arc::try_unwrap(submission) {
799 Ok(submission) => submission,
800 Err(_) => unreachable!("failed spawner must not retain submission"),
801 };
802 let inputs = submission.into_unstarted_inputs();
803 let error = Error::runtime_state_source(
804 "Runtime::submit",
805 ErrorPhase::Execution,
806 SubmissionError::WorkerSpawn { source },
807 );
808 return Err(Box::new((inputs, error)));
809 }
810 Ok(ExecutionHandle { submission })
811}
812
813pub(super) struct OsThreadSpawner;
814
815impl SubmissionSpawner for OsThreadSpawner {
816 fn spawn(&self, submission: Arc<InFlightSubmission>) -> std::io::Result<()> {
817 thread::Builder::new()
818 .name("tenferro-runtime-submit".to_string())
819 .spawn(move || submission.run())
820 .map(drop)
821 }
822}
823
824impl fmt::Debug for PreparedCompiledGraph {
825 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
826 formatter
827 .debug_struct("PreparedCompiledGraph")
828 .field("runtime_id", &self.runtime_id)
829 .field("epoch", &self.epoch)
830 .field("program", &self.program)
831 .finish_non_exhaustive()
832 }
833}
834
835fn panic_payload_message(payload: Box<dyn std::any::Any + Send + 'static>) -> String {
836 if let Some(message) = payload.downcast_ref::<&str>() {
837 format!("submitted execution panicked: {message}")
838 } else if let Some(message) = payload.downcast_ref::<String>() {
839 format!("submitted execution panicked: {message}")
840 } else {
841 "submitted execution panicked".to_string()
842 }
843}
844
845#[allow(
846 dead_code,
847 reason = "Phase 5 runtime execution task adds erased dispatch methods"
848)]
849pub(super) trait ErasedTensorBackendExecutor: fmt::Debug + Send + Sync {
850 fn backend_type_name(&self) -> &'static str;
851 fn extension_cache_stats(&self) -> std::result::Result<CacheStats, CacheOwnerError>;
852 fn clear_extension_caches(&self) -> std::result::Result<(), CacheOwnerError>;
853 fn execute(
854 &self,
855 program: &ExecProgram,
856 operations: &[PreparedOperationPlan],
857 inputs: Vec<Tensor>,
858 ) -> Result<Vec<Tensor>>;
859 fn execute_tensor_refs(
860 &self,
861 program: &ExecProgram,
862 operations: &[PreparedOperationPlan],
863 inputs: &[&Tensor],
864 ) -> Result<Vec<Tensor>>;
865 fn execute_values(
866 &self,
867 program: &ExecProgram,
868 operations: &[PreparedOperationPlan],
869 inputs: Vec<Tensor>,
870 ) -> Result<Vec<TensorValue>>;
871 fn execute_value_refs(
872 &self,
873 program: &ExecProgram,
874 operations: &[PreparedOperationPlan],
875 inputs: &[&Tensor],
876 ) -> Result<Vec<TensorValue>>;
877 fn execute_slot_instruction<'input>(
878 &self,
879 instruction_index: usize,
880 instruction: &ExecInstruction,
881 operations: &[PreparedOperationPlan],
882 slots: &mut [Option<ExecSlot<'input>>],
883 output_mode: RuntimeOutputMode,
884 terminal_slots: &[bool],
885 ) -> Result<()>;
886 fn execute_elementwise_fusion_slots<'input>(
893 &self,
894 input_slots: &[usize],
895 instruction_count: usize,
896 plan: &tenferro_tensor::backend::ElementwiseFusionPlan,
897 slots: &mut [Option<ExecSlot<'input>>],
898 output_slots: &[usize],
899 ) -> Result<bool>;
900 fn materialize_slot<'input>(&self, slot: ExecSlot<'input>) -> Result<Tensor>;
901 fn materialize_slot_value<'input>(&self, slot: ExecSlot<'input>) -> Result<TensorValue>;
902}
903
904pub(super) fn erased_tensor_backend_executor<B>(backend: B) -> Arc<dyn ErasedTensorBackendExecutor>
905where
906 B: TensorBackend + Send + Sync + 'static,
907{
908 Arc::new(TensorBackendExecutor::<B>::new(backend))
909}
910
911pub(super) fn extension_cache_owner(
912 executor: Arc<dyn ErasedTensorBackendExecutor>,
913) -> Arc<dyn RuntimeCacheOwner> {
914 Arc::new(TensorBackendExtensionCacheOwner { executor })
915}
916
917#[derive(Debug)]
918struct TensorBackendExtensionCacheOwner {
919 executor: Arc<dyn ErasedTensorBackendExecutor>,
920}
921
922impl RuntimeCacheOwner for TensorBackendExtensionCacheOwner {
923 fn cache_stats(&self) -> std::result::Result<CacheStats, CacheOwnerError> {
924 self.executor.extension_cache_stats()
925 }
926
927 fn clear_caches(&self) -> std::result::Result<(), CacheOwnerError> {
928 self.executor.clear_extension_caches()
929 }
930}
931
932#[allow(
933 dead_code,
934 reason = "Phase 5 runtime execution task consumes backend execution state"
935)]
936struct TensorBackendExecutorState<B: TensorBackend + 'static> {
937 backend: B,
938 backend_cache: B::RuntimeCache,
939 extension_caches: ExtensionCacheStore,
940 slot_workspace: Vec<Option<ExecSlot<'static>>>,
941 borrowed_slot_workspace_capacity: usize,
942}
943
944struct TensorBackendExecutor<B: TensorBackend + 'static> {
945 state: Mutex<TensorBackendExecutorSlot<B>>,
946 available: Condvar,
947}
948
949struct TensorBackendExecutorSlot<B: TensorBackend + 'static> {
950 state: Option<TensorBackendExecutorState<B>>,
951 active_thread: Option<ThreadId>,
952 execution_poisoned: bool,
953}
954
955impl<B> TensorBackendExecutor<B>
956where
957 B: TensorBackend + Send + Sync + 'static,
958{
959 fn new(backend: B) -> Self {
960 Self {
961 state: Mutex::new(TensorBackendExecutorSlot {
962 state: Some(TensorBackendExecutorState {
963 backend,
964 backend_cache: B::RuntimeCache::default(),
965 extension_caches: ExtensionCacheStore::new(),
966 slot_workspace: Vec::new(),
967 borrowed_slot_workspace_capacity: 0,
968 }),
969 active_thread: None,
970 execution_poisoned: false,
971 }),
972 available: Condvar::new(),
973 }
974 }
975
976 fn lease_state(&self, caller: &'static str) -> Result<TensorBackendExecutorLease<'_, B>> {
977 let current = thread::current().id();
978 let mut slot = self.lock_slot(caller)?;
979 loop {
980 if slot.execution_poisoned {
981 return Err(Error::runtime_state(
982 caller,
983 ErrorPhase::Execution,
984 "tensor backend executor state poisoned by panic during prior execution",
985 ));
986 }
987 if let Some(state) = slot.state.take() {
988 slot.active_thread = Some(current);
989 return Ok(TensorBackendExecutorLease {
990 executor: self,
991 state: Some(state),
992 });
993 }
994 if slot.active_thread == Some(current) {
995 return Err(Error::runtime_state(
996 caller,
997 ErrorPhase::Execution,
998 "reentrant tensor backend executor call would deadlock",
999 ));
1000 }
1001 slot = self.available.wait(slot).map_err(|_| {
1002 Error::runtime_state(
1003 caller,
1004 ErrorPhase::Execution,
1005 "tensor backend executor state lock poisoned while waiting",
1006 )
1007 })?;
1008 }
1009 }
1010
1011 fn lock_slot(
1012 &self,
1013 caller: &'static str,
1014 ) -> Result<MutexGuard<'_, TensorBackendExecutorSlot<B>>> {
1015 self.state.lock().map_err(|_| {
1016 Error::runtime_state(
1017 caller,
1018 ErrorPhase::Execution,
1019 "tensor backend executor state lock poisoned",
1020 )
1021 })
1022 }
1023}
1024
1025struct TensorBackendExecutorLease<'a, B: TensorBackend + 'static> {
1026 executor: &'a TensorBackendExecutor<B>,
1027 state: Option<TensorBackendExecutorState<B>>,
1028}
1029
1030impl<B: TensorBackend + 'static> TensorBackendExecutorLease<'_, B> {
1031 fn state_mut(&mut self) -> &mut TensorBackendExecutorState<B> {
1032 self.state
1033 .as_mut()
1034 .expect("executor state lease always owns state before drop")
1035 }
1036}
1037
1038impl<B: TensorBackend + 'static> Drop for TensorBackendExecutorLease<'_, B> {
1039 fn drop(&mut self) {
1040 let Some(state) = self.state.take() else {
1041 return;
1042 };
1043 let panicking = thread::panicking();
1044 let mut wake_all_waiters = panicking;
1045 match self.executor.state.lock() {
1046 Ok(mut slot) => {
1047 slot.state = Some(state);
1048 slot.active_thread = None;
1049 slot.execution_poisoned |= panicking;
1050 }
1051 Err(poisoned_lock) => {
1052 let mut slot = poisoned_lock.into_inner();
1053 slot.state = Some(state);
1054 slot.active_thread = None;
1055 slot.execution_poisoned = true;
1056 wake_all_waiters = true;
1057 }
1058 }
1059 if wake_all_waiters {
1060 self.executor.available.notify_all();
1061 } else {
1062 self.executor.available.notify_one();
1063 }
1064 }
1065}
1066
1067impl<B: TensorBackend + 'static> fmt::Debug for TensorBackendExecutor<B> {
1068 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1069 formatter
1070 .debug_struct("TensorBackendExecutor")
1071 .field("backend_type", &std::any::type_name::<B>())
1072 .field("state_lock_poisoned", &self.state.is_poisoned())
1073 .finish_non_exhaustive()
1074 }
1075}
1076
1077impl<B> ErasedTensorBackendExecutor for TensorBackendExecutor<B>
1078where
1079 B: TensorBackend + Send + Sync + 'static,
1080{
1081 fn backend_type_name(&self) -> &'static str {
1082 std::any::type_name::<B>()
1083 }
1084
1085 fn extension_cache_stats(&self) -> std::result::Result<CacheStats, CacheOwnerError> {
1086 let lease = self
1087 .lease_state("Runtime::extension_cache_stats")
1088 .map_err(cache_owner_error)?;
1089 let state = lease
1090 .state
1091 .as_ref()
1092 .expect("executor state lease always owns state before drop");
1093 Ok(cache_stats_from_tensor_stats(
1094 state.extension_caches.stats(ExtensionCacheSelector::All),
1095 ))
1096 }
1097
1098 fn clear_extension_caches(&self) -> std::result::Result<(), CacheOwnerError> {
1099 let mut lease = self
1100 .lease_state("Runtime::clear_extension_caches")
1101 .map_err(cache_owner_error)?;
1102 lease.state_mut().extension_caches.clear();
1103 Ok(())
1104 }
1105
1106 fn execute(
1107 &self,
1108 program: &ExecProgram,
1109 operations: &[PreparedOperationPlan],
1110 inputs: Vec<Tensor>,
1111 ) -> Result<Vec<Tensor>> {
1112 validate_exec_input_count(program, inputs.len())?;
1113 let mut lease = self.lease_state("Runtime::run_compiled")?;
1114 let TensorBackendExecutorState {
1115 backend,
1116 backend_cache,
1117 extension_caches,
1118 slot_workspace,
1119 borrowed_slot_workspace_capacity: _,
1120 } = lease.state_mut();
1121 let mut extension_dispatch = ExtensionExecutionDispatch {
1122 operations,
1123 caches: extension_caches,
1124 };
1125 crate::segment::eval_exec_segmented_with_cache_and_workspace(
1126 backend,
1127 program,
1128 inputs,
1129 slot_workspace,
1130 backend_cache,
1131 Some(&mut extension_dispatch),
1132 )
1133 }
1134
1135 fn execute_tensor_refs(
1136 &self,
1137 program: &ExecProgram,
1138 operations: &[PreparedOperationPlan],
1139 inputs: &[&Tensor],
1140 ) -> Result<Vec<Tensor>> {
1141 validate_exec_input_count(program, inputs.len())?;
1142 let inputs = inputs
1143 .iter()
1144 .map(|tensor| ExecSlot::Read(TensorRead::from_tensor(tensor)))
1145 .collect();
1146 let mut lease = self.lease_state("Runtime::run_compiled")?;
1147 let TensorBackendExecutorState {
1148 backend,
1149 backend_cache,
1150 extension_caches,
1151 borrowed_slot_workspace_capacity,
1152 ..
1153 } = lease.state_mut();
1154 let mut extension_dispatch = ExtensionExecutionDispatch {
1155 operations,
1156 caches: extension_caches,
1157 };
1158 let mut slot_workspace = Vec::with_capacity(*borrowed_slot_workspace_capacity);
1159 let result = crate::segment::eval_exec_segmented_slots_with_cache_and_workspace(
1160 backend,
1161 program,
1162 inputs,
1163 &mut slot_workspace,
1164 backend_cache,
1165 Some(&mut extension_dispatch),
1166 );
1167 *borrowed_slot_workspace_capacity = slot_workspace.capacity();
1168 result
1169 }
1170
1171 fn execute_values(
1172 &self,
1173 program: &ExecProgram,
1174 operations: &[PreparedOperationPlan],
1175 inputs: Vec<Tensor>,
1176 ) -> Result<Vec<TensorValue>> {
1177 validate_exec_input_count(program, inputs.len())?;
1178 let inputs = inputs.into_iter().map(ExecSlot::Owned).collect();
1179 let mut lease = self.lease_state("Runtime::run_compiled_values")?;
1180 let TensorBackendExecutorState {
1181 backend,
1182 backend_cache,
1183 extension_caches,
1184 slot_workspace,
1185 borrowed_slot_workspace_capacity: _,
1186 } = lease.state_mut();
1187 let mut extension_dispatch = ExtensionExecutionDispatch {
1188 operations,
1189 caches: extension_caches,
1190 };
1191 crate::segment::eval_exec_segmented_slot_values_with_cache_and_workspace(
1192 backend,
1193 program,
1194 inputs,
1195 slot_workspace,
1196 backend_cache,
1197 Some(&mut extension_dispatch),
1198 )
1199 }
1200
1201 fn execute_value_refs(
1202 &self,
1203 program: &ExecProgram,
1204 operations: &[PreparedOperationPlan],
1205 inputs: &[&Tensor],
1206 ) -> Result<Vec<TensorValue>> {
1207 validate_exec_input_count(program, inputs.len())?;
1208 let inputs = inputs
1209 .iter()
1210 .map(|tensor| ExecSlot::Read(TensorRead::from_tensor(tensor)))
1211 .collect();
1212 let mut lease = self.lease_state("Runtime::run_compiled_values")?;
1213 let TensorBackendExecutorState {
1214 backend,
1215 backend_cache,
1216 extension_caches,
1217 borrowed_slot_workspace_capacity,
1218 ..
1219 } = lease.state_mut();
1220 let mut extension_dispatch = ExtensionExecutionDispatch {
1221 operations,
1222 caches: extension_caches,
1223 };
1224 let mut slot_workspace = Vec::with_capacity(*borrowed_slot_workspace_capacity);
1225 let result = crate::segment::eval_exec_segmented_slot_values_with_cache_and_workspace(
1226 backend,
1227 program,
1228 inputs,
1229 &mut slot_workspace,
1230 backend_cache,
1231 Some(&mut extension_dispatch),
1232 );
1233 *borrowed_slot_workspace_capacity = slot_workspace.capacity();
1234 result
1235 }
1236
1237 fn execute_slot_instruction<'input>(
1238 &self,
1239 instruction_index: usize,
1240 instruction: &ExecInstruction,
1241 operations: &[PreparedOperationPlan],
1242 slots: &mut [Option<ExecSlot<'input>>],
1243 output_mode: RuntimeOutputMode,
1244 terminal_slots: &[bool],
1245 ) -> Result<()> {
1246 let mut lease = self.lease_state("Runtime::run_compiled scheduled instruction")?;
1247 let TensorBackendExecutorState {
1248 backend,
1249 backend_cache,
1250 extension_caches,
1251 ..
1252 } = lease.state_mut();
1253 let mut extension_dispatch = ExtensionExecutionDispatch {
1254 operations,
1255 caches: extension_caches,
1256 };
1257
1258 if matches!(output_mode, RuntimeOutputMode::Value)
1259 && backend.with_backend_session(|exec| {
1260 crate::exec::try_execute_terminal_value_instruction(
1261 exec,
1262 slots,
1263 instruction,
1264 terminal_slots,
1265 )
1266 })?
1267 {
1268 } else if crate::exec::is_host_instruction(instruction) {
1270 crate::exec::execute_host_instruction(backend, slots, instruction)?;
1271 } else if crate::exec::is_ffi_instruction(instruction) {
1272 crate::exec::execute_ffi_instruction_cached(
1273 backend,
1274 backend_cache,
1275 slots,
1276 instruction,
1277 DispatchMode::Unsegmented,
1278 Some(instruction_index),
1279 Some(&mut extension_dispatch),
1280 )?;
1281 } else {
1282 let result = backend.with_backend_session(|exec| {
1283 crate::exec::execute_backend_op(exec, slots, instruction)
1284 })?;
1285 slots[instruction.output_slots[0]] = Some(ExecSlot::Owned(result));
1286 }
1287 crate::exec::reclaim_last_use_inputs_backend(slots, instruction, backend);
1288 Ok(())
1289 }
1290
1291 fn execute_elementwise_fusion_slots<'input>(
1292 &self,
1293 input_slots: &[usize],
1294 instruction_count: usize,
1295 plan: &tenferro_tensor::backend::ElementwiseFusionPlan,
1296 slots: &mut [Option<ExecSlot<'input>>],
1297 output_slots: &[usize],
1298 ) -> Result<bool> {
1299 let mut views: Vec<usize> = Vec::new();
1304 for &slot in input_slots {
1305 let value = slots
1306 .get(slot)
1307 .and_then(Option::as_ref)
1308 .ok_or_else(|| crate::Error::from(tenferro_tensor::Error::MissingValue { slot }))?;
1309 if value.as_tensor("elementwise_region").is_err() {
1310 views.push(slot);
1311 }
1312 }
1313 if !views.is_empty() {
1314 if instruction_count < super::region::VIEW_COPY_MIN_INSTRUCTIONS {
1315 return Ok(false);
1316 }
1317 let mut lease = self.lease_state("Runtime::run_prepared elementwise region copy")?;
1318 let backend = &mut lease.state_mut().backend;
1319 for &slot in &views {
1320 let tensor = {
1321 let read = slots[slot].as_ref().map(ExecSlot::as_read).ok_or_else(|| {
1322 crate::Error::from(tenferro_tensor::Error::MissingValue { slot })
1323 })?;
1324 backend.with_backend_session(|exec| exec.to_contiguous_read(read))?
1325 };
1326 slots[slot] = Some(ExecSlot::Owned(tensor));
1327 }
1328 }
1329
1330 let mut inputs: Vec<&Tensor> = Vec::with_capacity(input_slots.len());
1331 for &slot in input_slots {
1332 let value = slots
1333 .get(slot)
1334 .and_then(Option::as_ref)
1335 .ok_or_else(|| crate::Error::from(tenferro_tensor::Error::MissingValue { slot }))?;
1336 inputs.push(value.as_tensor("elementwise_region")?);
1337 }
1338
1339 if !crate::segment::plan_matches_input_dtypes(plan, &inputs) {
1340 return Ok(false);
1341 }
1342
1343 let mut lease = self.lease_state("Runtime::run_prepared elementwise region")?;
1344 let backend = &mut lease.state_mut().backend;
1345 let outputs =
1346 backend.with_backend_session(|exec| exec.execute_elementwise_fusion(&inputs, plan))?;
1347 let Some(outputs) = outputs else {
1348 return Ok(false);
1349 };
1350 if outputs.len() != output_slots.len() {
1351 return Err(crate::Error::Internal(format!(
1352 "fused elementwise region produced {} outputs for {} slots",
1353 outputs.len(),
1354 output_slots.len()
1355 )));
1356 }
1357 for (&slot, tensor) in output_slots.iter().zip(outputs) {
1358 slots[slot] = Some(ExecSlot::Owned(tensor));
1359 }
1360 Ok(true)
1361 }
1362
1363 fn materialize_slot<'input>(&self, slot: ExecSlot<'input>) -> Result<Tensor> {
1364 let mut lease = self.lease_state("Runtime::run_compiled collect outputs")?;
1365 let backend = &mut lease.state_mut().backend;
1366 backend.with_backend_session(|exec| slot.into_tensor(exec))
1367 }
1368
1369 fn materialize_slot_value<'input>(&self, slot: ExecSlot<'input>) -> Result<TensorValue> {
1370 let mut lease = self.lease_state("Runtime::run_compiled_values collect outputs")?;
1371 let backend = &mut lease.state_mut().backend;
1372 backend.with_backend_session(|exec| slot.into_value(exec))
1373 }
1374}
1375
1376fn cache_owner_error(error: Error) -> CacheOwnerError {
1377 CacheOwnerError::new(Arc::new(error))
1378}
1379
1380fn cache_stats_from_tensor_stats(stats: tenferro_tensor::CacheStats) -> CacheStats {
1381 CacheStats {
1382 entries: stats.entries,
1383 retained_bytes: stats.retained_bytes,
1384 hits: stats.hits,
1385 misses: stats.misses,
1386 evictions: stats.evictions,
1387 clears: stats.clears,
1388 }
1389}
1390
1391pub(super) fn run_compiled(
1392 runtime: &Runtime,
1393 program: &CompiledGraph,
1394 inputs: &[&Tensor],
1395) -> Result<Vec<Tensor>> {
1396 let inputs = resolve_input_refs(program, inputs)?;
1397 let signature = input_signature_reads(&inputs)?;
1398 let prepared = prepare(runtime, program, &signature)?;
1399 validate_prepared_epoch(runtime, prepared.root().epoch(), "Runtime::run_compiled")?;
1400 execute_scheduled_reads(
1401 prepared.root().staging(),
1402 prepared.root().schedule(),
1403 prepared.root().regions(),
1404 prepared.root().region_counters(),
1405 prepared.operations(),
1406 &inputs,
1407 )
1408}
1409
1410pub(super) fn execute_scoped_read_only<'env>(
1415 runtime: &Runtime,
1416 program: &CompiledGraph,
1417 inputs: ScopedReadInputs<'env>,
1418) -> std::result::Result<ScopedExecutionOutcome<'env>, ScopedSubmitRejected<'env>> {
1419 if inputs.has_non_host_provider() {
1420 return Err(ScopedSubmitRejected {
1421 source: Box::new(Error::unsupported(
1422 "Runtime::execute_scoped_read_only",
1423 ErrorPhase::Execution,
1424 "scoped borrowed execution requires host/CPU storage",
1425 )),
1426 inputs,
1427 });
1428 }
1429
1430 let reads = inputs.as_reads();
1431 let prepared = match prepare_compiled_reads(runtime, program, &reads) {
1432 Ok(prepared) => prepared,
1433 Err(source) => {
1434 return Err(ScopedSubmitRejected {
1435 source: Box::new(source),
1436 inputs,
1437 })
1438 }
1439 };
1440 if !schedule_supports_scoped_execution(prepared.prepared.root().schedule()) {
1441 return Err(ScopedSubmitRejected {
1442 source: Box::new(Error::unsupported(
1443 "Runtime::execute_scoped_read_only",
1444 ErrorPhase::Execution,
1445 "the selected provider does not prove retirement before scoped return",
1446 )),
1447 inputs,
1448 });
1449 }
1450
1451 match execute_scoped_admitted(
1452 prepared.prepared.root().staging(),
1453 prepared.prepared.root().schedule(),
1454 prepared.prepared.operations(),
1455 &reads,
1456 ) {
1457 Ok(bundle) => Ok(ScopedExecutionOutcome::Completed(bundle)),
1458 Err(error) => Ok(ScopedExecutionOutcome::RetiredFailed { error, inputs }),
1459 }
1460}
1461
1462fn execute_scoped_admitted<'env>(
1463 program: &ExecProgram,
1464 schedule: &ScheduledGraph,
1465 operations: &[PreparedOperationPlan],
1466 inputs: &[TensorRead<'env>],
1467) -> Result<ScopedExecutionBundle<'env>> {
1468 validate_exec_input_count(program, inputs.len())?;
1469 crate::exec::validate_exec_program(program, "scoped tensor executor")?;
1470 let input_slots = inputs.iter().cloned().map(ExecSlot::Read).collect();
1471 let mut located = execute_scheduled_slots(
1472 program,
1473 schedule,
1474 &[],
1475 &super::region::RegionExecutionCounters::default(),
1476 operations,
1477 input_slots,
1478 RuntimeOutputMode::Tensor,
1479 )?;
1480 collect_scoped_outputs(program, &mut located)
1481}
1482
1483pub(crate) fn collect_scoped_outputs<'env>(
1484 program: &ExecProgram,
1485 outputs: &mut [Option<LocatedExecSlot<'env>>],
1486) -> Result<ScopedExecutionBundle<'env>> {
1487 let mut owned = AllocationGroup::from_tensors(Vec::new())
1488 .map(|(group, _)| group)
1489 .map_err(|error| {
1490 Error::runtime_state(
1491 "Runtime::collect_scoped_outputs",
1492 ErrorPhase::Execution,
1493 error.to_string(),
1494 )
1495 })?;
1496 let mut scoped = Vec::with_capacity(program.output_slots.len());
1497 for &slot in &program.output_slots {
1498 let located = outputs
1499 .get_mut(slot)
1500 .and_then(Option::take)
1501 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
1502 let output = match located.value {
1503 ExecSlot::Read(read) => ScopedOutput::Borrowed(read.tensor_view()),
1504 ExecSlot::Owned(tensor) => {
1505 let slot = owned.append_tensor(tensor).map_err(|error| {
1506 Error::runtime_state(
1507 "Runtime::collect_scoped_outputs",
1508 ErrorPhase::Execution,
1509 error.to_string(),
1510 )
1511 })?;
1512 ScopedOutput::Owned(slot)
1513 }
1514 ExecSlot::Value(value) => {
1515 let (group, slot, dtype, shape) = value.try_into_group_parts().map_err(|_| {
1516 Error::runtime_state(
1517 "Runtime::collect_scoped_outputs",
1518 ErrorPhase::Execution,
1519 "scoped value output was retained by another owner",
1520 )
1521 })?;
1522 let slot = owned.append_group(group, slot).map_err(|error| {
1523 Error::runtime_state(
1524 "Runtime::collect_scoped_outputs",
1525 ErrorPhase::Execution,
1526 error.to_string(),
1527 )
1528 })?;
1529 let _ = (dtype, shape);
1530 ScopedOutput::Owned(slot)
1531 }
1532 };
1533 scoped.push(output);
1534 }
1535 outputs.iter_mut().for_each(|output| *output = None);
1536 Ok(ScopedExecutionBundle {
1537 owned,
1538 outputs: scoped.into_boxed_slice(),
1539 })
1540}
1541
1542fn output_ref_from_group<'a>(
1543 group: &'a AllocationGroup,
1544 slot: DescriptorSlot,
1545) -> std::result::Result<OutputRef<'a>, OutputAccessError> {
1546 let mut views = group.read_views(std::slice::from_ref(&slot))?;
1547 match views.pop() {
1548 Some(TensorRead::View(view)) => Ok(OutputRef::Tensor(view)),
1549 Some(TensorRead::Tensor(_)) | None => Err(OutputAccessError::InvalidOutput {
1550 index: slot.index(),
1551 }),
1552 }
1553}
1554
1555fn schedule_supports_scoped_execution(schedule: &ScheduledGraph) -> bool {
1556 let supports = |location: &ExecutionLocation| {
1557 location
1558 .witness()
1559 .event_domain_driver()
1560 .supports_scoped_read_only()
1561 };
1562 supports(schedule.root_location())
1563 && schedule.input_locations().iter().all(supports)
1564 && schedule.operation_locations().iter().all(supports)
1565 && schedule.nodes().iter().all(|node| {
1566 node.event_domain_witness()
1567 .event_domain_driver()
1568 .supports_scoped_read_only()
1569 })
1570}
1571
1572pub(super) fn prepare_compiled(
1573 runtime: &Runtime,
1574 program: &CompiledGraph,
1575 inputs: &[&Tensor],
1576) -> Result<PreparedCompiledGraph> {
1577 let inputs = resolve_input_refs(program, inputs)?;
1578 let signature = input_signature_reads(&inputs)?;
1579 let prepared = prepare(runtime, program, &signature)?;
1580 validate_prepared_epoch(
1581 runtime,
1582 prepared.root().epoch(),
1583 "Runtime::prepare_compiled",
1584 )?;
1585 Ok(PreparedCompiledGraph {
1586 runtime_id: runtime.id(),
1587 epoch: prepared.root().epoch(),
1588 program: program.clone(),
1589 prepared,
1590 })
1591}
1592
1593pub(super) fn submit(
1594 runtime: &Runtime,
1595 program: &CompiledGraph,
1596 inputs: ExecutionInputs,
1597) -> std::result::Result<ExecutionHandle, SubmitError> {
1598 submit_with_spawner(runtime, program, inputs, &OsThreadSpawner)
1599}
1600
1601pub(super) fn submit_with_spawner(
1602 runtime: &Runtime,
1603 program: &CompiledGraph,
1604 inputs: ExecutionInputs,
1605 spawner: &dyn SubmissionSpawner,
1606) -> std::result::Result<ExecutionHandle, SubmitError> {
1607 let prepared = match prepare_submission(runtime, program, &inputs) {
1608 Ok(prepared) => prepared,
1609 Err(source) => {
1610 return Err(SubmitError::PreAdmission {
1611 source: Box::new(source),
1612 inputs: Box::new(inputs),
1613 })
1614 }
1615 };
1616 let submission = Arc::new(InFlightSubmission::new(prepared, inputs));
1617 match spawn_in_flight(submission, spawner) {
1618 Ok(handle) => Ok(handle),
1619 Err(failure) => {
1620 let (inputs, source) = *failure;
1621 Err(SubmitError::PreAdmission {
1622 source: Box::new(source),
1623 inputs: Box::new(inputs),
1624 })
1625 }
1626 }
1627}
1628
1629pub(super) fn run_prepared(
1630 runtime: &Runtime,
1631 prepared: &PreparedCompiledGraph,
1632 inputs: &[&Tensor],
1633) -> Result<Vec<Tensor>> {
1634 validate_prepared_runtime(runtime, prepared, "Runtime::run_prepared")?;
1635 let inputs = resolve_input_refs(&prepared.program, inputs)?;
1636 execute_scheduled_reads(
1637 prepared.prepared.root().staging(),
1638 prepared.prepared.root().schedule(),
1639 prepared.prepared.root().regions(),
1640 prepared.prepared.root().region_counters(),
1641 prepared.prepared.operations(),
1642 &inputs,
1643 )
1644}
1645
1646fn prepare_submission(
1647 runtime: &Runtime,
1648 program: &CompiledGraph,
1649 inputs: &ExecutionInputs,
1650) -> Result<PreparedCompiledGraph> {
1651 let input_reads = inputs.as_reads()?;
1652 prepare_compiled_reads(runtime, program, &input_reads)
1653}
1654
1655fn prepare_compiled_reads(
1656 runtime: &Runtime,
1657 program: &CompiledGraph,
1658 inputs: &[TensorRead<'_>],
1659) -> Result<PreparedCompiledGraph> {
1660 validate_ordered_input_metadata_reads(program, inputs)?;
1661 let signature = input_signature_reads(inputs)?;
1662 let prepared = prepare(runtime, program, &signature)?;
1663 validate_prepared_epoch(runtime, prepared.root().epoch(), "Runtime::submit")?;
1664 Ok(PreparedCompiledGraph {
1665 runtime_id: runtime.id(),
1666 epoch: prepared.root().epoch(),
1667 program: program.clone(),
1668 prepared,
1669 })
1670}
1671
1672fn execute_admitted(
1673 prepared: &PreparedCompiledGraph,
1674 inputs: &[TensorRead<'_>],
1675) -> Result<Vec<Tensor>> {
1676 execute_scheduled_reads(
1677 prepared.prepared.root().staging(),
1678 prepared.prepared.root().schedule(),
1679 prepared.prepared.root().regions(),
1680 prepared.prepared.root().region_counters(),
1681 prepared.prepared.operations(),
1682 inputs,
1683 )
1684}
1685
1686pub(super) fn run_compiled_values(
1687 runtime: &Runtime,
1688 program: &CompiledGraph,
1689 inputs: &[&Tensor],
1690) -> Result<Vec<TensorValue>> {
1691 let inputs = resolve_input_refs(program, inputs)?;
1692 let signature = input_signature_reads(&inputs)?;
1693 let prepared = prepare(runtime, program, &signature)?;
1694 validate_prepared_epoch(
1695 runtime,
1696 prepared.root().epoch(),
1697 "Runtime::run_compiled_values",
1698 )?;
1699 execute_scheduled_value_reads(
1700 prepared.root().staging(),
1701 prepared.root().schedule(),
1702 prepared.operations(),
1703 &inputs,
1704 )
1705}
1706
1707fn validate_prepared_runtime(
1708 runtime: &Runtime,
1709 prepared: &PreparedCompiledGraph,
1710 caller: &'static str,
1711) -> Result<()> {
1712 if runtime.id() != prepared.runtime_id {
1713 return Err(Error::runtime_state(
1714 caller,
1715 ErrorPhase::Execution,
1716 "prepared compiled graph belongs to a different runtime",
1717 ));
1718 }
1719 let epoch = runtime
1720 .epoch()
1721 .map_err(|source| Error::runtime_state_source(caller, ErrorPhase::Execution, source))?;
1722 if epoch != prepared.epoch {
1723 return Err(Error::runtime_state(
1724 caller,
1725 ErrorPhase::Execution,
1726 format!(
1727 "prepared epoch {:?} does not match current epoch {:?}",
1728 prepared.epoch, epoch
1729 ),
1730 ));
1731 }
1732 Ok(())
1733}
1734
1735fn prepare(
1736 runtime: &Runtime,
1737 program: &CompiledGraph,
1738 signature: &InputSignature,
1739) -> Result<Arc<super::preparation::PreparedProgram>> {
1740 runtime
1741 .prepare_compiled_for(program, signature, &PrepareOptions::new())
1742 .map_err(prepare_error)
1743}
1744
1745fn validate_prepared_epoch(
1746 runtime: &Runtime,
1747 prepared_epoch: super::RuntimeEpoch,
1748 caller: &'static str,
1749) -> Result<()> {
1750 let epoch = runtime
1751 .epoch()
1752 .map_err(|source| Error::runtime_state_source(caller, ErrorPhase::Execution, source))?;
1753 if epoch != prepared_epoch {
1754 return Err(Error::runtime_state(
1755 caller,
1756 ErrorPhase::Execution,
1757 format!(
1758 "prepared epoch {:?} does not match current epoch {:?}",
1759 prepared_epoch, epoch
1760 ),
1761 ));
1762 }
1763 Ok(())
1764}
1765
1766fn execute_scheduled_reads(
1767 program: &ExecProgram,
1768 schedule: &ScheduledGraph,
1769 regions: &[super::region::ElementwiseRegion],
1770 region_counters: &super::region::RegionExecutionCounters,
1771 operations: &[PreparedOperationPlan],
1772 inputs: &[TensorRead<'_>],
1773) -> Result<Vec<Tensor>> {
1774 validate_exec_input_count(program, inputs.len())?;
1775 crate::exec::validate_exec_program(program, "scheduled tensor executor")?;
1776 let inputs = inputs.iter().cloned().map(ExecSlot::Read).collect();
1777 execute_scheduled_slots(
1778 program,
1779 schedule,
1780 regions,
1781 region_counters,
1782 operations,
1783 inputs,
1784 RuntimeOutputMode::Tensor,
1785 )
1786 .and_then(|mut slots| {
1787 collect_tensor_outputs_with(program, &mut slots, |location, slot| {
1788 location.witness().executor().materialize_slot(slot)
1789 })
1790 })
1791}
1792
1793fn execute_scheduled_value_reads(
1794 program: &ExecProgram,
1795 schedule: &ScheduledGraph,
1796 operations: &[PreparedOperationPlan],
1797 inputs: &[TensorRead<'_>],
1798) -> Result<Vec<TensorValue>> {
1799 validate_exec_input_count(program, inputs.len())?;
1800 crate::exec::validate_exec_program(program, "scheduled value executor")?;
1801 let inputs = inputs.iter().cloned().map(ExecSlot::Read).collect();
1802 execute_scheduled_slots(
1803 program,
1804 schedule,
1805 &[],
1806 &super::region::RegionExecutionCounters::default(),
1807 operations,
1808 inputs,
1809 RuntimeOutputMode::Value,
1810 )
1811 .and_then(|mut slots| {
1812 collect_value_outputs_with(program, &mut slots, |location, slot| {
1813 location.witness().executor().materialize_slot_value(slot)
1814 })
1815 })
1816}
1817
1818fn execute_scheduled_slots<'input>(
1819 program: &ExecProgram,
1820 schedule: &ScheduledGraph,
1821 regions: &[super::region::ElementwiseRegion],
1822 region_counters: &super::region::RegionExecutionCounters,
1823 operations: &[PreparedOperationPlan],
1824 inputs: Vec<ExecSlot<'input>>,
1825 output_mode: RuntimeOutputMode,
1826) -> Result<Vec<Option<LocatedExecSlot<'input>>>> {
1827 let terminal_slots = if matches!(output_mode, RuntimeOutputMode::Value) {
1828 crate::exec::terminal_output_slots(program)
1829 } else {
1830 Vec::new()
1831 };
1832 let mut staged = Vec::new();
1833 let mut located = (0..program.n_slots)
1834 .map(|_| Vec::new())
1835 .collect::<Vec<Vec<LocatedExecSlot<'input>>>>();
1836 let regions_applicable = matches!(output_mode, RuntimeOutputMode::Tensor);
1844 let mut region_starts: HashMap<usize, usize> = HashMap::new();
1845 let mut region_covered: HashSet<usize> = HashSet::new();
1846 if regions_applicable {
1847 for (region_index, region) in regions.iter().enumerate() {
1848 if let Some(&first) = region.node_indices.first() {
1849 region_starts.insert(first, region_index);
1850 }
1851 region_covered.extend(region.node_indices.iter().copied());
1852 }
1853 }
1854
1855 let mut event_domains = ScheduledEventDomains::new(schedule)?;
1856 let result = (|| {
1857 crate::exec::initialize_exec_slots_in(program, inputs, &mut staged)?;
1858 if schedule.input_locations().len() != program.input_slots.len() {
1859 return Err(Error::runtime_state(
1860 "Runtime::run_compiled",
1861 ErrorPhase::Execution,
1862 format!(
1863 "prepared schedule has {} input locations for {} inputs",
1864 schedule.input_locations().len(),
1865 program.input_slots.len()
1866 ),
1867 ));
1868 }
1869 for (&slot, location) in program.input_slots.iter().zip(schedule.input_locations()) {
1870 let value = staged
1871 .get_mut(slot)
1872 .and_then(Option::take)
1873 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
1874 validate_runtime_input_ingress(location, &value.as_read(), slot)?;
1875 located[slot].push(LocatedExecSlot {
1876 location: location.clone(),
1877 value,
1878 });
1879 }
1880 for (node_index, node) in schedule.nodes().iter().enumerate() {
1881 match node {
1882 ScheduledNode::Operation(operation_node) => {
1883 if let Some(®ion_index) = region_starts.get(&node_index) {
1884 let region = ®ions[region_index];
1885 let first_instruction = program
1886 .instructions
1887 .get(region.instruction_range.start)
1888 .ok_or_else(|| {
1889 Error::runtime_state(
1890 "Runtime::run_prepared",
1891 ErrorPhase::Execution,
1892 format!(
1893 "elementwise region references instruction {}, but the execution program has {} instructions",
1894 region.instruction_range.start,
1895 program.instructions.len()
1896 ),
1897 )
1898 })?;
1899 let operation = instruction_execution(schedule, first_instruction)?;
1900 if operation.location() != operation_node.location() {
1901 return Err(Error::runtime_state(
1902 "Runtime::run_prepared",
1903 ErrorPhase::Execution,
1904 "elementwise region location does not match its first prepared operation"
1905 .to_string(),
1906 ));
1907 }
1908 let location = operation.location().clone();
1909 let mut launch = || {
1910 for &slot in ®ion.input_slots {
1911 stage_slot_input(slot, &location, &mut located, &mut staged)?;
1912 }
1913 let applied = operation.executor().execute_elementwise_fusion_slots(
1914 ®ion.input_slots,
1915 region.instructions.len(),
1916 ®ion.plan,
1917 &mut staged,
1918 ®ion.output_slots,
1919 )?;
1920 if applied {
1921 region_counters.record_fused();
1922 validate_region_outputs(region, &location, &staged)?;
1923 return retain_region_results(
1924 region,
1925 &location,
1926 &mut located,
1927 &mut staged,
1928 );
1929 }
1930 region_counters.record_fallback();
1933 for (offset, instruction) in region.instructions.iter().enumerate() {
1934 let instruction_index = region.instruction_range.start + offset;
1935 let member = instruction_execution(schedule, instruction)?;
1936 if member.location() != operation_node.location() {
1937 return Err(Error::runtime_state(
1938 "Runtime::run_prepared",
1939 ErrorPhase::Execution,
1940 "elementwise region member location does not match the region location"
1941 .to_string(),
1942 ));
1943 }
1944 stage_instruction_inputs(
1945 instruction,
1946 member.location(),
1947 &mut located,
1948 &mut staged,
1949 )?;
1950 member.executor().execute_slot_instruction(
1951 instruction_index,
1952 instruction,
1953 operations,
1954 &mut staged,
1955 output_mode,
1956 &terminal_slots,
1957 )?;
1958 validate_instruction_outputs(
1959 instruction_index,
1960 instruction,
1961 member.location(),
1962 &staged,
1963 )?;
1964 retain_instruction_results(
1965 instruction,
1966 member.location(),
1967 &mut located,
1968 &mut staged,
1969 )?;
1970 }
1971 Ok(())
1972 };
1973 event_domains.enqueue(node_index, node, &mut launch)?;
1974 region_counters.record_submission();
1975 let completion = EventDependency::from_completion(node.completion());
1979 if let Some(token) = event_domains.completions.get(&completion).cloned() {
1980 for &covered in ®ion.node_indices {
1981 if covered == node_index {
1982 continue;
1983 }
1984 let covered_node = schedule.nodes().get(covered).ok_or_else(|| {
1985 Error::runtime_state(
1986 "Runtime::run_prepared",
1987 ErrorPhase::Execution,
1988 format!(
1989 "elementwise region references schedule node {covered}, but the schedule has {} nodes",
1990 schedule.nodes().len()
1991 ),
1992 )
1993 })?;
1994 event_domains.completions.insert(
1995 EventDependency::from_completion(covered_node.completion()),
1996 Arc::clone(&token),
1997 );
1998 }
1999 }
2000 continue;
2001 }
2002 if region_covered.contains(&node_index) {
2003 continue;
2005 }
2006 let mut launch = || {
2007 let instruction_index = operation_node.instruction_index();
2008 let instruction =
2009 program.instructions.get(instruction_index).ok_or_else(|| {
2010 Error::runtime_state(
2011 "Runtime::run_compiled",
2012 ErrorPhase::Execution,
2013 format!(
2014 "scheduled operation references instruction \
2015 {instruction_index}, but the execution program has {} \
2016 instructions",
2017 program.instructions.len()
2018 ),
2019 )
2020 })?;
2021 let operation = instruction_execution(schedule, instruction)?;
2022 if operation.location() != operation_node.location() {
2023 return Err(Error::runtime_state(
2024 "Runtime::run_compiled",
2025 ErrorPhase::Execution,
2026 format!(
2027 "scheduled instruction {instruction_index} location does not \
2028 match its prepared executor"
2029 ),
2030 ));
2031 }
2032 stage_instruction_inputs(
2033 instruction,
2034 operation.location(),
2035 &mut located,
2036 &mut staged,
2037 )?;
2038 operation.executor().execute_slot_instruction(
2039 instruction_index,
2040 instruction,
2041 operations,
2042 &mut staged,
2043 output_mode,
2044 &terminal_slots,
2045 )?;
2046 validate_instruction_outputs(
2047 instruction_index,
2048 instruction,
2049 operation.location(),
2050 &staged,
2051 )?;
2052 retain_instruction_results(
2053 instruction,
2054 operation.location(),
2055 &mut located,
2056 &mut staged,
2057 )
2058 };
2059 event_domains.enqueue(node_index, node, &mut launch)?;
2060 region_counters.record_submission();
2061 }
2062 ScheduledNode::Transfer(transfer) => {
2063 let mut launch = || execute_scheduled_transfer(transfer, &mut located);
2064 event_domains.enqueue(node_index, node, &mut launch)?;
2065 region_counters.record_submission();
2066 }
2067 ScheduledNode::Collective(_) => {
2068 return Err(Error::runtime_state_source(
2069 "Runtime::run_compiled",
2070 ErrorPhase::Execution,
2071 UnsupportedScheduledNodeError {
2072 node_index,
2073 node_kind: ScheduledNodeKind::Collective,
2074 },
2075 ));
2076 }
2077 ScheduledNode::Barrier(_) => {
2078 let mut launch = || Ok(());
2079 event_domains.enqueue(node_index, node, &mut launch)?;
2080 region_counters.record_submission();
2081 }
2082 }
2083 }
2084 Ok(())
2085 })();
2086 let drain = event_domains.drain();
2087 match (result, drain) {
2088 (Ok(()), Ok(())) => collect_located_outputs(program, &mut located),
2089 (Err(error), Ok(())) | (Ok(()), Err(error)) => {
2090 staged.clear();
2091 located.clear();
2092 Err(error)
2093 }
2094 (Err(primary), Err(cleanup)) => {
2095 staged.clear();
2096 located.clear();
2097 Err(scheduled_execution_cleanup_error(primary, cleanup))
2098 }
2099 }
2100}
2101
2102#[derive(Debug)]
2103pub(crate) struct ScheduledEventDomains {
2104 runs: Vec<RuntimeOwnedEventDomainRun>,
2105 completions: HashMap<EventDependency, Arc<dyn EventToken>>,
2106}
2107
2108#[derive(Debug, thiserror::Error)]
2109#[error(
2110 "scheduled node {node_index} depends on completion {dependency:?}, but no completion token was recorded"
2111)]
2112pub(crate) struct MissingScheduledDependencyCompletionError {
2113 pub(crate) dependency: EventDependency,
2114 pub(crate) node_index: usize,
2115}
2116
2117#[derive(Debug, thiserror::Error)]
2118#[error("scheduled transfer destination {destination:?} already contains value slot {value_slot}")]
2119pub(crate) struct DuplicateTransferDestinationError {
2120 pub(crate) value_slot: usize,
2121 pub(crate) destination: ExecutionLocation,
2122}
2123
2124impl ScheduledEventDomains {
2125 pub(crate) fn new(schedule: &ScheduledGraph) -> Result<Self> {
2126 schedule.preflight().map_err(|source| {
2127 Error::runtime_state_source("Runtime::run_compiled", ErrorPhase::Execution, source)
2128 })?;
2129 let mut drivers = Vec::new();
2130 let mut seen_domains = HashSet::new();
2131 for node in schedule.nodes() {
2132 let domain = node.completion().domain();
2133 if !seen_domains.insert(domain) {
2134 continue;
2135 }
2136 let witness = node.event_domain_witness();
2137 debug_assert_eq!(witness.event_domain_id(), domain);
2138 drivers.push((domain, witness.event_domain_driver().clone()));
2139 }
2140 let mut runs = Vec::with_capacity(drivers.len());
2141 for (domain, driver) in drivers {
2142 let run = RuntimeOwnedEventDomainRun::new(domain, driver.begin_run(domain)?);
2143 let actual = run.domain(EventDomainOperation::BeginRun)?;
2144 if actual != domain {
2145 return Err(event_domain_error(EventDomainError::RunDomainMismatch {
2146 operation: EventDomainOperation::BeginRun,
2147 node_index: None,
2148 expected: domain,
2149 actual,
2150 }));
2151 }
2152 runs.push(run);
2153 }
2154 Ok(Self {
2155 runs,
2156 completions: HashMap::new(),
2157 })
2158 }
2159
2160 #[cfg(test)]
2161 pub(crate) fn for_test(
2162 drivers: Vec<(super::EventDomainId, Arc<dyn super::EventDomainDriver>)>,
2163 ) -> Result<Self> {
2164 let mut runs = Vec::with_capacity(drivers.len());
2165 for (domain, driver) in drivers {
2166 let run = RuntimeOwnedEventDomainRun::new(domain, driver.begin_run(domain)?);
2167 let actual = run.domain(EventDomainOperation::BeginRun)?;
2168 if actual != domain {
2169 return Err(event_domain_error(EventDomainError::RunDomainMismatch {
2170 operation: EventDomainOperation::BeginRun,
2171 node_index: None,
2172 expected: domain,
2173 actual,
2174 }));
2175 }
2176 runs.push(run);
2177 }
2178 Ok(Self {
2179 runs,
2180 completions: HashMap::new(),
2181 })
2182 }
2183
2184 pub(crate) fn enqueue(
2185 &mut self,
2186 node_index: usize,
2187 node: &ScheduledNode,
2188 launch: &mut dyn FnMut() -> Result<()>,
2189 ) -> Result<()> {
2190 let completion = node.completion();
2191 let destination = completion.domain();
2192 let run_index = self.run_index(completion.domain())?;
2193 let actual_preflight_domain = self.runs[run_index].domain(EventDomainOperation::Enqueue)?;
2194 if actual_preflight_domain != destination {
2195 return Err(event_domain_error(EventDomainError::RunDomainMismatch {
2196 operation: EventDomainOperation::Enqueue,
2197 node_index: Some(node_index),
2198 expected: destination,
2199 actual: actual_preflight_domain,
2200 }));
2201 }
2202 let dependencies = self.classify_dependencies(node_index, node, destination)?;
2203 let actual_run_domain = self.runs[run_index].domain(EventDomainOperation::Enqueue)?;
2204 if actual_run_domain != destination {
2205 return Err(event_domain_error(EventDomainError::RunDomainMismatch {
2206 operation: EventDomainOperation::Enqueue,
2207 node_index: Some(node_index),
2208 expected: destination,
2209 actual: actual_run_domain,
2210 }));
2211 }
2212 let completion_event = self.runs[run_index].enqueue(&dependencies, launch)?;
2213 let actual = completion_event.origin();
2214 if actual != completion.domain() {
2215 return Err(event_domain_error(
2216 EventDomainError::CompletionTokenDomainMismatch {
2217 operation: EventDomainOperation::ValidateCompletion,
2218 node_index: Some(node_index),
2219 expected: completion.domain(),
2220 actual,
2221 },
2222 ));
2223 }
2224 self.completions.insert(
2225 EventDependency::from_completion(completion),
2226 completion_event,
2227 );
2228 Ok(())
2229 }
2230
2231 fn classify_dependencies(
2232 &self,
2233 node_index: usize,
2234 node: &ScheduledNode,
2235 destination: super::EventDomainId,
2236 ) -> Result<SmallVec<[Arc<dyn EventToken>; 4]>> {
2237 let mut admitted = SmallVec::with_capacity(node.dependencies().len());
2238 for dependency in node.dependencies() {
2239 let dependency_completion =
2240 self.completions.get(dependency).cloned().ok_or_else(|| {
2241 Error::runtime_state_source(
2242 "Runtime::run_compiled",
2243 ErrorPhase::Execution,
2244 MissingScheduledDependencyCompletionError {
2245 dependency: *dependency,
2246 node_index,
2247 },
2248 )
2249 })?;
2250 let actual = dependency_completion.origin();
2251 if actual != dependency.domain() {
2252 return Err(event_domain_error(
2253 EventDomainError::DependencyDomainMismatch {
2254 operation: match node {
2255 ScheduledNode::Transfer(_) => EventDomainOperation::TransferBridge,
2256 ScheduledNode::Operation(_)
2257 | ScheduledNode::Collective(_)
2258 | ScheduledNode::Barrier(_) => EventDomainOperation::Enqueue,
2259 },
2260 node_index: Some(node_index),
2261 expected: dependency.domain(),
2262 actual,
2263 },
2264 ));
2265 }
2266 match node {
2267 ScheduledNode::Transfer(transfer) => {
2268 let source = transfer.source_event_domain();
2269 if actual == destination {
2270 admitted.push(dependency_completion);
2271 } else if actual == source {
2272 dependency_completion.wait().map_err(|source_error| {
2273 event_domain_error(EventDomainError::DependencyWaitFailed {
2274 operation: EventDomainOperation::TransferBridge,
2275 node_index: Some(node_index),
2276 expected: destination,
2277 actual,
2278 source: Box::new(source_error),
2279 })
2280 })?;
2281 } else {
2282 return Err(event_domain_error(
2283 EventDomainError::DependencyDomainMismatch {
2284 operation: EventDomainOperation::TransferBridge,
2285 node_index: Some(node_index),
2286 expected: source,
2287 actual,
2288 },
2289 ));
2290 }
2291 }
2292 ScheduledNode::Operation(_)
2293 | ScheduledNode::Collective(_)
2294 | ScheduledNode::Barrier(_) => {
2295 if actual != destination {
2296 return Err(event_domain_error(
2297 EventDomainError::DependencyDomainMismatch {
2298 operation: EventDomainOperation::Enqueue,
2299 node_index: Some(node_index),
2300 expected: destination,
2301 actual,
2302 },
2303 ));
2304 }
2305 admitted.push(dependency_completion);
2306 }
2307 }
2308 }
2309 Ok(admitted)
2310 }
2311
2312 fn run_index(&self, domain: super::EventDomainId) -> Result<usize> {
2313 if let Some(index) = self
2314 .runs
2315 .iter()
2316 .position(|run| run.requested_domain() == domain)
2317 {
2318 return Ok(index);
2319 }
2320 Err(missing_event_domain_driver(domain))
2321 }
2322
2323 pub(crate) fn drain(&mut self) -> Result<()> {
2324 let mut failures = Vec::new();
2325 for run in &mut self.runs {
2326 if let Err(error) = run.drain() {
2327 failures.push(error);
2328 }
2329 }
2330 let mut failures = failures.into_iter();
2331 let Some(mut error) = failures.next() else {
2332 return Ok(());
2333 };
2334 for failure in failures {
2335 error = Error::with_suppressed(error, failure);
2336 }
2337 Err(error)
2338 }
2339}
2340
2341#[derive(Debug)]
2342enum RuntimeOwnedEventDomainRunState {
2343 Pending(Box<dyn EventDomainRun>),
2344 Retired,
2345 Failed,
2346}
2347
2348#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2349enum EventDomainRunTerminalState {
2350 Retired,
2351 Failed,
2352}
2353
2354impl fmt::Display for EventDomainRunTerminalState {
2355 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2356 match self {
2357 Self::Retired => formatter.write_str("retired"),
2358 Self::Failed => formatter.write_str("failed"),
2359 }
2360 }
2361}
2362
2363#[derive(Debug)]
2364struct RuntimeOwnedEventDomainRun {
2365 requested_domain: super::EventDomainId,
2366 state: RuntimeOwnedEventDomainRunState,
2367}
2368
2369impl RuntimeOwnedEventDomainRun {
2370 fn new(requested_domain: super::EventDomainId, inner: Box<dyn EventDomainRun>) -> Self {
2371 Self {
2372 requested_domain,
2373 state: RuntimeOwnedEventDomainRunState::Pending(inner),
2374 }
2375 }
2376
2377 fn requested_domain(&self) -> super::EventDomainId {
2378 self.requested_domain
2379 }
2380
2381 fn domain(&self, operation: EventDomainOperation) -> Result<super::EventDomainId> {
2382 match &self.state {
2383 RuntimeOwnedEventDomainRunState::Pending(run) => Ok(run.domain()),
2384 RuntimeOwnedEventDomainRunState::Retired => Err(event_domain_run_state_error(
2385 operation,
2386 self.requested_domain,
2387 EventDomainRunTerminalState::Retired,
2388 )),
2389 RuntimeOwnedEventDomainRunState::Failed => Err(event_domain_run_state_error(
2390 operation,
2391 self.requested_domain,
2392 EventDomainRunTerminalState::Failed,
2393 )),
2394 }
2395 }
2396
2397 fn enqueue(
2398 &mut self,
2399 dependencies: &[Arc<dyn EventToken>],
2400 launch: &mut dyn FnMut() -> Result<()>,
2401 ) -> Result<Arc<dyn EventToken>> {
2402 match &mut self.state {
2403 RuntimeOwnedEventDomainRunState::Pending(run) => run.enqueue(dependencies, launch),
2404 RuntimeOwnedEventDomainRunState::Retired => Err(event_domain_run_state_error(
2405 EventDomainOperation::Enqueue,
2406 self.requested_domain,
2407 EventDomainRunTerminalState::Retired,
2408 )),
2409 RuntimeOwnedEventDomainRunState::Failed => Err(event_domain_run_state_error(
2410 EventDomainOperation::Enqueue,
2411 self.requested_domain,
2412 EventDomainRunTerminalState::Failed,
2413 )),
2414 }
2415 }
2416
2417 fn drain(&mut self) -> Result<()> {
2418 let run = match std::mem::replace(&mut self.state, RuntimeOwnedEventDomainRunState::Failed)
2419 {
2420 RuntimeOwnedEventDomainRunState::Pending(run) => run,
2421 RuntimeOwnedEventDomainRunState::Retired => {
2422 self.state = RuntimeOwnedEventDomainRunState::Retired;
2423 return Err(event_domain_run_state_error(
2424 EventDomainOperation::Drain,
2425 self.requested_domain,
2426 EventDomainRunTerminalState::Retired,
2427 ));
2428 }
2429 RuntimeOwnedEventDomainRunState::Failed => {
2430 self.state = RuntimeOwnedEventDomainRunState::Failed;
2431 return Err(event_domain_run_state_error(
2432 EventDomainOperation::Drain,
2433 self.requested_domain,
2434 EventDomainRunTerminalState::Failed,
2435 ));
2436 }
2437 };
2438 let domain = self.requested_domain;
2439 let mut run = run;
2440 let drain_result = catch_unwind(AssertUnwindSafe(|| run.drain()));
2441 drop_event_domain_run(run);
2442 match drain_result {
2443 Ok(Ok(())) => {
2444 self.state = RuntimeOwnedEventDomainRunState::Retired;
2445 Ok(())
2446 }
2447 Ok(Err(error)) => {
2448 self.state = RuntimeOwnedEventDomainRunState::Failed;
2449 Err(error)
2450 }
2451 Err(payload) => {
2452 self.state = RuntimeOwnedEventDomainRunState::Failed;
2453 Err(event_domain_error(EventDomainError::DrainPanicked {
2454 operation: EventDomainOperation::Drain,
2455 domain,
2456 message: safe_event_domain_panic_message(payload),
2457 }))
2458 }
2459 }
2460 }
2461}
2462
2463#[derive(Debug, thiserror::Error)]
2466#[error("{operation} used event-domain run {domain:?} after it reached terminal state {state}")]
2467pub(crate) struct EventDomainRunLifecycleError {
2468 operation: EventDomainOperation,
2469 domain: super::EventDomainId,
2470 state: EventDomainRunTerminalState,
2471}
2472
2473fn event_domain_run_state_error(
2474 operation: EventDomainOperation,
2475 domain: super::EventDomainId,
2476 state: EventDomainRunTerminalState,
2477) -> Error {
2478 Error::runtime_state_source(
2479 "Runtime::run_compiled",
2480 ErrorPhase::Execution,
2481 EventDomainRunLifecycleError {
2482 operation,
2483 domain,
2484 state,
2485 },
2486 )
2487}
2488
2489fn drop_event_domain_run(run: Box<dyn EventDomainRun>) {
2490 if catch_unwind(AssertUnwindSafe(|| drop(run))).is_err() {
2491 }
2493}
2494
2495impl Drop for RuntimeOwnedEventDomainRun {
2496 fn drop(&mut self) {
2497 let state = std::mem::replace(&mut self.state, RuntimeOwnedEventDomainRunState::Failed);
2498 if let RuntimeOwnedEventDomainRunState::Pending(run) = state {
2499 drop_event_domain_run(run);
2500 }
2501 }
2502}
2503
2504fn missing_event_domain_driver(domain: super::EventDomainId) -> Error {
2505 Error::from(EventDomainError::MissingDriver { domain })
2506}
2507
2508fn event_domain_error(source: EventDomainError) -> Error {
2509 Error::from(source)
2510}
2511
2512fn safe_event_domain_panic_message(payload: Box<dyn std::any::Any + Send + 'static>) -> String {
2513 match payload.downcast::<&'static str>() {
2514 Ok(message) => (*message).to_owned(),
2515 Err(payload) => match payload.downcast::<String>() {
2516 Ok(message) => *message,
2517 Err(_) => "non-string panic payload".to_owned(),
2518 },
2519 }
2520}
2521
2522fn scheduled_execution_cleanup_error(primary: Error, cleanup: Error) -> Error {
2523 Error::with_suppressed(primary, cleanup)
2524}
2525
2526fn instruction_execution<'a>(
2527 schedule: &'a ScheduledGraph,
2528 instruction: &ExecInstruction,
2529) -> Result<InstructionExecution<'a>> {
2530 let Some(operation_index) = instruction.semantic_operation_index else {
2531 let location = schedule.root_location();
2532 return Ok(InstructionExecution {
2533 witness: location.witness(),
2534 location,
2535 });
2536 };
2537 let location = schedule
2538 .operation_locations()
2539 .get(operation_index)
2540 .ok_or_else(|| {
2541 Error::runtime_state(
2542 "Runtime::run_compiled",
2543 ErrorPhase::Execution,
2544 format!(
2545 "instruction references semantic operation {operation_index}, but prepared schedule has {} operations",
2546 schedule.operation_locations().len()
2547 ),
2548 )
2549 })?;
2550 Ok(InstructionExecution {
2551 witness: location.witness(),
2552 location,
2553 })
2554}
2555
2556struct InstructionExecution<'a> {
2557 witness: &'a super::snapshot::ExecutableEngineSnapshot,
2558 location: &'a ExecutionLocation,
2559}
2560
2561impl InstructionExecution<'_> {
2562 fn executor(&self) -> &Arc<dyn ErasedTensorBackendExecutor> {
2563 self.witness.executor()
2564 }
2565
2566 fn location(&self) -> &ExecutionLocation {
2567 self.location
2568 }
2569}
2570
2571pub(crate) struct LocatedExecSlot<'input> {
2572 pub(crate) location: ExecutionLocation,
2573 pub(crate) value: ExecSlot<'input>,
2574}
2575
2576fn stage_instruction_inputs<'input>(
2577 instruction: &ExecInstruction,
2578 location: &ExecutionLocation,
2579 located: &mut [Vec<LocatedExecSlot<'input>>],
2580 staged: &mut [Option<ExecSlot<'input>>],
2581) -> Result<()> {
2582 for &slot in &instruction.input_slots {
2583 stage_slot_input(slot, location, located, staged)?;
2584 }
2585 Ok(())
2586}
2587
2588fn stage_slot_input<'input>(
2590 slot: usize,
2591 location: &ExecutionLocation,
2592 located: &mut [Vec<LocatedExecSlot<'input>>],
2593 staged: &mut [Option<ExecSlot<'input>>],
2594) -> Result<()> {
2595 if staged
2596 .get(slot)
2597 .ok_or(tenferro_tensor::Error::MissingValue { slot })?
2598 .is_some()
2599 {
2600 return Ok(());
2601 }
2602 let values = located
2603 .get_mut(slot)
2604 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
2605 let value_index = values
2606 .iter()
2607 .position(|value| &value.location == location)
2608 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
2609 staged[slot] = Some(values.swap_remove(value_index).value);
2610 Ok(())
2611}
2612
2613fn validate_region_outputs(
2616 region: &super::region::ElementwiseRegion,
2617 location: &ExecutionLocation,
2618 staged: &[Option<ExecSlot<'_>>],
2619) -> Result<()> {
2620 for &output_slot in ®ion.output_slots {
2621 let output = staged
2622 .get(output_slot)
2623 .and_then(Option::as_ref)
2624 .ok_or(tenferro_tensor::Error::MissingValue { slot: output_slot })?;
2625 let output = output.as_read();
2626 if !location
2627 .witness()
2628 .owns_resident_tensor(&output, location.storage_class())
2629 {
2630 return Err(Error::runtime_state_source(
2631 "Runtime::run_prepared",
2632 ErrorPhase::Execution,
2633 super::EngineExecutionContractError::OutputResidencyMismatch {
2634 instruction_index: region.instruction_range.start,
2635 output_slot,
2636 engine_id: location.engine_id().clone(),
2637 storage_class: location.storage_class().clone(),
2638 backend_family: output.backend_family(),
2639 allocation_domain: output.allocation_domain(),
2640 },
2641 ));
2642 }
2643 }
2644 Ok(())
2645}
2646
2647fn retain_region_results<'input>(
2650 region: &super::region::ElementwiseRegion,
2651 location: &ExecutionLocation,
2652 located: &mut [Vec<LocatedExecSlot<'input>>],
2653 staged: &mut [Option<ExecSlot<'input>>],
2654) -> Result<()> {
2655 for (&slot, &last_use) in region.input_slots.iter().zip(region.input_last_use.iter()) {
2656 if last_use {
2657 located[slot].clear();
2658 staged[slot].take();
2659 }
2660 }
2661 for &slot in ®ion.output_slots {
2662 let value = staged
2663 .get_mut(slot)
2664 .and_then(Option::take)
2665 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
2666 let values = located
2667 .get_mut(slot)
2668 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
2669 values.clear();
2670 values.push(LocatedExecSlot {
2671 location: location.clone(),
2672 value,
2673 });
2674 }
2675 Ok(())
2676}
2677
2678fn validate_instruction_outputs(
2679 instruction_index: usize,
2680 instruction: &ExecInstruction,
2681 location: &ExecutionLocation,
2682 staged: &[Option<ExecSlot<'_>>],
2683) -> Result<()> {
2684 for &output_slot in &instruction.output_slots {
2685 let output = staged
2686 .get(output_slot)
2687 .and_then(Option::as_ref)
2688 .ok_or(tenferro_tensor::Error::MissingValue { slot: output_slot })?;
2689 let output = output.as_read();
2690 if !location
2691 .witness()
2692 .owns_resident_tensor(&output, location.storage_class())
2693 {
2694 return Err(Error::runtime_state_source(
2695 "Runtime::run_compiled",
2696 ErrorPhase::Execution,
2697 super::EngineExecutionContractError::OutputResidencyMismatch {
2698 instruction_index,
2699 output_slot,
2700 engine_id: location.engine_id().clone(),
2701 storage_class: location.storage_class().clone(),
2702 backend_family: output.backend_family(),
2703 allocation_domain: output.allocation_domain(),
2704 },
2705 ));
2706 }
2707 }
2708 Ok(())
2709}
2710
2711pub(crate) fn retain_instruction_results<'input>(
2712 instruction: &ExecInstruction,
2713 location: &ExecutionLocation,
2714 located: &mut [Vec<LocatedExecSlot<'input>>],
2715 staged: &mut [Option<ExecSlot<'input>>],
2716) -> Result<()> {
2717 for &slot in &instruction.input_slots {
2718 let is_output = instruction.output_slots.contains(&slot);
2719 let is_last_use = instruction
2720 .input_slots
2721 .iter()
2722 .enumerate()
2723 .any(|(index, &candidate)| {
2724 candidate == slot && instruction.last_use.get(index).copied().unwrap_or(false)
2725 });
2726 if is_last_use {
2727 located[slot].clear();
2728 if !is_output {
2729 staged[slot].take();
2730 }
2731 } else if !is_output && let Some(value) = staged[slot].take() {
2732 located[slot].push(LocatedExecSlot {
2733 location: location.clone(),
2734 value,
2735 });
2736 }
2737 }
2738
2739 for &slot in &instruction.output_slots {
2740 let value = staged
2741 .get_mut(slot)
2742 .and_then(Option::take)
2743 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
2744 let values = located
2745 .get_mut(slot)
2746 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
2747 values.clear();
2748 values.push(LocatedExecSlot {
2749 location: location.clone(),
2750 value,
2751 });
2752 }
2753 Ok(())
2754}
2755
2756fn validate_runtime_input_ingress(
2757 location: &ExecutionLocation,
2758 input: &TensorRead<'_>,
2759 slot: usize,
2760) -> Result<()> {
2761 let accepted = location
2762 .witness()
2763 .accepts_runtime_input(input, location.storage_class());
2764 if accepted {
2765 return Ok(());
2766 }
2767 Err(Error::runtime_state_source(
2768 "Runtime::run_compiled",
2769 ErrorPhase::Execution,
2770 super::InputIngressContractError::ResidencyMismatch {
2771 input_slot: slot,
2772 ingress_engine_id: location.engine_id().clone(),
2773 ingress_storage_class: location.storage_class().clone(),
2774 placement: input.placement().clone(),
2775 backend_family: input.backend_family(),
2776 allocation_domain: input.allocation_domain(),
2777 },
2778 ))
2779}
2780
2781fn execute_scheduled_transfer<'input>(
2782 transfer: &ScheduledTransfer,
2783 located: &mut [Vec<LocatedExecSlot<'input>>],
2784) -> Result<()> {
2785 let source = transfer.source_location();
2786 let destination = transfer.destination_location();
2787 let provider = transfer.provider();
2788 let values =
2789 located
2790 .get_mut(transfer.value_slot())
2791 .ok_or(tenferro_tensor::Error::MissingValue {
2792 slot: transfer.value_slot(),
2793 })?;
2794 if values.iter().any(|value| &value.location == destination) {
2795 return Err(Error::runtime_state_source(
2796 "Runtime::run_compiled",
2797 ErrorPhase::Execution,
2798 DuplicateTransferDestinationError {
2799 value_slot: transfer.value_slot(),
2800 destination: destination.clone(),
2801 },
2802 ));
2803 }
2804 let transferred = {
2805 let source_value = values
2806 .iter()
2807 .find(|value| &value.location == source)
2808 .ok_or(tenferro_tensor::Error::MissingValue {
2809 slot: transfer.value_slot(),
2810 })?;
2811 let source_read = source_value.value.as_read();
2812 let expected_dtype = source_read.dtype();
2813 let expected_shape = source_read.shape().to_vec();
2814 let transferred =
2815 provider.transfer_blocking(TransferRequest::new(source, destination, source_read))?;
2816 validate_transfer_output(destination, expected_dtype, &expected_shape, &transferred)?;
2817 transferred
2818 };
2819 values.push(LocatedExecSlot {
2820 location: destination.clone(),
2821 value: ExecSlot::Owned(transferred),
2822 });
2823 Ok(())
2824}
2825
2826fn validate_transfer_output(
2827 destination: &ExecutionLocation,
2828 expected_dtype: tenferro_tensor::DType,
2829 expected_shape: &[usize],
2830 output: &Tensor,
2831) -> Result<()> {
2832 let expected_elements = checked_transfer_element_count(expected_shape).map_err(|source| {
2833 Error::runtime_state_source(
2834 "Runtime::run_compiled",
2835 ErrorPhase::Execution,
2836 TransferError::ProviderContract { source },
2837 )
2838 })?;
2839 let contract_error = if output.dtype() != expected_dtype {
2840 Some(TransferProviderContractError::DTypeMismatch {
2841 expected: expected_dtype,
2842 actual: output.dtype(),
2843 })
2844 } else if output.shape() != expected_shape {
2845 Some(TransferProviderContractError::ShapeMismatch {
2846 expected: expected_shape.to_vec(),
2847 actual: output.shape().to_vec(),
2848 })
2849 } else if tensor_buffer_len(output) != expected_elements {
2850 Some(TransferProviderContractError::InvalidBufferLength {
2851 expected: expected_elements,
2852 actual: tensor_buffer_len(output),
2853 })
2854 } else if !destination
2855 .witness()
2856 .accepts_input_placement(output.placement(), destination.storage_class())
2857 {
2858 Some(
2859 TransferProviderContractError::DestinationPlacementMismatch {
2860 destination_engine_id: destination.engine_id().clone(),
2861 destination_storage_class: destination.storage_class().clone(),
2862 actual: output.placement().clone(),
2863 },
2864 )
2865 } else if !destination.witness().owns_resident_tensor(
2866 &TensorRead::from_tensor(output),
2867 destination.storage_class(),
2868 ) {
2869 Some(
2870 TransferProviderContractError::DestinationResidencyMismatch {
2871 destination_engine_id: destination.engine_id().clone(),
2872 destination_storage_class: destination.storage_class().clone(),
2873 actual_backend_family: TensorRead::from_tensor(output).backend_family(),
2874 actual_allocation_domain: TensorRead::from_tensor(output).allocation_domain(),
2875 },
2876 )
2877 } else {
2878 None
2879 };
2880 match contract_error {
2881 None => Ok(()),
2882 Some(source) => Err(Error::runtime_state_source(
2883 "Runtime::run_compiled",
2884 ErrorPhase::Execution,
2885 TransferError::ProviderContract { source },
2886 )),
2887 }
2888}
2889
2890fn checked_transfer_element_count(
2891 shape: &[usize],
2892) -> std::result::Result<usize, TransferProviderContractError> {
2893 tenferro_tensor::validate::checked_shape_product(
2894 "Runtime::run_compiled",
2895 "transfer source shape",
2896 shape,
2897 )
2898 .map_err(|source| TransferProviderContractError::LogicalElementCount { source })
2899}
2900
2901fn tensor_buffer_len(tensor: &Tensor) -> usize {
2902 match tensor.dtype() {
2903 DType::F32 => tensor.as_typed::<f32>().map_or(0, |t| t.buffer().len()),
2904 DType::F64 => tensor.as_typed::<f64>().map_or(0, |t| t.buffer().len()),
2905 DType::I32 => tensor.as_typed::<i32>().map_or(0, |t| t.buffer().len()),
2906 DType::I64 => tensor.as_typed::<i64>().map_or(0, |t| t.buffer().len()),
2907 DType::Bool => tensor.as_typed::<bool>().map_or(0, |t| t.buffer().len()),
2908 DType::C32 => tensor
2909 .as_typed::<Complex32>()
2910 .map_or(0, |t| t.buffer().len()),
2911 DType::C64 => tensor
2912 .as_typed::<Complex64>()
2913 .map_or(0, |t| t.buffer().len()),
2914 DType::External(_) => 0,
2918 }
2919}
2920
2921#[cfg(test)]
2922mod transfer_validation_tests {
2923 use std::error::Error as _;
2924
2925 use super::checked_transfer_element_count;
2926 use crate::TransferProviderContractError;
2927
2928 #[test]
2929 fn transfer_element_count_overflow_is_typed_and_preserves_source() {
2930 let error = checked_transfer_element_count(&[usize::MAX, 2]).unwrap_err();
2931
2932 assert!(matches!(
2933 error,
2934 TransferProviderContractError::LogicalElementCount { .. }
2935 ));
2936 assert!(error.source().is_some());
2937 }
2938}
2939
2940fn collect_located_outputs<'input>(
2941 program: &ExecProgram,
2942 located: &mut [Vec<LocatedExecSlot<'input>>],
2943) -> Result<Vec<Option<LocatedExecSlot<'input>>>> {
2944 let mut outputs = (0..program.n_slots).map(|_| None).collect::<Vec<_>>();
2945 for &slot in &program.output_slots {
2946 if outputs[slot].is_some() {
2947 continue;
2948 }
2949 let values = located
2950 .get_mut(slot)
2951 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
2952 let value = values
2953 .pop()
2954 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
2955 values.clear();
2956 outputs[slot] = Some(value);
2957 }
2958 located.iter_mut().for_each(Vec::clear);
2959 Ok(outputs)
2960}
2961
2962pub(super) fn collect_tensor_outputs_with<'input>(
2963 program: &ExecProgram,
2964 outputs: &mut [Option<LocatedExecSlot<'input>>],
2965 mut materialize: impl FnMut(&ExecutionLocation, ExecSlot<'input>) -> Result<Tensor>,
2966) -> Result<Vec<Tensor>> {
2967 program
2968 .output_slots
2969 .iter()
2970 .map(|&slot| {
2971 let located = outputs
2972 .get_mut(slot)
2973 .and_then(Option::take)
2974 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
2975 materialize(&located.location, located.value)
2976 })
2977 .collect()
2978}
2979
2980fn collect_value_outputs_with<'input>(
2981 program: &ExecProgram,
2982 outputs: &mut [Option<LocatedExecSlot<'input>>],
2983 mut materialize: impl FnMut(&ExecutionLocation, ExecSlot<'input>) -> Result<TensorValue>,
2984) -> Result<Vec<TensorValue>> {
2985 program
2986 .output_slots
2987 .iter()
2988 .map(|&slot| {
2989 let located = outputs
2990 .get_mut(slot)
2991 .and_then(Option::take)
2992 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
2993 materialize(&located.location, located.value)
2994 })
2995 .collect()
2996}
2997
2998fn input_signature_reads(inputs: &[TensorRead<'_>]) -> Result<InputSignature> {
2999 InputSignature::from_reads(inputs).map_err(|source| prepare_error(Arc::new(source)))
3000}
3001
3002fn resolve_input_refs<'a>(
3003 program: &'a CompiledGraph,
3004 inputs: &'a [&'a Tensor],
3005) -> Result<RuntimeInputReads<'a>> {
3006 let expected = program.program().inputs().len();
3007 if inputs.len() > expected {
3008 return Err(Error::GraphInputCountMismatch {
3009 expected,
3010 actual: inputs.len(),
3011 });
3012 }
3013 let resolved = if inputs.is_empty() {
3014 semantic_default_inputs(program)?
3015 } else if inputs.len() == expected {
3016 inputs
3017 .iter()
3018 .map(|tensor| TensorRead::from_tensor(tensor))
3019 .collect()
3020 } else {
3021 let mut explicit = inputs.iter();
3022 let mut resolved = RuntimeInputReads::new();
3023 for value in program.program().inputs() {
3024 if let Some(retained) = program.bindings().tensor_ref_for_input(*value) {
3025 resolved.push(retained.tensor_read()?);
3026 } else {
3027 let tensor = explicit.next().ok_or_else(|| {
3028 Error::invalid_argument(
3029 "Runtime::resolve_input_refs",
3030 ErrorPhase::Execution,
3031 "inputs",
3032 format!(
3033 "expected {expected} inputs, or one explicit input for each unbound placeholder"
3034 ),
3035 )
3036 })?;
3037 resolved.push(TensorRead::from_tensor(tensor));
3038 }
3039 }
3040 if explicit.next().is_some() {
3041 return Err(Error::invalid_argument(
3042 "Runtime::resolve_input_refs",
3043 ErrorPhase::Execution,
3044 "inputs",
3045 format!("expected {expected} inputs, received {}", inputs.len()),
3046 ));
3047 }
3048 resolved
3049 };
3050 validate_ordered_input_metadata_reads(program, &resolved)?;
3051 Ok(resolved)
3052}
3053
3054fn semantic_default_inputs(program: &CompiledGraph) -> Result<RuntimeInputReads<'_>> {
3055 program
3056 .program()
3057 .inputs()
3058 .iter()
3059 .enumerate()
3060 .map(|(input_index, value)| {
3061 let retained = program
3062 .bindings()
3063 .tensor_ref_for_input(*value)
3064 .ok_or_else(|| Error::UnboundPlaceholder {
3065 input_key: format!("semantic input {input_index}"),
3066 })?;
3067 retained.tensor_read()
3068 })
3069 .collect()
3070}
3071
3072fn validate_ordered_input_metadata_reads(
3073 program: &CompiledGraph,
3074 inputs: &[TensorRead<'_>],
3075) -> Result<()> {
3076 let actuals = inputs
3077 .iter()
3078 .map(|input| (input.dtype(), input.shape().to_vec()))
3079 .collect::<Vec<_>>();
3080 validate_ordered_input_metadata_values(program, &actuals, "Runtime::submit")
3081}
3082
3083fn validate_ordered_input_metadata_values(
3084 program: &CompiledGraph,
3085 actuals: &[(tenferro_tensor::DType, Vec<usize>)],
3086 caller: &'static str,
3087) -> Result<()> {
3088 let expected = program.input_count();
3089 if actuals.len() != expected {
3090 return Err(Error::GraphInputCountMismatch {
3091 expected,
3092 actual: actuals.len(),
3093 });
3094 }
3095 let input_shapes: RuntimeInputShapes<'_> =
3096 actuals.iter().map(|(_, shape)| shape.as_slice()).collect();
3097 for (input_value, (actual_dtype, actual_shape)) in
3098 program.program().inputs().iter().zip(actuals)
3099 {
3100 let metadata = program
3101 .program()
3102 .value_metadata(*input_value)
3103 .map_err(|source| Error::runtime_state_source(caller, ErrorPhase::Execution, source))?;
3104 if metadata.dtype() != *actual_dtype {
3105 return Err(Error::PlaceholderDtypeMismatch {
3106 expected: metadata.dtype(),
3107 actual: *actual_dtype,
3108 });
3109 }
3110 if metadata.shape().len() != actual_shape.len() {
3111 return Err(Error::PlaceholderRankMismatch {
3112 expected: metadata.shape().len(),
3113 actual: actual_shape.len(),
3114 });
3115 }
3116 let mut expected_shape: RuntimeShapeScratch = actual_shape.iter().copied().collect();
3117 let mut exact_mismatch = false;
3118 for (axis, (extent, actual_size)) in metadata.shape().iter().zip(actual_shape).enumerate() {
3119 match extent {
3120 ShapeExtent::Exact(expression) => {
3121 let expected = expression.eval(&input_shapes).map_err(|source| {
3122 Error::runtime_state_source(caller, ErrorPhase::Execution, source)
3123 })?;
3124 expected_shape[axis] = expected;
3125 exact_mismatch |= expected != *actual_size;
3126 }
3127 ShapeExtent::UpperBound(expression) => {
3128 let bound = expression.eval(&input_shapes).map_err(|source| {
3129 Error::runtime_state_source(caller, ErrorPhase::Execution, source)
3130 })?;
3131 if *actual_size > bound {
3132 return Err(Error::PlaceholderShapeBoundExceeded {
3133 axis,
3134 bound,
3135 actual: *actual_size,
3136 });
3137 }
3138 }
3139 ShapeExtent::Unknown => {}
3140 }
3141 }
3142 if exact_mismatch {
3143 return Err(Error::PlaceholderShapeMismatch {
3144 expected: expected_shape.into_vec(),
3145 actual: actual_shape.clone(),
3146 });
3147 }
3148 }
3149 Ok(())
3150}
3151
3152fn validate_exec_input_count(program: &ExecProgram, actual: usize) -> Result<()> {
3153 let expected = program.input_slots.len();
3154 if actual != expected {
3155 return Err(Error::runtime_state(
3156 "Runtime::run_compiled",
3157 ErrorPhase::Execution,
3158 format!("expected {expected} inputs for execution program, got {actual}"),
3159 ));
3160 }
3161 Ok(())
3162}
3163
3164fn prepare_error(source: Arc<PrepareError>) -> Error {
3165 Error::runtime_state_source(
3166 "Runtime::run_compiled",
3167 ErrorPhase::Execution,
3168 SharedPrepareError(source),
3169 )
3170}
3171
3172#[derive(Clone, Debug)]
3173struct SharedPrepareError(Arc<PrepareError>);
3174
3175impl fmt::Display for SharedPrepareError {
3176 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3177 write!(formatter, "{}", self.0)
3178 }
3179}
3180
3181impl StdError for SharedPrepareError {
3182 fn source(&self) -> Option<&(dyn StdError + 'static)> {
3183 Some(self.0.as_ref())
3184 }
3185}
3186
3187#[cfg(test)]
3188mod tests {
3189 use std::any::Any;
3190 use std::error::Error as StdError;
3191 use std::hash::Hasher;
3192 use std::num::NonZeroU64;
3193 use std::sync::atomic::{AtomicBool, Ordering};
3194 use std::sync::{Arc, TryLockError, Weak};
3195
3196 use tenferro_cpu::CpuBackend;
3197 use tenferro_ops::dim_expr::DimExpr;
3198 use tenferro_ops::ext_op::ExtensionOp;
3199 use tenferro_ops::SymDim;
3200 use tenferro_tensor::{
3201 BackendSession, BackendSessionHost, DType, Tensor, TensorBackend, TensorRead, TypedTensor,
3202 };
3203
3204 use crate::exec::{ExecInstruction, ExecOp, ExecProgram, ExecSlot};
3205 use crate::runtime::{
3206 CoreCapabilityBundle, EngineId, ErasedExecutionContext, EventDomainDriver, EventDomainId,
3207 ExecutableEngineContract, ExecutionContextIdentity, HardwareClassId, InputIngressContract,
3208 InputSignature, PreparedOperation, PreparedOperationBinding, PreparedOperationExecutor,
3209 PreparedOperationPlan, ProviderDeviceIdentity, ProviderId, RegistrationIdentity,
3210 RuntimeCacheOwner, RuntimeEpoch, RuntimeId, SpecializationProjection,
3211 SpecializationRequirements, StorageClass,
3212 };
3213 use crate::{Error, ErrorPhase, ExtensionCacheStore, Result};
3214
3215 use super::{
3216 DuplicateTransferDestinationError, ErasedTensorBackendExecutor, LocatedExecSlot,
3217 TensorBackendExecutor,
3218 };
3219 use crate::runtime::schedule::{
3220 EventCompletion, EventSlotId, ExecutionLocation, ScheduledTransfer,
3221 };
3222
3223 const LOCK_PROBE_FAMILY: &str = "runtime.lock-probe.v1";
3224 const REENTRANT_PROBE_FAMILY: &str = "runtime.reentrant-probe.v1";
3225
3226 #[derive(Clone, Debug)]
3227 struct LockProbeOp;
3228
3229 impl ExtensionOp for LockProbeOp {
3230 fn family_id(&self) -> &'static str {
3231 LOCK_PROBE_FAMILY
3232 }
3233
3234 fn payload_hash(&self, _hasher: &mut dyn Hasher) {}
3235
3236 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
3237 other.as_any().downcast_ref::<Self>().is_some()
3238 }
3239
3240 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
3241 Arc::new(self.clone())
3242 }
3243
3244 fn as_any(&self) -> &dyn Any {
3245 self
3246 }
3247
3248 fn input_count(&self) -> usize {
3249 1
3250 }
3251
3252 fn output_count(&self) -> usize {
3253 1
3254 }
3255
3256 fn infer_output_meta(
3257 &self,
3258 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
3259 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
3260 Ok(vec![(ctx.input_dtype(0)?, ctx.input_shape(0)?.to_vec())])
3261 }
3262 }
3263
3264 #[derive(Clone, Debug)]
3265 struct ReentrantProbeOp;
3266
3267 impl ExtensionOp for ReentrantProbeOp {
3268 fn family_id(&self) -> &'static str {
3269 REENTRANT_PROBE_FAMILY
3270 }
3271
3272 fn payload_hash(&self, _hasher: &mut dyn Hasher) {}
3273
3274 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
3275 other.as_any().downcast_ref::<Self>().is_some()
3276 }
3277
3278 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
3279 Arc::new(self.clone())
3280 }
3281
3282 fn as_any(&self) -> &dyn Any {
3283 self
3284 }
3285
3286 fn input_count(&self) -> usize {
3287 1
3288 }
3289
3290 fn output_count(&self) -> usize {
3291 1
3292 }
3293
3294 fn infer_output_meta(
3295 &self,
3296 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
3297 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
3298 Ok(vec![(ctx.input_dtype(0)?, ctx.input_shape(0)?.to_vec())])
3299 }
3300 }
3301
3302 #[derive(Debug)]
3303 struct LockProbePreparedOperation {
3304 binding: PreparedOperationBinding,
3305 specialization: SpecializationProjection,
3306 executor: Weak<TensorBackendExecutor<CpuBackend>>,
3307 observed_unlocked_state: Arc<AtomicBool>,
3308 }
3309
3310 impl PreparedOperation for LockProbePreparedOperation {
3311 fn binding(&self) -> &PreparedOperationBinding {
3312 &self.binding
3313 }
3314
3315 fn specialization(&self) -> &SpecializationProjection {
3316 &self.specialization
3317 }
3318
3319 fn retained_bytes(&self) -> usize {
3320 0
3321 }
3322 }
3323
3324 impl PreparedOperationExecutor for LockProbePreparedOperation {
3325 fn execute(
3326 &self,
3327 context: &mut ErasedExecutionContext<'_>,
3328 _extension_caches: &mut ExtensionCacheStore,
3329 inputs: &[TensorRead<'_>],
3330 ) -> Result<Vec<Tensor>> {
3331 let executor = self.executor.upgrade().expect("executor still alive");
3332 let unlocked = match executor.state.try_lock() {
3333 Ok(_guard) => true,
3334 Err(TryLockError::WouldBlock) => false,
3335 Err(TryLockError::Poisoned(_)) => false,
3336 };
3337 self.observed_unlocked_state
3338 .store(unlocked, Ordering::SeqCst);
3339 let backend = context
3340 .downcast_mut::<CpuBackend>(self.binding.context_identity())
3341 .map_err(|source| {
3342 Error::runtime_state_source("lock_probe", ErrorPhase::Execution, source)
3343 })?;
3344 Ok(vec![backend.with_backend_session(|exec| {
3345 exec.to_contiguous_read(inputs[0].clone())
3346 })?])
3347 }
3348 }
3349
3350 #[derive(Debug)]
3351 struct ReentrantProbePreparedOperation {
3352 binding: PreparedOperationBinding,
3353 specialization: SpecializationProjection,
3354 executor: Weak<TensorBackendExecutor<CpuBackend>>,
3355 observed_reentrant_error: Arc<AtomicBool>,
3356 }
3357
3358 #[derive(Debug)]
3359 struct SessionProbePreparedOperation {
3360 binding: PreparedOperationBinding,
3361 specialization: SpecializationProjection,
3362 observed_session: Arc<AtomicBool>,
3363 }
3364
3365 impl PreparedOperation for SessionProbePreparedOperation {
3366 fn binding(&self) -> &PreparedOperationBinding {
3367 &self.binding
3368 }
3369
3370 fn specialization(&self) -> &SpecializationProjection {
3371 &self.specialization
3372 }
3373
3374 fn retained_bytes(&self) -> usize {
3375 0
3376 }
3377 }
3378
3379 impl PreparedOperationExecutor for SessionProbePreparedOperation {
3380 fn execute(
3381 &self,
3382 _context: &mut ErasedExecutionContext<'_>,
3383 _extension_caches: &mut ExtensionCacheStore,
3384 _inputs: &[TensorRead<'_>],
3385 ) -> Result<Vec<Tensor>> {
3386 Err(Error::unsupported(
3387 "session_probe",
3388 ErrorPhase::Execution,
3389 "session probe must use the scheduler-owned session path",
3390 ))
3391 }
3392
3393 fn supports_session(&self) -> bool {
3394 true
3395 }
3396
3397 fn execute_in_session(
3398 &self,
3399 session: &mut dyn BackendSession,
3400 _extension_caches: &mut ExtensionCacheStore,
3401 inputs: &[TensorRead<'_>],
3402 ) -> Result<Vec<Tensor>> {
3403 self.observed_session.store(true, Ordering::SeqCst);
3404 Ok(vec![session.to_contiguous_read(inputs[0].clone())?])
3405 }
3406 }
3407
3408 impl ReentrantProbePreparedOperation {
3409 fn probe_reentrant_call(&self, input: Tensor) {
3410 let executor = self.executor.upgrade().expect("executor still alive");
3411 let nested = ErasedTensorBackendExecutor::execute(
3412 executor.as_ref(),
3413 &passthrough_program(),
3414 &[],
3415 vec![input],
3416 );
3417 let observed = nested.is_err_and(|error| {
3418 error
3419 .to_string()
3420 .contains("reentrant tensor backend executor call would deadlock")
3421 });
3422 self.observed_reentrant_error
3423 .store(observed, Ordering::SeqCst);
3424 }
3425 }
3426
3427 impl PreparedOperation for ReentrantProbePreparedOperation {
3428 fn binding(&self) -> &PreparedOperationBinding {
3429 &self.binding
3430 }
3431
3432 fn specialization(&self) -> &SpecializationProjection {
3433 &self.specialization
3434 }
3435
3436 fn retained_bytes(&self) -> usize {
3437 0
3438 }
3439 }
3440
3441 impl PreparedOperationExecutor for ReentrantProbePreparedOperation {
3442 fn execute(
3443 &self,
3444 context: &mut ErasedExecutionContext<'_>,
3445 _extension_caches: &mut ExtensionCacheStore,
3446 inputs: &[TensorRead<'_>],
3447 ) -> Result<Vec<Tensor>> {
3448 let backend = context
3449 .downcast_mut::<CpuBackend>(self.binding.context_identity())
3450 .map_err(|source| {
3451 Error::runtime_state_source("reentrant_probe", ErrorPhase::Execution, source)
3452 })?;
3453 let materialized =
3454 backend.with_backend_session(|exec| exec.to_contiguous_read(inputs[0].clone()))?;
3455 self.probe_reentrant_call(materialized.duplicate()?);
3456 Ok(vec![materialized])
3457 }
3458 }
3459
3460 fn lock_probe_program() -> ExecProgram {
3461 ExecProgram {
3462 instructions: vec![ExecInstruction {
3463 op: ExecOp::Extension(Arc::new(LockProbeOp)),
3464 semantic_operation_index: Some(0),
3465 input_slots: vec![0],
3466 output_slots: vec![1],
3467 dtype: DType::F64,
3468 output_shapes: vec![vec![DimExpr::InputDim {
3469 input_idx: 0,
3470 axis: 0,
3471 }]]
3472 .into(),
3473 output_extents: vec![vec![]].into(),
3474 last_use: vec![false],
3475 }],
3476 input_slots: vec![0],
3477 output_slots: vec![1],
3478 n_slots: 2,
3479 shape_guards: vec![],
3480 }
3481 }
3482
3483 fn partial_session_probe_program() -> ExecProgram {
3484 let output_shape = || {
3485 vec![DimExpr::InputDim {
3486 input_idx: 0,
3487 axis: 0,
3488 }]
3489 };
3490 ExecProgram {
3491 instructions: vec![
3492 ExecInstruction {
3493 op: ExecOp::Extension(Arc::new(LockProbeOp)),
3494 semantic_operation_index: Some(0),
3495 input_slots: vec![0],
3496 output_slots: vec![1],
3497 dtype: DType::F64,
3498 output_shapes: vec![output_shape()].into(),
3499 output_extents: vec![vec![]].into(),
3500 last_use: vec![false],
3501 },
3502 ExecInstruction {
3503 op: ExecOp::Extension(Arc::new(LockProbeOp)),
3504 semantic_operation_index: Some(1),
3505 input_slots: vec![1],
3506 output_slots: vec![2],
3507 dtype: DType::F64,
3508 output_shapes: vec![output_shape()].into(),
3509 output_extents: vec![vec![]].into(),
3510 last_use: vec![false],
3511 },
3512 ],
3513 input_slots: vec![0],
3514 output_slots: vec![2],
3515 n_slots: 3,
3516 shape_guards: vec![],
3517 }
3518 }
3519
3520 fn reentrant_probe_program() -> ExecProgram {
3521 ExecProgram {
3522 instructions: vec![ExecInstruction {
3523 op: ExecOp::Extension(Arc::new(ReentrantProbeOp)),
3524 semantic_operation_index: Some(0),
3525 input_slots: vec![0],
3526 output_slots: vec![1],
3527 dtype: DType::F64,
3528 output_shapes: vec![vec![DimExpr::InputDim {
3529 input_idx: 0,
3530 axis: 0,
3531 }]]
3532 .into(),
3533 output_extents: vec![vec![]].into(),
3534 last_use: vec![false],
3535 }],
3536 input_slots: vec![0],
3537 output_slots: vec![1],
3538 n_slots: 2,
3539 shape_guards: vec![],
3540 }
3541 }
3542
3543 fn passthrough_program() -> ExecProgram {
3544 ExecProgram {
3545 instructions: vec![],
3546 input_slots: vec![0],
3547 output_slots: vec![0],
3548 n_slots: 1,
3549 shape_guards: vec![],
3550 }
3551 }
3552
3553 fn f64_zeros(shape: Vec<usize>) -> Tensor {
3554 Tensor::from_typed::<f64>(TypedTensor::zeros(shape).unwrap())
3555 }
3556
3557 fn nz(value: u64) -> NonZeroU64 {
3558 NonZeroU64::new(value).unwrap_or(NonZeroU64::MIN)
3559 }
3560
3561 fn probe_binding() -> PreparedOperationBinding {
3562 PreparedOperationBinding::new(
3563 RuntimeId::from_nonzero(nz(1)),
3564 RuntimeEpoch::from_nonzero(nz(2)),
3565 EngineId::new("tenferro.cpu").unwrap(),
3566 RegistrationIdentity::new(nz(3), nz(4)),
3567 ExecutionContextIdentity::of::<CpuBackend>(),
3568 HardwareClassId::new("tenferro.cpu.host").unwrap(),
3569 )
3570 }
3571
3572 fn probe_specialization() -> SpecializationProjection {
3573 SpecializationRequirements::polymorphic(0)
3574 .project(&InputSignature::new(Vec::new()))
3575 .unwrap()
3576 }
3577
3578 #[test]
3579 fn tensor_backend_executor_releases_state_lock_during_extension_execution() {
3580 let executor = Arc::new(TensorBackendExecutor::<CpuBackend>::new(CpuBackend::new()));
3581 let observed_unlocked_state = Arc::new(AtomicBool::new(false));
3582 let prepared = Arc::new(LockProbePreparedOperation {
3583 binding: probe_binding(),
3584 specialization: probe_specialization(),
3585 executor: Arc::downgrade(&executor),
3586 observed_unlocked_state: Arc::clone(&observed_unlocked_state),
3587 });
3588 let operations = vec![PreparedOperationPlan::executable(
3589 prepared.clone(),
3590 prepared,
3591 )];
3592
3593 let output = ErasedTensorBackendExecutor::execute(
3594 executor.as_ref(),
3595 &lock_probe_program(),
3596 &operations,
3597 vec![f64_zeros(vec![2])],
3598 )
3599 .expect("extension executes");
3600
3601 assert_eq!(output[0].shape(), &[2]);
3602 assert!(
3603 observed_unlocked_state.load(Ordering::SeqCst),
3604 "executor state lock must not be held while extension runtime callbacks execute"
3605 );
3606 }
3607
3608 #[test]
3609 fn scheduled_execution_cleanup_error_preserves_primary_error() {
3610 let primary = Error::runtime_state(
3611 "primary-execution",
3612 ErrorPhase::Execution,
3613 "primary execution failure",
3614 );
3615 let cleanup = Error::runtime_state(
3616 "event-domain-cleanup",
3617 ErrorPhase::Execution,
3618 "cleanup failure",
3619 );
3620 let primary_display = primary.to_string();
3621
3622 let combined = super::scheduled_execution_cleanup_error(primary, cleanup);
3623 let primary_error = combined.primary().expect("primary execution error");
3624 let cleanup_error = combined.suppressed().expect("cleanup error");
3625 assert_eq!(primary_error.to_string(), primary_display);
3626 assert_eq!(
3627 cleanup_error.to_string(),
3628 "event-domain-cleanup (Execution): runtime state failure: cleanup failure"
3629 );
3630 assert_eq!(
3631 StdError::source(&combined)
3632 .expect("primary error in the standard source chain")
3633 .to_string(),
3634 primary_display
3635 );
3636 }
3637
3638 #[test]
3639 fn duplicate_scheduled_transfer_destination_reports_typed_fields() {
3640 let domain = EventDomainId::runtime_created_for_test(
3641 RuntimeId::from_nonzero(nz(1)),
3642 RuntimeEpoch::from_nonzero(nz(1)),
3643 RegistrationIdentity::new(nz(1), nz(1)),
3644 );
3645 let source = ExecutionLocation::new(
3646 EngineId::new("tenferro-test.transfer-source").expect("source engine"),
3647 ProviderDeviceIdentity::new(
3648 ProviderId::new("tenferro-test.transfer").expect("provider id"),
3649 "source",
3650 )
3651 .expect("source provider target"),
3652 domain,
3653 StorageClass::new("tenferro-test.transfer-source").expect("source storage"),
3654 );
3655 let destination = ExecutionLocation::new(
3656 EngineId::new("tenferro-test.transfer-destination").expect("destination engine"),
3657 ProviderDeviceIdentity::new(
3658 ProviderId::new("tenferro-test.transfer").expect("provider id"),
3659 "destination",
3660 )
3661 .expect("destination provider target"),
3662 domain,
3663 StorageClass::new("tenferro-test.transfer-destination").expect("destination storage"),
3664 );
3665 let transfer = ScheduledTransfer::new(
3666 3,
3667 source,
3668 destination.clone(),
3669 [],
3670 EventCompletion::new(domain, EventSlotId::new(0), 0),
3671 );
3672 let mut located = vec![
3673 Vec::new(),
3674 Vec::new(),
3675 Vec::new(),
3676 vec![LocatedExecSlot {
3677 location: destination.clone(),
3678 value: ExecSlot::Owned(f64_zeros(vec![1])),
3679 }],
3680 ];
3681
3682 let error = super::execute_scheduled_transfer(&transfer, &mut located)
3683 .expect_err("duplicate transfer destination");
3684 let Error::RuntimeStateSource { source, .. } = error else {
3685 panic!("duplicate transfer destination must retain a typed source");
3686 };
3687 let duplicate = source
3688 .downcast_ref::<DuplicateTransferDestinationError>()
3689 .expect("typed duplicate transfer destination source");
3690 assert_eq!(duplicate.value_slot, 3);
3691 assert_eq!(duplicate.destination, destination);
3692 }
3693
3694 #[test]
3695 fn tensor_backend_executor_reentrant_call_returns_error_instead_of_deadlocking() {
3696 let executor = Arc::new(TensorBackendExecutor::<CpuBackend>::new(CpuBackend::new()));
3697 let observed_reentrant_error = Arc::new(AtomicBool::new(false));
3698 let prepared = Arc::new(ReentrantProbePreparedOperation {
3699 binding: probe_binding(),
3700 specialization: probe_specialization(),
3701 executor: Arc::downgrade(&executor),
3702 observed_reentrant_error: Arc::clone(&observed_reentrant_error),
3703 });
3704 let operations = vec![PreparedOperationPlan::executable(
3705 prepared.clone(),
3706 prepared,
3707 )];
3708
3709 let output = ErasedTensorBackendExecutor::execute(
3710 executor.as_ref(),
3711 &reentrant_probe_program(),
3712 &operations,
3713 vec![f64_zeros(vec![2])],
3714 )
3715 .expect("outer extension executes");
3716
3717 assert_eq!(output[0].shape(), &[2]);
3718 assert!(
3719 observed_reentrant_error.load(Ordering::SeqCst),
3720 "same-thread reentrant executor call must fail immediately instead of deadlocking"
3721 );
3722 }
3723
3724 #[test]
3725 fn tensor_backend_executor_dispatches_session_capable_extension_in_one_session_path() {
3726 let executor = TensorBackendExecutor::<CpuBackend>::new(CpuBackend::new());
3727 let observed_session = Arc::new(AtomicBool::new(false));
3728 let prepared = Arc::new(SessionProbePreparedOperation {
3729 binding: probe_binding(),
3730 specialization: probe_specialization(),
3731 observed_session: Arc::clone(&observed_session),
3732 });
3733 let operations = vec![PreparedOperationPlan::executable(
3734 prepared.clone(),
3735 prepared,
3736 )];
3737
3738 let output = ErasedTensorBackendExecutor::execute(
3739 &executor,
3740 &lock_probe_program(),
3741 &operations,
3742 vec![f64_zeros(vec![2])],
3743 )
3744 .expect("session-capable extension executes");
3745
3746 assert_eq!(output[0].shape(), &[2]);
3747 assert!(observed_session.load(Ordering::SeqCst));
3748 }
3749
3750 #[test]
3751 fn tensor_backend_executor_batches_session_capable_region_after_boundary() {
3752 let executor = Arc::new(TensorBackendExecutor::<CpuBackend>::new(CpuBackend::new()));
3753 let observed_unlocked_state = Arc::new(AtomicBool::new(false));
3754 let observed_session = Arc::new(AtomicBool::new(false));
3755 let ordinary = Arc::new(LockProbePreparedOperation {
3756 binding: probe_binding(),
3757 specialization: probe_specialization(),
3758 executor: Arc::downgrade(&executor),
3759 observed_unlocked_state: Arc::clone(&observed_unlocked_state),
3760 });
3761 let session = Arc::new(SessionProbePreparedOperation {
3762 binding: probe_binding(),
3763 specialization: probe_specialization(),
3764 observed_session: Arc::clone(&observed_session),
3765 });
3766 let operations = vec![
3767 PreparedOperationPlan::executable(ordinary.clone(), ordinary),
3768 PreparedOperationPlan::executable(session.clone(), session),
3769 ];
3770
3771 let output = ErasedTensorBackendExecutor::execute(
3772 executor.as_ref(),
3773 &partial_session_probe_program(),
3774 &operations,
3775 vec![f64_zeros(vec![2])],
3776 )
3777 .expect("mixed session regions execute");
3778
3779 assert_eq!(output[0].shape(), &[2]);
3780 assert!(observed_unlocked_state.load(Ordering::SeqCst));
3781 assert!(observed_session.load(Ordering::SeqCst));
3782
3783 let values = ErasedTensorBackendExecutor::execute_values(
3784 executor.as_ref(),
3785 &partial_session_probe_program(),
3786 &operations,
3787 vec![f64_zeros(vec![2])],
3788 )
3789 .expect("mixed session regions execute in value mode");
3790 assert_eq!(values[0].shape(), &[2]);
3791 }
3792
3793 #[test]
3794 fn tensor_backend_executor_bridge_does_not_require_clone_source_contract() {
3795 type ContractConstructor<B> = fn(
3796 ProviderDeviceIdentity,
3797 CoreCapabilityBundle,
3798 B,
3799 Arc<dyn EventDomainDriver>,
3800 InputIngressContract,
3801 Option<Arc<dyn RuntimeCacheOwner>>,
3802 ) -> ExecutableEngineContract;
3803
3804 fn factory_accepts_backend_without_clone_bound<B>()
3805 where
3806 B: TensorBackend + Send + Sync + 'static,
3807 {
3808 let _factory: fn(B) -> Arc<dyn ErasedTensorBackendExecutor> =
3809 super::erased_tensor_backend_executor::<B>;
3810 }
3811
3812 fn contract_accepts_backend_without_clone_bound<B>()
3813 where
3814 B: TensorBackend + Send + Sync + 'static,
3815 {
3816 let _constructor: ContractConstructor<B> = ExecutableEngineContract::new::<B>;
3817 }
3818
3819 factory_accepts_backend_without_clone_bound::<CpuBackend>();
3820 contract_accepts_backend_without_clone_bound::<CpuBackend>();
3821 }
3822}