1use std::collections::{HashMap, HashSet};
7use std::error::Error as StdError;
8use std::fmt;
9use std::panic::{catch_unwind, AssertUnwindSafe};
10use std::sync::{Arc, Condvar, Mutex, MutexGuard};
11use std::thread::{self, ThreadId};
12
13use smallvec::SmallVec;
14use tenferro_ops::shape_extent::ShapeExtent;
15use tenferro_tensor::{Tensor, TensorBackend, TensorRead, TensorValue};
16
17use crate::error::ErrorPhase;
18use crate::exec::{
19 DispatchMode, ExecInstruction, ExecProgram, ExecSlot, ExtensionExecutionDispatch,
20};
21use crate::extension_cache::{ExtensionCacheSelector, ExtensionCacheStore};
22use crate::graph::CompiledGraph;
23use crate::runtime::schedule::{
24 EventDependency, ExecutionLocation, ScheduledGraph, ScheduledNode, ScheduledNodeKind,
25 ScheduledTransfer, UnsupportedScheduledNodeError,
26};
27use crate::runtime::{
28 CacheOwnerError, CacheStats, EventDomainError, EventDomainOperation, EventDomainRun,
29 EventToken, InputSignature, PrepareError, PrepareOptions, PreparedOperationPlan, Runtime,
30 RuntimeCacheOwner, SubmissionError, TransferError, TransferProviderContractError,
31 TransferRequest,
32};
33use crate::{Error, Result};
34
35type RuntimeInputRefs<'a> = SmallVec<[&'a Tensor; 8]>;
36type RuntimeInputReads<'a> = SmallVec<[TensorRead<'a>; 8]>;
37type RuntimeInputShapes<'a> = SmallVec<[&'a [usize]; 8]>;
38type RuntimeShapeScratch = SmallVec<[usize; 8]>;
39
40#[derive(Clone, Copy, Debug)]
41pub(super) enum RuntimeOutputMode {
42 Tensor,
43 Value,
44}
45
46#[derive(Clone)]
52pub struct PreparedCompiledGraph {
53 runtime_id: super::RuntimeId,
54 epoch: super::RuntimeEpoch,
55 program: CompiledGraph,
56 prepared: Arc<super::preparation::PreparedProgram>,
57}
58
59pub struct ExecutionHandle {
61 submission: Arc<InFlightSubmission>,
62}
63
64impl ExecutionHandle {
65 pub fn wait(self) -> Result<Vec<Tensor>> {
73 self.submission.wait()
74 }
75}
76
77impl fmt::Debug for ExecutionHandle {
78 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
79 formatter
80 .debug_struct("ExecutionHandle")
81 .field("pending", &self.submission.is_pending())
82 .finish_non_exhaustive()
83 }
84}
85
86pub(super) struct InFlightSubmission {
87 work: Mutex<Option<InFlightWork>>,
88 completion: Mutex<Option<Result<Vec<Tensor>>>>,
89 completed: Condvar,
90}
91
92struct AdmittedExecution {
93 prepared: PreparedCompiledGraph,
94 inputs: Vec<Tensor>,
95}
96
97enum InFlightWork {
98 Admitted(AdmittedExecution),
99 #[cfg(test)]
100 Test(Box<dyn FnOnce() -> Result<Vec<Tensor>> + Send>),
101}
102
103impl InFlightSubmission {
104 fn new(prepared: PreparedCompiledGraph, inputs: Vec<Tensor>) -> Self {
105 Self {
106 work: Mutex::new(Some(InFlightWork::Admitted(AdmittedExecution {
107 prepared,
108 inputs,
109 }))),
110 completion: Mutex::new(None),
111 completed: Condvar::new(),
112 }
113 }
114
115 #[cfg(test)]
116 pub(super) fn for_test(work: impl FnOnce() -> Result<Vec<Tensor>> + Send + 'static) -> Self {
117 Self {
118 work: Mutex::new(Some(InFlightWork::Test(Box::new(work)))),
119 completion: Mutex::new(None),
120 completed: Condvar::new(),
121 }
122 }
123
124 pub(super) fn run(&self) {
125 let work = match self.work.lock() {
126 Ok(mut work) => work.take(),
127 Err(poisoned) => poisoned.into_inner().take(),
128 };
129 let result = catch_unwind(AssertUnwindSafe(|| match work {
130 Some(InFlightWork::Admitted(admitted)) => {
131 let input_refs = admitted.inputs.iter().collect::<Vec<_>>();
132 execute_admitted(&admitted.prepared, &input_refs)
133 }
134 #[cfg(test)]
135 Some(InFlightWork::Test(work)) => work(),
136 None => Err(Error::runtime_state(
137 "Runtime::submit",
138 ErrorPhase::Execution,
139 "in-flight submission work was already consumed",
140 )),
141 }))
142 .unwrap_or_else(|payload| {
143 Err(Error::runtime_state(
144 "ExecutionHandle::wait",
145 ErrorPhase::Execution,
146 panic_payload_message(payload),
147 ))
148 });
149 match self.completion.lock() {
150 Ok(mut completion) => {
151 *completion = Some(result);
152 }
153 Err(poisoned) => {
154 *poisoned.into_inner() = Some(Err(Error::runtime_state(
155 "ExecutionHandle::wait",
156 ErrorPhase::Execution,
157 "in-flight completion lock poisoned",
158 )));
159 }
160 }
161 self.completed.notify_all();
162 }
163
164 fn wait(&self) -> Result<Vec<Tensor>> {
165 let mut completion = self.completion.lock().map_err(|_| {
166 Error::runtime_state(
167 "ExecutionHandle::wait",
168 ErrorPhase::Execution,
169 "in-flight completion lock poisoned",
170 )
171 })?;
172 loop {
173 if let Some(result) = completion.take() {
174 return result;
175 }
176 completion = self.completed.wait(completion).map_err(|_| {
177 Error::runtime_state(
178 "ExecutionHandle::wait",
179 ErrorPhase::Execution,
180 "in-flight completion lock poisoned while waiting",
181 )
182 })?;
183 }
184 }
185
186 fn is_pending(&self) -> bool {
187 self.completion
188 .lock()
189 .map_or(true, |completion| completion.is_none())
190 }
191}
192
193pub(super) trait SubmissionSpawner {
194 fn spawn(&self, submission: Arc<InFlightSubmission>) -> std::io::Result<()>;
195}
196
197pub(super) fn spawn_in_flight(
198 submission: Arc<InFlightSubmission>,
199 spawner: &dyn SubmissionSpawner,
200) -> Result<ExecutionHandle> {
201 spawner.spawn(Arc::clone(&submission)).map_err(|source| {
202 Error::runtime_state_source(
203 "Runtime::submit",
204 ErrorPhase::Execution,
205 SubmissionError::WorkerSpawn { source },
206 )
207 })?;
208 Ok(ExecutionHandle { submission })
209}
210
211pub(super) struct OsThreadSpawner;
212
213impl SubmissionSpawner for OsThreadSpawner {
214 fn spawn(&self, submission: Arc<InFlightSubmission>) -> std::io::Result<()> {
215 thread::Builder::new()
216 .name("tenferro-runtime-submit".to_string())
217 .spawn(move || submission.run())
218 .map(drop)
219 }
220}
221
222impl fmt::Debug for PreparedCompiledGraph {
223 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
224 formatter
225 .debug_struct("PreparedCompiledGraph")
226 .field("runtime_id", &self.runtime_id)
227 .field("epoch", &self.epoch)
228 .field("program", &self.program)
229 .finish_non_exhaustive()
230 }
231}
232
233fn panic_payload_message(payload: Box<dyn std::any::Any + Send + 'static>) -> String {
234 if let Some(message) = payload.downcast_ref::<&str>() {
235 format!("submitted execution panicked: {message}")
236 } else if let Some(message) = payload.downcast_ref::<String>() {
237 format!("submitted execution panicked: {message}")
238 } else {
239 "submitted execution panicked".to_string()
240 }
241}
242
243#[allow(
244 dead_code,
245 reason = "Phase 5 runtime execution task adds erased dispatch methods"
246)]
247pub(super) trait ErasedTensorBackendExecutor: fmt::Debug + Send + Sync {
248 fn backend_type_name(&self) -> &'static str;
249 fn extension_cache_stats(&self) -> std::result::Result<CacheStats, CacheOwnerError>;
250 fn clear_extension_caches(&self) -> std::result::Result<(), CacheOwnerError>;
251 fn execute(
252 &self,
253 program: &ExecProgram,
254 operations: &[PreparedOperationPlan],
255 inputs: Vec<Tensor>,
256 ) -> Result<Vec<Tensor>>;
257 fn execute_tensor_refs(
258 &self,
259 program: &ExecProgram,
260 operations: &[PreparedOperationPlan],
261 inputs: &[&Tensor],
262 ) -> Result<Vec<Tensor>>;
263 fn execute_values(
264 &self,
265 program: &ExecProgram,
266 operations: &[PreparedOperationPlan],
267 inputs: Vec<Tensor>,
268 ) -> Result<Vec<TensorValue>>;
269 fn execute_value_refs(
270 &self,
271 program: &ExecProgram,
272 operations: &[PreparedOperationPlan],
273 inputs: &[&Tensor],
274 ) -> Result<Vec<TensorValue>>;
275 fn execute_slot_instruction<'input>(
276 &self,
277 instruction_index: usize,
278 instruction: &ExecInstruction,
279 operations: &[PreparedOperationPlan],
280 slots: &mut [Option<ExecSlot<'input>>],
281 output_mode: RuntimeOutputMode,
282 terminal_slots: &[bool],
283 ) -> Result<()>;
284 fn materialize_slot<'input>(&self, slot: ExecSlot<'input>) -> Result<Tensor>;
285 fn materialize_slot_value<'input>(&self, slot: ExecSlot<'input>) -> Result<TensorValue>;
286}
287
288pub(super) fn erased_tensor_backend_executor<B>(backend: B) -> Arc<dyn ErasedTensorBackendExecutor>
289where
290 B: TensorBackend + Send + Sync + 'static,
291{
292 Arc::new(TensorBackendExecutor::<B>::new(backend))
293}
294
295pub(super) fn extension_cache_owner(
296 executor: Arc<dyn ErasedTensorBackendExecutor>,
297) -> Arc<dyn RuntimeCacheOwner> {
298 Arc::new(TensorBackendExtensionCacheOwner { executor })
299}
300
301#[derive(Debug)]
302struct TensorBackendExtensionCacheOwner {
303 executor: Arc<dyn ErasedTensorBackendExecutor>,
304}
305
306impl RuntimeCacheOwner for TensorBackendExtensionCacheOwner {
307 fn cache_stats(&self) -> std::result::Result<CacheStats, CacheOwnerError> {
308 self.executor.extension_cache_stats()
309 }
310
311 fn clear_caches(&self) -> std::result::Result<(), CacheOwnerError> {
312 self.executor.clear_extension_caches()
313 }
314}
315
316#[allow(
317 dead_code,
318 reason = "Phase 5 runtime execution task consumes backend execution state"
319)]
320struct TensorBackendExecutorState<B: TensorBackend + 'static> {
321 backend: B,
322 backend_cache: B::RuntimeCache,
323 extension_caches: ExtensionCacheStore,
324 slot_workspace: Vec<Option<ExecSlot<'static>>>,
325 borrowed_slot_workspace_capacity: usize,
326}
327
328struct TensorBackendExecutor<B: TensorBackend + 'static> {
329 state: Mutex<TensorBackendExecutorSlot<B>>,
330 available: Condvar,
331}
332
333struct TensorBackendExecutorSlot<B: TensorBackend + 'static> {
334 state: Option<TensorBackendExecutorState<B>>,
335 active_thread: Option<ThreadId>,
336 execution_poisoned: bool,
337}
338
339impl<B> TensorBackendExecutor<B>
340where
341 B: TensorBackend + Send + Sync + 'static,
342{
343 fn new(backend: B) -> Self {
344 Self {
345 state: Mutex::new(TensorBackendExecutorSlot {
346 state: Some(TensorBackendExecutorState {
347 backend,
348 backend_cache: B::RuntimeCache::default(),
349 extension_caches: ExtensionCacheStore::new(),
350 slot_workspace: Vec::new(),
351 borrowed_slot_workspace_capacity: 0,
352 }),
353 active_thread: None,
354 execution_poisoned: false,
355 }),
356 available: Condvar::new(),
357 }
358 }
359
360 fn lease_state(&self, caller: &'static str) -> Result<TensorBackendExecutorLease<'_, B>> {
361 let current = thread::current().id();
362 let mut slot = self.lock_slot(caller)?;
363 loop {
364 if slot.execution_poisoned {
365 return Err(Error::runtime_state(
366 caller,
367 ErrorPhase::Execution,
368 "tensor backend executor state poisoned by panic during prior execution",
369 ));
370 }
371 if let Some(state) = slot.state.take() {
372 slot.active_thread = Some(current);
373 return Ok(TensorBackendExecutorLease {
374 executor: self,
375 state: Some(state),
376 });
377 }
378 if slot.active_thread == Some(current) {
379 return Err(Error::runtime_state(
380 caller,
381 ErrorPhase::Execution,
382 "reentrant tensor backend executor call would deadlock",
383 ));
384 }
385 slot = self.available.wait(slot).map_err(|_| {
386 Error::runtime_state(
387 caller,
388 ErrorPhase::Execution,
389 "tensor backend executor state lock poisoned while waiting",
390 )
391 })?;
392 }
393 }
394
395 fn lock_slot(
396 &self,
397 caller: &'static str,
398 ) -> Result<MutexGuard<'_, TensorBackendExecutorSlot<B>>> {
399 self.state.lock().map_err(|_| {
400 Error::runtime_state(
401 caller,
402 ErrorPhase::Execution,
403 "tensor backend executor state lock poisoned",
404 )
405 })
406 }
407}
408
409struct TensorBackendExecutorLease<'a, B: TensorBackend + 'static> {
410 executor: &'a TensorBackendExecutor<B>,
411 state: Option<TensorBackendExecutorState<B>>,
412}
413
414impl<B: TensorBackend + 'static> TensorBackendExecutorLease<'_, B> {
415 fn state_mut(&mut self) -> &mut TensorBackendExecutorState<B> {
416 self.state
417 .as_mut()
418 .expect("executor state lease always owns state before drop")
419 }
420}
421
422impl<B: TensorBackend + 'static> Drop for TensorBackendExecutorLease<'_, B> {
423 fn drop(&mut self) {
424 let Some(state) = self.state.take() else {
425 return;
426 };
427 let panicking = thread::panicking();
428 let mut wake_all_waiters = panicking;
429 match self.executor.state.lock() {
430 Ok(mut slot) => {
431 slot.state = Some(state);
432 slot.active_thread = None;
433 slot.execution_poisoned |= panicking;
434 }
435 Err(poisoned_lock) => {
436 let mut slot = poisoned_lock.into_inner();
437 slot.state = Some(state);
438 slot.active_thread = None;
439 slot.execution_poisoned = true;
440 wake_all_waiters = true;
441 }
442 }
443 if wake_all_waiters {
444 self.executor.available.notify_all();
445 } else {
446 self.executor.available.notify_one();
447 }
448 }
449}
450
451impl<B: TensorBackend + 'static> fmt::Debug for TensorBackendExecutor<B> {
452 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
453 formatter
454 .debug_struct("TensorBackendExecutor")
455 .field("backend_type", &std::any::type_name::<B>())
456 .field("state_lock_poisoned", &self.state.is_poisoned())
457 .finish_non_exhaustive()
458 }
459}
460
461impl<B> ErasedTensorBackendExecutor for TensorBackendExecutor<B>
462where
463 B: TensorBackend + Send + Sync + 'static,
464{
465 fn backend_type_name(&self) -> &'static str {
466 std::any::type_name::<B>()
467 }
468
469 fn extension_cache_stats(&self) -> std::result::Result<CacheStats, CacheOwnerError> {
470 let lease = self
471 .lease_state("Runtime::extension_cache_stats")
472 .map_err(cache_owner_error)?;
473 let state = lease
474 .state
475 .as_ref()
476 .expect("executor state lease always owns state before drop");
477 Ok(cache_stats_from_tensor_stats(
478 state.extension_caches.stats(ExtensionCacheSelector::All),
479 ))
480 }
481
482 fn clear_extension_caches(&self) -> std::result::Result<(), CacheOwnerError> {
483 let mut lease = self
484 .lease_state("Runtime::clear_extension_caches")
485 .map_err(cache_owner_error)?;
486 lease.state_mut().extension_caches.clear();
487 Ok(())
488 }
489
490 fn execute(
491 &self,
492 program: &ExecProgram,
493 operations: &[PreparedOperationPlan],
494 inputs: Vec<Tensor>,
495 ) -> Result<Vec<Tensor>> {
496 validate_exec_input_count(program, inputs.len())?;
497 let mut lease = self.lease_state("Runtime::run_compiled")?;
498 let TensorBackendExecutorState {
499 backend,
500 backend_cache,
501 extension_caches,
502 slot_workspace,
503 borrowed_slot_workspace_capacity: _,
504 } = lease.state_mut();
505 let mut extension_dispatch = ExtensionExecutionDispatch {
506 operations,
507 caches: extension_caches,
508 };
509 crate::segment::eval_exec_segmented_with_cache_and_workspace(
510 backend,
511 program,
512 inputs,
513 slot_workspace,
514 backend_cache,
515 Some(&mut extension_dispatch),
516 )
517 }
518
519 fn execute_tensor_refs(
520 &self,
521 program: &ExecProgram,
522 operations: &[PreparedOperationPlan],
523 inputs: &[&Tensor],
524 ) -> Result<Vec<Tensor>> {
525 validate_exec_input_count(program, inputs.len())?;
526 let inputs = inputs
527 .iter()
528 .map(|tensor| ExecSlot::Read(TensorRead::from_tensor(tensor)))
529 .collect();
530 let mut lease = self.lease_state("Runtime::run_compiled")?;
531 let TensorBackendExecutorState {
532 backend,
533 backend_cache,
534 extension_caches,
535 borrowed_slot_workspace_capacity,
536 ..
537 } = lease.state_mut();
538 let mut extension_dispatch = ExtensionExecutionDispatch {
539 operations,
540 caches: extension_caches,
541 };
542 let mut slot_workspace = Vec::with_capacity(*borrowed_slot_workspace_capacity);
543 let result = crate::segment::eval_exec_segmented_slots_with_cache_and_workspace(
544 backend,
545 program,
546 inputs,
547 &mut slot_workspace,
548 backend_cache,
549 Some(&mut extension_dispatch),
550 );
551 *borrowed_slot_workspace_capacity = slot_workspace.capacity();
552 result
553 }
554
555 fn execute_values(
556 &self,
557 program: &ExecProgram,
558 operations: &[PreparedOperationPlan],
559 inputs: Vec<Tensor>,
560 ) -> Result<Vec<TensorValue>> {
561 validate_exec_input_count(program, inputs.len())?;
562 let inputs = inputs.into_iter().map(ExecSlot::Owned).collect();
563 let mut lease = self.lease_state("Runtime::run_compiled_values")?;
564 let TensorBackendExecutorState {
565 backend,
566 backend_cache,
567 extension_caches,
568 slot_workspace,
569 borrowed_slot_workspace_capacity: _,
570 } = lease.state_mut();
571 let mut extension_dispatch = ExtensionExecutionDispatch {
572 operations,
573 caches: extension_caches,
574 };
575 crate::segment::eval_exec_segmented_slot_values_with_cache_and_workspace(
576 backend,
577 program,
578 inputs,
579 slot_workspace,
580 backend_cache,
581 Some(&mut extension_dispatch),
582 )
583 }
584
585 fn execute_value_refs(
586 &self,
587 program: &ExecProgram,
588 operations: &[PreparedOperationPlan],
589 inputs: &[&Tensor],
590 ) -> Result<Vec<TensorValue>> {
591 validate_exec_input_count(program, inputs.len())?;
592 let inputs = inputs
593 .iter()
594 .map(|tensor| ExecSlot::Read(TensorRead::from_tensor(tensor)))
595 .collect();
596 let mut lease = self.lease_state("Runtime::run_compiled_values")?;
597 let TensorBackendExecutorState {
598 backend,
599 backend_cache,
600 extension_caches,
601 borrowed_slot_workspace_capacity,
602 ..
603 } = lease.state_mut();
604 let mut extension_dispatch = ExtensionExecutionDispatch {
605 operations,
606 caches: extension_caches,
607 };
608 let mut slot_workspace = Vec::with_capacity(*borrowed_slot_workspace_capacity);
609 let result = crate::segment::eval_exec_segmented_slot_values_with_cache_and_workspace(
610 backend,
611 program,
612 inputs,
613 &mut slot_workspace,
614 backend_cache,
615 Some(&mut extension_dispatch),
616 );
617 *borrowed_slot_workspace_capacity = slot_workspace.capacity();
618 result
619 }
620
621 fn execute_slot_instruction<'input>(
622 &self,
623 instruction_index: usize,
624 instruction: &ExecInstruction,
625 operations: &[PreparedOperationPlan],
626 slots: &mut [Option<ExecSlot<'input>>],
627 output_mode: RuntimeOutputMode,
628 terminal_slots: &[bool],
629 ) -> Result<()> {
630 let mut lease = self.lease_state("Runtime::run_compiled scheduled instruction")?;
631 let TensorBackendExecutorState {
632 backend,
633 backend_cache,
634 extension_caches,
635 ..
636 } = lease.state_mut();
637 let mut extension_dispatch = ExtensionExecutionDispatch {
638 operations,
639 caches: extension_caches,
640 };
641
642 if matches!(output_mode, RuntimeOutputMode::Value)
643 && backend.with_backend_session(|exec| {
644 crate::exec::try_execute_terminal_value_instruction(
645 exec,
646 slots,
647 instruction,
648 terminal_slots,
649 )
650 })?
651 {
652 } else if crate::exec::is_host_instruction(instruction) {
654 crate::exec::execute_host_instruction(backend, slots, instruction)?;
655 } else if crate::exec::is_ffi_instruction(instruction) {
656 crate::exec::execute_ffi_instruction_cached(
657 backend,
658 backend_cache,
659 slots,
660 instruction,
661 DispatchMode::Unsegmented,
662 Some(instruction_index),
663 Some(&mut extension_dispatch),
664 )?;
665 } else {
666 let result = backend.with_backend_session(|exec| {
667 crate::exec::execute_backend_op(exec, slots, instruction)
668 })?;
669 slots[instruction.output_slots[0]] = Some(ExecSlot::Owned(result));
670 }
671 crate::exec::reclaim_last_use_inputs_backend(slots, instruction, backend);
672 Ok(())
673 }
674
675 fn materialize_slot<'input>(&self, slot: ExecSlot<'input>) -> Result<Tensor> {
676 let mut lease = self.lease_state("Runtime::run_compiled collect outputs")?;
677 let backend = &mut lease.state_mut().backend;
678 backend.with_backend_session(|exec| slot.into_tensor(exec))
679 }
680
681 fn materialize_slot_value<'input>(&self, slot: ExecSlot<'input>) -> Result<TensorValue> {
682 let mut lease = self.lease_state("Runtime::run_compiled_values collect outputs")?;
683 let backend = &mut lease.state_mut().backend;
684 backend.with_backend_session(|exec| slot.into_value(exec))
685 }
686}
687
688fn cache_owner_error(error: Error) -> CacheOwnerError {
689 CacheOwnerError::new(Arc::new(error))
690}
691
692fn cache_stats_from_tensor_stats(stats: tenferro_tensor::CacheStats) -> CacheStats {
693 CacheStats {
694 entries: stats.entries,
695 retained_bytes: stats.retained_bytes,
696 hits: stats.hits,
697 misses: stats.misses,
698 evictions: stats.evictions,
699 clears: stats.clears,
700 }
701}
702
703pub(super) fn run_compiled(
704 runtime: &Runtime,
705 program: &CompiledGraph,
706 inputs: &[&Tensor],
707) -> Result<Vec<Tensor>> {
708 let inputs = resolve_input_refs(program, inputs)?;
709 let signature = input_signature(&inputs)?;
710 let prepared = prepare(runtime, program, &signature)?;
711 validate_prepared_epoch(runtime, prepared.root().epoch(), "Runtime::run_compiled")?;
712 execute_scheduled_tensor_refs(
713 prepared.root().staging(),
714 prepared.root().schedule(),
715 prepared.operations(),
716 &inputs,
717 )
718}
719
720pub(super) fn prepare_compiled(
721 runtime: &Runtime,
722 program: &CompiledGraph,
723 inputs: &[&Tensor],
724) -> Result<PreparedCompiledGraph> {
725 let inputs = resolve_input_refs(program, inputs)?;
726 let signature = input_signature(&inputs)?;
727 let prepared = prepare(runtime, program, &signature)?;
728 validate_prepared_epoch(
729 runtime,
730 prepared.root().epoch(),
731 "Runtime::prepare_compiled",
732 )?;
733 Ok(PreparedCompiledGraph {
734 runtime_id: runtime.id(),
735 epoch: prepared.root().epoch(),
736 program: program.clone(),
737 prepared,
738 })
739}
740
741pub(super) fn submit(
742 runtime: &Runtime,
743 program: &CompiledGraph,
744 inputs: &[&Tensor],
745) -> Result<ExecutionHandle> {
746 submit_with_spawner(runtime, program, inputs, &OsThreadSpawner)
747}
748
749pub(super) fn submit_with_spawner(
750 runtime: &Runtime,
751 program: &CompiledGraph,
752 inputs: &[&Tensor],
753 spawner: &dyn SubmissionSpawner,
754) -> Result<ExecutionHandle> {
755 let inputs = resolve_input_refs(program, inputs)?
756 .into_iter()
757 .cloned()
758 .collect::<Vec<_>>();
759 let input_refs = inputs.iter().collect::<Vec<_>>();
760 let prepared = prepare_compiled(runtime, program, &input_refs)?;
761 let submission = Arc::new(InFlightSubmission::new(prepared, inputs));
762 spawn_in_flight(submission, spawner)
763}
764
765pub(super) fn run_prepared(
766 runtime: &Runtime,
767 prepared: &PreparedCompiledGraph,
768 inputs: &[&Tensor],
769) -> Result<Vec<Tensor>> {
770 validate_prepared_runtime(runtime, prepared, "Runtime::run_prepared")?;
771 let inputs = resolve_input_refs(&prepared.program, inputs)?;
772 execute_scheduled_tensor_refs(
773 prepared.prepared.root().staging(),
774 prepared.prepared.root().schedule(),
775 prepared.prepared.operations(),
776 &inputs,
777 )
778}
779
780fn execute_admitted(prepared: &PreparedCompiledGraph, inputs: &[&Tensor]) -> Result<Vec<Tensor>> {
781 let inputs = resolve_input_refs(&prepared.program, inputs)?;
782 execute_scheduled_tensor_refs(
783 prepared.prepared.root().staging(),
784 prepared.prepared.root().schedule(),
785 prepared.prepared.operations(),
786 &inputs,
787 )
788}
789
790pub(super) fn run_compiled_values(
791 runtime: &Runtime,
792 program: &CompiledGraph,
793 inputs: &[&Tensor],
794) -> Result<Vec<TensorValue>> {
795 let inputs = resolve_input_refs(program, inputs)?;
796 let signature = input_signature(&inputs)?;
797 let prepared = prepare(runtime, program, &signature)?;
798 validate_prepared_epoch(
799 runtime,
800 prepared.root().epoch(),
801 "Runtime::run_compiled_values",
802 )?;
803 execute_scheduled_value_refs(
804 prepared.root().staging(),
805 prepared.root().schedule(),
806 prepared.operations(),
807 &inputs,
808 )
809}
810
811fn validate_prepared_runtime(
812 runtime: &Runtime,
813 prepared: &PreparedCompiledGraph,
814 caller: &'static str,
815) -> Result<()> {
816 if runtime.id() != prepared.runtime_id {
817 return Err(Error::runtime_state(
818 caller,
819 ErrorPhase::Execution,
820 "prepared compiled graph belongs to a different runtime",
821 ));
822 }
823 let epoch = runtime
824 .epoch()
825 .map_err(|source| Error::runtime_state_source(caller, ErrorPhase::Execution, source))?;
826 if epoch != prepared.epoch {
827 return Err(Error::runtime_state(
828 caller,
829 ErrorPhase::Execution,
830 format!(
831 "prepared epoch {:?} does not match current epoch {:?}",
832 prepared.epoch, epoch
833 ),
834 ));
835 }
836 Ok(())
837}
838
839fn prepare(
840 runtime: &Runtime,
841 program: &CompiledGraph,
842 signature: &InputSignature,
843) -> Result<Arc<super::preparation::PreparedProgram>> {
844 runtime
845 .prepare_compiled_for(program, signature, &PrepareOptions::new())
846 .map_err(prepare_error)
847}
848
849fn validate_prepared_epoch(
850 runtime: &Runtime,
851 prepared_epoch: super::RuntimeEpoch,
852 caller: &'static str,
853) -> Result<()> {
854 let epoch = runtime
855 .epoch()
856 .map_err(|source| Error::runtime_state_source(caller, ErrorPhase::Execution, source))?;
857 if epoch != prepared_epoch {
858 return Err(Error::runtime_state(
859 caller,
860 ErrorPhase::Execution,
861 format!(
862 "prepared epoch {:?} does not match current epoch {:?}",
863 prepared_epoch, epoch
864 ),
865 ));
866 }
867 Ok(())
868}
869
870fn execute_scheduled_tensor_refs(
871 program: &ExecProgram,
872 schedule: &ScheduledGraph,
873 operations: &[PreparedOperationPlan],
874 inputs: &[&Tensor],
875) -> Result<Vec<Tensor>> {
876 validate_exec_input_count(program, inputs.len())?;
877 crate::exec::validate_exec_program(program, "scheduled tensor executor")?;
878 let inputs = inputs
879 .iter()
880 .map(|tensor| ExecSlot::Read(TensorRead::from_tensor(tensor)))
881 .collect();
882 execute_scheduled_slots(
883 program,
884 schedule,
885 operations,
886 inputs,
887 RuntimeOutputMode::Tensor,
888 )
889 .and_then(|mut slots| {
890 collect_tensor_outputs_with(program, &mut slots, |location, slot| {
891 location.witness().executor().materialize_slot(slot)
892 })
893 })
894}
895
896fn execute_scheduled_value_refs(
897 program: &ExecProgram,
898 schedule: &ScheduledGraph,
899 operations: &[PreparedOperationPlan],
900 inputs: &[&Tensor],
901) -> Result<Vec<TensorValue>> {
902 validate_exec_input_count(program, inputs.len())?;
903 crate::exec::validate_exec_program(program, "scheduled value executor")?;
904 let inputs = inputs
905 .iter()
906 .map(|tensor| ExecSlot::Read(TensorRead::from_tensor(tensor)))
907 .collect();
908 execute_scheduled_slots(
909 program,
910 schedule,
911 operations,
912 inputs,
913 RuntimeOutputMode::Value,
914 )
915 .and_then(|mut slots| {
916 collect_value_outputs_with(program, &mut slots, |location, slot| {
917 location.witness().executor().materialize_slot_value(slot)
918 })
919 })
920}
921
922fn execute_scheduled_slots<'input>(
923 program: &ExecProgram,
924 schedule: &ScheduledGraph,
925 operations: &[PreparedOperationPlan],
926 inputs: Vec<ExecSlot<'input>>,
927 output_mode: RuntimeOutputMode,
928) -> Result<Vec<Option<LocatedExecSlot<'input>>>> {
929 let terminal_slots = if matches!(output_mode, RuntimeOutputMode::Value) {
930 crate::exec::terminal_output_slots(program)
931 } else {
932 Vec::new()
933 };
934 let mut staged = Vec::new();
935 let mut located = (0..program.n_slots)
936 .map(|_| Vec::new())
937 .collect::<Vec<Vec<LocatedExecSlot<'input>>>>();
938 let mut event_domains = ScheduledEventDomains::new(schedule)?;
943 let result = (|| {
944 crate::exec::initialize_exec_slots_in(program, inputs, &mut staged)?;
945 if schedule.input_locations().len() != program.input_slots.len() {
946 return Err(Error::runtime_state(
947 "Runtime::run_compiled",
948 ErrorPhase::Execution,
949 format!(
950 "prepared schedule has {} input locations for {} inputs",
951 schedule.input_locations().len(),
952 program.input_slots.len()
953 ),
954 ));
955 }
956 for (&slot, location) in program.input_slots.iter().zip(schedule.input_locations()) {
957 let value = staged
958 .get_mut(slot)
959 .and_then(Option::take)
960 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
961 validate_runtime_input_ingress(location, &value.as_read(), slot)?;
962 located[slot].push(LocatedExecSlot {
963 location: location.clone(),
964 value,
965 });
966 }
967 for (node_index, node) in schedule.nodes().iter().enumerate() {
968 match node {
969 ScheduledNode::Operation(operation_node) => {
970 let mut launch = || {
971 let instruction_index = operation_node.instruction_index();
972 let instruction =
973 program.instructions.get(instruction_index).ok_or_else(|| {
974 Error::runtime_state(
975 "Runtime::run_compiled",
976 ErrorPhase::Execution,
977 format!(
978 "scheduled operation references instruction \
979 {instruction_index}, but the execution program has {} \
980 instructions",
981 program.instructions.len()
982 ),
983 )
984 })?;
985 let operation = instruction_execution(schedule, instruction)?;
986 if operation.location() != operation_node.location() {
987 return Err(Error::runtime_state(
988 "Runtime::run_compiled",
989 ErrorPhase::Execution,
990 format!(
991 "scheduled instruction {instruction_index} location does not \
992 match its prepared executor"
993 ),
994 ));
995 }
996 stage_instruction_inputs(
997 instruction,
998 operation.location(),
999 &mut located,
1000 &mut staged,
1001 )?;
1002 operation.executor().execute_slot_instruction(
1003 instruction_index,
1004 instruction,
1005 operations,
1006 &mut staged,
1007 output_mode,
1008 &terminal_slots,
1009 )?;
1010 validate_instruction_outputs(
1011 instruction_index,
1012 instruction,
1013 operation.location(),
1014 &staged,
1015 )?;
1016 retain_instruction_results(
1017 instruction,
1018 operation.location(),
1019 &mut located,
1020 &mut staged,
1021 )
1022 };
1023 event_domains.enqueue(node_index, node, &mut launch)?;
1024 }
1025 ScheduledNode::Transfer(transfer) => {
1026 let mut launch = || execute_scheduled_transfer(transfer, &mut located);
1027 event_domains.enqueue(node_index, node, &mut launch)?;
1028 }
1029 ScheduledNode::Collective(_) => {
1030 return Err(Error::runtime_state_source(
1031 "Runtime::run_compiled",
1032 ErrorPhase::Execution,
1033 UnsupportedScheduledNodeError {
1034 node_index,
1035 node_kind: ScheduledNodeKind::Collective,
1036 },
1037 ));
1038 }
1039 ScheduledNode::Barrier(_) => {
1040 let mut launch = || Ok(());
1041 event_domains.enqueue(node_index, node, &mut launch)?;
1042 }
1043 }
1044 }
1045 Ok(())
1046 })();
1047 let drain = event_domains.drain();
1048 match (result, drain) {
1049 (Ok(()), Ok(())) => collect_located_outputs(program, &mut located),
1050 (Err(error), Ok(())) | (Ok(()), Err(error)) => {
1051 staged.clear();
1052 located.clear();
1053 Err(error)
1054 }
1055 (Err(primary), Err(cleanup)) => {
1056 staged.clear();
1057 located.clear();
1058 Err(scheduled_execution_cleanup_error(primary, cleanup))
1059 }
1060 }
1061}
1062
1063#[derive(Debug)]
1064pub(crate) struct ScheduledEventDomains {
1065 runs: Vec<RuntimeOwnedEventDomainRun>,
1066 completions: HashMap<EventDependency, Arc<dyn EventToken>>,
1067}
1068
1069#[derive(Debug, thiserror::Error)]
1070#[error(
1071 "scheduled node {node_index} depends on completion {dependency:?}, but no completion token was recorded"
1072)]
1073pub(crate) struct MissingScheduledDependencyCompletionError {
1074 pub(crate) dependency: EventDependency,
1075 pub(crate) node_index: usize,
1076}
1077
1078#[derive(Debug, thiserror::Error)]
1079#[error("scheduled transfer destination {destination:?} already contains value slot {value_slot}")]
1080pub(crate) struct DuplicateTransferDestinationError {
1081 pub(crate) value_slot: usize,
1082 pub(crate) destination: ExecutionLocation,
1083}
1084
1085impl ScheduledEventDomains {
1086 pub(crate) fn new(schedule: &ScheduledGraph) -> Result<Self> {
1087 schedule.preflight().map_err(|source| {
1088 Error::runtime_state_source("Runtime::run_compiled", ErrorPhase::Execution, source)
1089 })?;
1090 let mut drivers = Vec::new();
1091 let mut seen_domains = HashSet::new();
1092 for node in schedule.nodes() {
1093 let domain = node.completion().domain();
1094 if !seen_domains.insert(domain) {
1095 continue;
1096 }
1097 let witness = node.event_domain_witness();
1098 debug_assert_eq!(witness.event_domain_id(), domain);
1099 drivers.push((domain, witness.event_domain_driver().clone()));
1100 }
1101 let mut runs = Vec::with_capacity(drivers.len());
1102 for (domain, driver) in drivers {
1103 let run = RuntimeOwnedEventDomainRun::new(domain, driver.begin_run(domain)?);
1104 let actual = run.domain(EventDomainOperation::BeginRun)?;
1105 if actual != domain {
1106 return Err(event_domain_error(EventDomainError::RunDomainMismatch {
1107 operation: EventDomainOperation::BeginRun,
1108 node_index: None,
1109 expected: domain,
1110 actual,
1111 }));
1112 }
1113 runs.push(run);
1114 }
1115 Ok(Self {
1116 runs,
1117 completions: HashMap::new(),
1118 })
1119 }
1120
1121 #[cfg(test)]
1122 pub(crate) fn for_test(
1123 drivers: Vec<(super::EventDomainId, Arc<dyn super::EventDomainDriver>)>,
1124 ) -> Result<Self> {
1125 let mut runs = Vec::with_capacity(drivers.len());
1126 for (domain, driver) in drivers {
1127 let run = RuntimeOwnedEventDomainRun::new(domain, driver.begin_run(domain)?);
1128 let actual = run.domain(EventDomainOperation::BeginRun)?;
1129 if actual != domain {
1130 return Err(event_domain_error(EventDomainError::RunDomainMismatch {
1131 operation: EventDomainOperation::BeginRun,
1132 node_index: None,
1133 expected: domain,
1134 actual,
1135 }));
1136 }
1137 runs.push(run);
1138 }
1139 Ok(Self {
1140 runs,
1141 completions: HashMap::new(),
1142 })
1143 }
1144
1145 pub(crate) fn enqueue(
1146 &mut self,
1147 node_index: usize,
1148 node: &ScheduledNode,
1149 launch: &mut dyn FnMut() -> Result<()>,
1150 ) -> Result<()> {
1151 let completion = node.completion();
1152 let destination = completion.domain();
1153 let run_index = self.run_index(completion.domain())?;
1154 let actual_preflight_domain = self.runs[run_index].domain(EventDomainOperation::Enqueue)?;
1155 if actual_preflight_domain != destination {
1156 return Err(event_domain_error(EventDomainError::RunDomainMismatch {
1157 operation: EventDomainOperation::Enqueue,
1158 node_index: Some(node_index),
1159 expected: destination,
1160 actual: actual_preflight_domain,
1161 }));
1162 }
1163 let dependencies = self.classify_dependencies(node_index, node, destination)?;
1164 let actual_run_domain = self.runs[run_index].domain(EventDomainOperation::Enqueue)?;
1165 if actual_run_domain != destination {
1166 return Err(event_domain_error(EventDomainError::RunDomainMismatch {
1167 operation: EventDomainOperation::Enqueue,
1168 node_index: Some(node_index),
1169 expected: destination,
1170 actual: actual_run_domain,
1171 }));
1172 }
1173 let completion_event = self.runs[run_index].enqueue(&dependencies, launch)?;
1174 let actual = completion_event.origin();
1175 if actual != completion.domain() {
1176 return Err(event_domain_error(
1177 EventDomainError::CompletionTokenDomainMismatch {
1178 operation: EventDomainOperation::ValidateCompletion,
1179 node_index: Some(node_index),
1180 expected: completion.domain(),
1181 actual,
1182 },
1183 ));
1184 }
1185 self.completions.insert(
1186 EventDependency::from_completion(completion),
1187 completion_event,
1188 );
1189 Ok(())
1190 }
1191
1192 fn classify_dependencies(
1193 &self,
1194 node_index: usize,
1195 node: &ScheduledNode,
1196 destination: super::EventDomainId,
1197 ) -> Result<SmallVec<[Arc<dyn EventToken>; 4]>> {
1198 let mut admitted = SmallVec::with_capacity(node.dependencies().len());
1199 for dependency in node.dependencies() {
1200 let dependency_completion =
1201 self.completions.get(dependency).cloned().ok_or_else(|| {
1202 Error::runtime_state_source(
1203 "Runtime::run_compiled",
1204 ErrorPhase::Execution,
1205 MissingScheduledDependencyCompletionError {
1206 dependency: *dependency,
1207 node_index,
1208 },
1209 )
1210 })?;
1211 let actual = dependency_completion.origin();
1212 if actual != dependency.domain() {
1213 return Err(event_domain_error(
1214 EventDomainError::DependencyDomainMismatch {
1215 operation: match node {
1216 ScheduledNode::Transfer(_) => EventDomainOperation::TransferBridge,
1217 ScheduledNode::Operation(_)
1218 | ScheduledNode::Collective(_)
1219 | ScheduledNode::Barrier(_) => EventDomainOperation::Enqueue,
1220 },
1221 node_index: Some(node_index),
1222 expected: dependency.domain(),
1223 actual,
1224 },
1225 ));
1226 }
1227 match node {
1228 ScheduledNode::Transfer(transfer) => {
1229 let source = transfer.source_event_domain();
1230 if actual == destination {
1231 admitted.push(dependency_completion);
1232 } else if actual == source {
1233 dependency_completion.wait().map_err(|source_error| {
1234 event_domain_error(EventDomainError::DependencyWaitFailed {
1235 operation: EventDomainOperation::TransferBridge,
1236 node_index: Some(node_index),
1237 expected: destination,
1238 actual,
1239 source: Box::new(source_error),
1240 })
1241 })?;
1242 } else {
1243 return Err(event_domain_error(
1244 EventDomainError::DependencyDomainMismatch {
1245 operation: EventDomainOperation::TransferBridge,
1246 node_index: Some(node_index),
1247 expected: source,
1248 actual,
1249 },
1250 ));
1251 }
1252 }
1253 ScheduledNode::Operation(_)
1254 | ScheduledNode::Collective(_)
1255 | ScheduledNode::Barrier(_) => {
1256 if actual != destination {
1257 return Err(event_domain_error(
1258 EventDomainError::DependencyDomainMismatch {
1259 operation: EventDomainOperation::Enqueue,
1260 node_index: Some(node_index),
1261 expected: destination,
1262 actual,
1263 },
1264 ));
1265 }
1266 admitted.push(dependency_completion);
1267 }
1268 }
1269 }
1270 Ok(admitted)
1271 }
1272
1273 fn run_index(&self, domain: super::EventDomainId) -> Result<usize> {
1274 if let Some(index) = self
1275 .runs
1276 .iter()
1277 .position(|run| run.requested_domain() == domain)
1278 {
1279 return Ok(index);
1280 }
1281 Err(missing_event_domain_driver(domain))
1282 }
1283
1284 pub(crate) fn drain(&mut self) -> Result<()> {
1285 let mut failures = Vec::new();
1286 for run in &mut self.runs {
1287 if let Err(error) = run.drain() {
1288 failures.push(error);
1289 }
1290 }
1291 let mut failures = failures.into_iter();
1292 let Some(mut error) = failures.next() else {
1293 return Ok(());
1294 };
1295 for failure in failures {
1296 error = Error::with_suppressed(error, failure);
1297 }
1298 Err(error)
1299 }
1300}
1301
1302#[derive(Debug)]
1303enum RuntimeOwnedEventDomainRunState {
1304 Pending(Box<dyn EventDomainRun>),
1305 Retired,
1306 Failed,
1307}
1308
1309#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1310enum EventDomainRunTerminalState {
1311 Retired,
1312 Failed,
1313}
1314
1315impl fmt::Display for EventDomainRunTerminalState {
1316 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1317 match self {
1318 Self::Retired => formatter.write_str("retired"),
1319 Self::Failed => formatter.write_str("failed"),
1320 }
1321 }
1322}
1323
1324#[derive(Debug)]
1325struct RuntimeOwnedEventDomainRun {
1326 requested_domain: super::EventDomainId,
1327 state: RuntimeOwnedEventDomainRunState,
1328}
1329
1330impl RuntimeOwnedEventDomainRun {
1331 fn new(requested_domain: super::EventDomainId, inner: Box<dyn EventDomainRun>) -> Self {
1332 Self {
1333 requested_domain,
1334 state: RuntimeOwnedEventDomainRunState::Pending(inner),
1335 }
1336 }
1337
1338 fn requested_domain(&self) -> super::EventDomainId {
1339 self.requested_domain
1340 }
1341
1342 fn domain(&self, operation: EventDomainOperation) -> Result<super::EventDomainId> {
1343 match &self.state {
1344 RuntimeOwnedEventDomainRunState::Pending(run) => Ok(run.domain()),
1345 RuntimeOwnedEventDomainRunState::Retired => Err(event_domain_run_state_error(
1346 operation,
1347 self.requested_domain,
1348 EventDomainRunTerminalState::Retired,
1349 )),
1350 RuntimeOwnedEventDomainRunState::Failed => Err(event_domain_run_state_error(
1351 operation,
1352 self.requested_domain,
1353 EventDomainRunTerminalState::Failed,
1354 )),
1355 }
1356 }
1357
1358 fn enqueue(
1359 &mut self,
1360 dependencies: &[Arc<dyn EventToken>],
1361 launch: &mut dyn FnMut() -> Result<()>,
1362 ) -> Result<Arc<dyn EventToken>> {
1363 match &mut self.state {
1364 RuntimeOwnedEventDomainRunState::Pending(run) => run.enqueue(dependencies, launch),
1365 RuntimeOwnedEventDomainRunState::Retired => Err(event_domain_run_state_error(
1366 EventDomainOperation::Enqueue,
1367 self.requested_domain,
1368 EventDomainRunTerminalState::Retired,
1369 )),
1370 RuntimeOwnedEventDomainRunState::Failed => Err(event_domain_run_state_error(
1371 EventDomainOperation::Enqueue,
1372 self.requested_domain,
1373 EventDomainRunTerminalState::Failed,
1374 )),
1375 }
1376 }
1377
1378 fn drain(&mut self) -> Result<()> {
1379 let run = match std::mem::replace(&mut self.state, RuntimeOwnedEventDomainRunState::Failed)
1380 {
1381 RuntimeOwnedEventDomainRunState::Pending(run) => run,
1382 RuntimeOwnedEventDomainRunState::Retired => {
1383 self.state = RuntimeOwnedEventDomainRunState::Retired;
1384 return Err(event_domain_run_state_error(
1385 EventDomainOperation::Drain,
1386 self.requested_domain,
1387 EventDomainRunTerminalState::Retired,
1388 ));
1389 }
1390 RuntimeOwnedEventDomainRunState::Failed => {
1391 self.state = RuntimeOwnedEventDomainRunState::Failed;
1392 return Err(event_domain_run_state_error(
1393 EventDomainOperation::Drain,
1394 self.requested_domain,
1395 EventDomainRunTerminalState::Failed,
1396 ));
1397 }
1398 };
1399 let domain = self.requested_domain;
1400 let mut run = run;
1401 let drain_result = catch_unwind(AssertUnwindSafe(|| run.drain()));
1402 drop_event_domain_run(run);
1403 match drain_result {
1404 Ok(Ok(())) => {
1405 self.state = RuntimeOwnedEventDomainRunState::Retired;
1406 Ok(())
1407 }
1408 Ok(Err(error)) => {
1409 self.state = RuntimeOwnedEventDomainRunState::Failed;
1410 Err(error)
1411 }
1412 Err(payload) => {
1413 self.state = RuntimeOwnedEventDomainRunState::Failed;
1414 Err(event_domain_error(EventDomainError::DrainPanicked {
1415 operation: EventDomainOperation::Drain,
1416 domain,
1417 message: safe_event_domain_panic_message(payload),
1418 }))
1419 }
1420 }
1421 }
1422}
1423
1424#[derive(Debug, thiserror::Error)]
1427#[error("{operation} used event-domain run {domain:?} after it reached terminal state {state}")]
1428pub(crate) struct EventDomainRunLifecycleError {
1429 operation: EventDomainOperation,
1430 domain: super::EventDomainId,
1431 state: EventDomainRunTerminalState,
1432}
1433
1434fn event_domain_run_state_error(
1435 operation: EventDomainOperation,
1436 domain: super::EventDomainId,
1437 state: EventDomainRunTerminalState,
1438) -> Error {
1439 Error::runtime_state_source(
1440 "Runtime::run_compiled",
1441 ErrorPhase::Execution,
1442 EventDomainRunLifecycleError {
1443 operation,
1444 domain,
1445 state,
1446 },
1447 )
1448}
1449
1450fn drop_event_domain_run(run: Box<dyn EventDomainRun>) {
1451 if catch_unwind(AssertUnwindSafe(|| drop(run))).is_err() {
1452 }
1454}
1455
1456impl Drop for RuntimeOwnedEventDomainRun {
1457 fn drop(&mut self) {
1458 let state = std::mem::replace(&mut self.state, RuntimeOwnedEventDomainRunState::Failed);
1459 if let RuntimeOwnedEventDomainRunState::Pending(run) = state {
1460 drop_event_domain_run(run);
1461 }
1462 }
1463}
1464
1465fn missing_event_domain_driver(domain: super::EventDomainId) -> Error {
1466 Error::from(EventDomainError::MissingDriver { domain })
1467}
1468
1469fn event_domain_error(source: EventDomainError) -> Error {
1470 Error::from(source)
1471}
1472
1473fn safe_event_domain_panic_message(payload: Box<dyn std::any::Any + Send + 'static>) -> String {
1474 match payload.downcast::<&'static str>() {
1475 Ok(message) => (*message).to_owned(),
1476 Err(payload) => match payload.downcast::<String>() {
1477 Ok(message) => *message,
1478 Err(_) => "non-string panic payload".to_owned(),
1479 },
1480 }
1481}
1482
1483fn scheduled_execution_cleanup_error(primary: Error, cleanup: Error) -> Error {
1484 Error::with_suppressed(primary, cleanup)
1485}
1486
1487fn instruction_execution<'a>(
1488 schedule: &'a ScheduledGraph,
1489 instruction: &ExecInstruction,
1490) -> Result<InstructionExecution<'a>> {
1491 let Some(operation_index) = instruction.semantic_operation_index else {
1492 let location = schedule.root_location();
1493 return Ok(InstructionExecution {
1494 witness: location.witness(),
1495 location,
1496 });
1497 };
1498 let location = schedule
1499 .operation_locations()
1500 .get(operation_index)
1501 .ok_or_else(|| {
1502 Error::runtime_state(
1503 "Runtime::run_compiled",
1504 ErrorPhase::Execution,
1505 format!(
1506 "instruction references semantic operation {operation_index}, but prepared schedule has {} operations",
1507 schedule.operation_locations().len()
1508 ),
1509 )
1510 })?;
1511 Ok(InstructionExecution {
1512 witness: location.witness(),
1513 location,
1514 })
1515}
1516
1517struct InstructionExecution<'a> {
1518 witness: &'a super::snapshot::ExecutableEngineSnapshot,
1519 location: &'a ExecutionLocation,
1520}
1521
1522impl InstructionExecution<'_> {
1523 fn executor(&self) -> &Arc<dyn ErasedTensorBackendExecutor> {
1524 self.witness.executor()
1525 }
1526
1527 fn location(&self) -> &ExecutionLocation {
1528 self.location
1529 }
1530}
1531
1532pub(crate) struct LocatedExecSlot<'input> {
1533 pub(crate) location: ExecutionLocation,
1534 pub(crate) value: ExecSlot<'input>,
1535}
1536
1537fn stage_instruction_inputs<'input>(
1538 instruction: &ExecInstruction,
1539 location: &ExecutionLocation,
1540 located: &mut [Vec<LocatedExecSlot<'input>>],
1541 staged: &mut [Option<ExecSlot<'input>>],
1542) -> Result<()> {
1543 for &slot in &instruction.input_slots {
1544 if staged
1545 .get(slot)
1546 .ok_or(tenferro_tensor::Error::MissingValue { slot })?
1547 .is_some()
1548 {
1549 continue;
1550 }
1551 let values = located
1552 .get_mut(slot)
1553 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
1554 let value_index = values
1555 .iter()
1556 .position(|value| &value.location == location)
1557 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
1558 staged[slot] = Some(values.swap_remove(value_index).value);
1559 }
1560 Ok(())
1561}
1562
1563fn validate_instruction_outputs(
1564 instruction_index: usize,
1565 instruction: &ExecInstruction,
1566 location: &ExecutionLocation,
1567 staged: &[Option<ExecSlot<'_>>],
1568) -> Result<()> {
1569 for &output_slot in &instruction.output_slots {
1570 let output = staged
1571 .get(output_slot)
1572 .and_then(Option::as_ref)
1573 .ok_or(tenferro_tensor::Error::MissingValue { slot: output_slot })?;
1574 let output = output.as_read();
1575 if !location
1576 .witness()
1577 .owns_resident_tensor(&output, location.storage_class())
1578 {
1579 return Err(Error::runtime_state_source(
1580 "Runtime::run_compiled",
1581 ErrorPhase::Execution,
1582 super::EngineExecutionContractError::OutputResidencyMismatch {
1583 instruction_index,
1584 output_slot,
1585 engine_id: location.engine_id().clone(),
1586 storage_class: location.storage_class().clone(),
1587 backend_family: output.backend_family(),
1588 allocation_domain: output.allocation_domain(),
1589 },
1590 ));
1591 }
1592 }
1593 Ok(())
1594}
1595
1596pub(crate) fn retain_instruction_results<'input>(
1597 instruction: &ExecInstruction,
1598 location: &ExecutionLocation,
1599 located: &mut [Vec<LocatedExecSlot<'input>>],
1600 staged: &mut [Option<ExecSlot<'input>>],
1601) -> Result<()> {
1602 for &slot in &instruction.input_slots {
1603 let is_output = instruction.output_slots.contains(&slot);
1604 let is_last_use = instruction
1605 .input_slots
1606 .iter()
1607 .enumerate()
1608 .any(|(index, &candidate)| {
1609 candidate == slot && instruction.last_use.get(index).copied().unwrap_or(false)
1610 });
1611 if is_last_use {
1612 located[slot].clear();
1613 if !is_output {
1614 staged[slot].take();
1615 }
1616 } else if !is_output {
1617 if let Some(value) = staged[slot].take() {
1618 located[slot].push(LocatedExecSlot {
1619 location: location.clone(),
1620 value,
1621 });
1622 }
1623 }
1624 }
1625
1626 for &slot in &instruction.output_slots {
1627 let value = staged
1628 .get_mut(slot)
1629 .and_then(Option::take)
1630 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
1631 let values = located
1632 .get_mut(slot)
1633 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
1634 values.clear();
1635 values.push(LocatedExecSlot {
1636 location: location.clone(),
1637 value,
1638 });
1639 }
1640 Ok(())
1641}
1642
1643fn validate_runtime_input_ingress(
1644 location: &ExecutionLocation,
1645 input: &TensorRead<'_>,
1646 slot: usize,
1647) -> Result<()> {
1648 let accepted = location
1649 .witness()
1650 .accepts_runtime_input(input, location.storage_class());
1651 if accepted {
1652 return Ok(());
1653 }
1654 Err(Error::runtime_state_source(
1655 "Runtime::run_compiled",
1656 ErrorPhase::Execution,
1657 super::InputIngressContractError::ResidencyMismatch {
1658 input_slot: slot,
1659 ingress_engine_id: location.engine_id().clone(),
1660 ingress_storage_class: location.storage_class().clone(),
1661 placement: input.placement().clone(),
1662 backend_family: input.backend_family(),
1663 allocation_domain: input.allocation_domain(),
1664 },
1665 ))
1666}
1667
1668fn execute_scheduled_transfer<'input>(
1669 transfer: &ScheduledTransfer,
1670 located: &mut [Vec<LocatedExecSlot<'input>>],
1671) -> Result<()> {
1672 let source = transfer.source_location();
1673 let destination = transfer.destination_location();
1674 let provider = transfer.provider();
1675 let values =
1676 located
1677 .get_mut(transfer.value_slot())
1678 .ok_or(tenferro_tensor::Error::MissingValue {
1679 slot: transfer.value_slot(),
1680 })?;
1681 if values.iter().any(|value| &value.location == destination) {
1682 return Err(Error::runtime_state_source(
1683 "Runtime::run_compiled",
1684 ErrorPhase::Execution,
1685 DuplicateTransferDestinationError {
1686 value_slot: transfer.value_slot(),
1687 destination: destination.clone(),
1688 },
1689 ));
1690 }
1691 let transferred = {
1692 let source_value = values
1693 .iter()
1694 .find(|value| &value.location == source)
1695 .ok_or(tenferro_tensor::Error::MissingValue {
1696 slot: transfer.value_slot(),
1697 })?;
1698 let source_read = source_value.value.as_read();
1699 let expected_dtype = source_read.dtype();
1700 let expected_shape = source_read.shape().to_vec();
1701 let transferred =
1702 provider.transfer_blocking(TransferRequest::new(source, destination, source_read))?;
1703 validate_transfer_output(destination, expected_dtype, &expected_shape, &transferred)?;
1704 transferred
1705 };
1706 values.push(LocatedExecSlot {
1707 location: destination.clone(),
1708 value: ExecSlot::Owned(transferred),
1709 });
1710 Ok(())
1711}
1712
1713fn validate_transfer_output(
1714 destination: &ExecutionLocation,
1715 expected_dtype: tenferro_tensor::DType,
1716 expected_shape: &[usize],
1717 output: &Tensor,
1718) -> Result<()> {
1719 let expected_elements = checked_transfer_element_count(expected_shape).map_err(|source| {
1720 Error::runtime_state_source(
1721 "Runtime::run_compiled",
1722 ErrorPhase::Execution,
1723 TransferError::ProviderContract { source },
1724 )
1725 })?;
1726 let contract_error = if output.dtype() != expected_dtype {
1727 Some(TransferProviderContractError::DTypeMismatch {
1728 expected: expected_dtype,
1729 actual: output.dtype(),
1730 })
1731 } else if output.shape() != expected_shape {
1732 Some(TransferProviderContractError::ShapeMismatch {
1733 expected: expected_shape.to_vec(),
1734 actual: output.shape().to_vec(),
1735 })
1736 } else if tensor_buffer_len(output) != expected_elements {
1737 Some(TransferProviderContractError::InvalidBufferLength {
1738 expected: expected_elements,
1739 actual: tensor_buffer_len(output),
1740 })
1741 } else if !destination
1742 .witness()
1743 .accepts_input_placement(output.placement(), destination.storage_class())
1744 {
1745 Some(
1746 TransferProviderContractError::DestinationPlacementMismatch {
1747 destination_engine_id: destination.engine_id().clone(),
1748 destination_storage_class: destination.storage_class().clone(),
1749 actual: output.placement().clone(),
1750 },
1751 )
1752 } else if !destination.witness().owns_resident_tensor(
1753 &TensorRead::from_tensor(output),
1754 destination.storage_class(),
1755 ) {
1756 Some(
1757 TransferProviderContractError::DestinationResidencyMismatch {
1758 destination_engine_id: destination.engine_id().clone(),
1759 destination_storage_class: destination.storage_class().clone(),
1760 actual_backend_family: TensorRead::from_tensor(output).backend_family(),
1761 actual_allocation_domain: TensorRead::from_tensor(output).allocation_domain(),
1762 },
1763 )
1764 } else {
1765 None
1766 };
1767 match contract_error {
1768 None => Ok(()),
1769 Some(source) => Err(Error::runtime_state_source(
1770 "Runtime::run_compiled",
1771 ErrorPhase::Execution,
1772 TransferError::ProviderContract { source },
1773 )),
1774 }
1775}
1776
1777fn checked_transfer_element_count(
1778 shape: &[usize],
1779) -> std::result::Result<usize, TransferProviderContractError> {
1780 tenferro_tensor::validate::checked_shape_product(
1781 "Runtime::run_compiled",
1782 "transfer source shape",
1783 shape,
1784 )
1785 .map_err(|source| TransferProviderContractError::LogicalElementCount { source })
1786}
1787
1788fn tensor_buffer_len(tensor: &Tensor) -> usize {
1789 match tensor {
1790 Tensor::F32(tensor) => tensor.buffer().len(),
1791 Tensor::F64(tensor) => tensor.buffer().len(),
1792 Tensor::I32(tensor) => tensor.buffer().len(),
1793 Tensor::I64(tensor) => tensor.buffer().len(),
1794 Tensor::Bool(tensor) => tensor.buffer().len(),
1795 Tensor::C32(tensor) => tensor.buffer().len(),
1796 Tensor::C64(tensor) => tensor.buffer().len(),
1797 }
1798}
1799
1800#[cfg(test)]
1801mod transfer_validation_tests {
1802 use std::error::Error as _;
1803
1804 use super::checked_transfer_element_count;
1805 use crate::TransferProviderContractError;
1806
1807 #[test]
1808 fn transfer_element_count_overflow_is_typed_and_preserves_source() {
1809 let error = checked_transfer_element_count(&[usize::MAX, 2]).unwrap_err();
1810
1811 assert!(matches!(
1812 error,
1813 TransferProviderContractError::LogicalElementCount { .. }
1814 ));
1815 assert!(error.source().is_some());
1816 }
1817}
1818
1819fn collect_located_outputs<'input>(
1820 program: &ExecProgram,
1821 located: &mut [Vec<LocatedExecSlot<'input>>],
1822) -> Result<Vec<Option<LocatedExecSlot<'input>>>> {
1823 let mut outputs = (0..program.n_slots).map(|_| None).collect::<Vec<_>>();
1824 for &slot in &program.output_slots {
1825 if outputs[slot].is_some() {
1826 continue;
1827 }
1828 let values = located
1829 .get_mut(slot)
1830 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
1831 let value = values
1832 .pop()
1833 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
1834 values.clear();
1835 outputs[slot] = Some(value);
1836 }
1837 located.iter_mut().for_each(Vec::clear);
1838 Ok(outputs)
1839}
1840
1841pub(super) fn collect_tensor_outputs_with<'input>(
1842 program: &ExecProgram,
1843 outputs: &mut [Option<LocatedExecSlot<'input>>],
1844 mut materialize: impl FnMut(&ExecutionLocation, ExecSlot<'input>) -> Result<Tensor>,
1845) -> Result<Vec<Tensor>> {
1846 program
1847 .output_slots
1848 .iter()
1849 .map(|&slot| {
1850 let located = outputs
1851 .get_mut(slot)
1852 .and_then(Option::take)
1853 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
1854 materialize(&located.location, located.value)
1855 })
1856 .collect()
1857}
1858
1859fn collect_value_outputs_with<'input>(
1860 program: &ExecProgram,
1861 outputs: &mut [Option<LocatedExecSlot<'input>>],
1862 mut materialize: impl FnMut(&ExecutionLocation, ExecSlot<'input>) -> Result<TensorValue>,
1863) -> Result<Vec<TensorValue>> {
1864 program
1865 .output_slots
1866 .iter()
1867 .map(|&slot| {
1868 let located = outputs
1869 .get_mut(slot)
1870 .and_then(Option::take)
1871 .ok_or(tenferro_tensor::Error::MissingValue { slot })?;
1872 materialize(&located.location, located.value)
1873 })
1874 .collect()
1875}
1876
1877fn input_signature(inputs: &[&Tensor]) -> Result<InputSignature> {
1878 let reads: RuntimeInputReads<'_> = inputs
1879 .iter()
1880 .map(|tensor| TensorRead::from_tensor(tensor))
1881 .collect();
1882 InputSignature::from_reads(&reads).map_err(|source| prepare_error(Arc::new(source)))
1883}
1884
1885fn resolve_input_refs<'a>(
1886 program: &'a CompiledGraph,
1887 inputs: &'a [&'a Tensor],
1888) -> Result<RuntimeInputRefs<'a>> {
1889 let resolved = if inputs.is_empty() {
1890 semantic_default_inputs(program)?
1891 } else {
1892 inputs.iter().copied().collect()
1893 };
1894 validate_ordered_input_metadata(program, &resolved)?;
1895 Ok(resolved)
1896}
1897
1898fn semantic_default_inputs(program: &CompiledGraph) -> Result<RuntimeInputRefs<'_>> {
1899 program
1900 .program()
1901 .inputs()
1902 .iter()
1903 .enumerate()
1904 .map(|(input_index, value)| {
1905 program
1906 .bindings()
1907 .tensor_ref_for_input(*value)
1908 .map(AsRef::as_ref)
1909 .ok_or_else(|| Error::UnboundPlaceholder {
1910 input_key: format!("semantic input {input_index}"),
1911 })
1912 })
1913 .collect()
1914}
1915
1916fn validate_ordered_input_metadata(program: &CompiledGraph, inputs: &[&Tensor]) -> Result<()> {
1917 let expected = program.input_count();
1918 if inputs.len() != expected {
1919 return Err(Error::GraphInputCountMismatch {
1920 expected,
1921 actual: inputs.len(),
1922 });
1923 }
1924 let input_shapes: RuntimeInputShapes<'_> = inputs.iter().map(|tensor| tensor.shape()).collect();
1925 for (input_value, actual) in program.program().inputs().iter().zip(inputs) {
1926 let metadata = program
1927 .program()
1928 .value_metadata(*input_value)
1929 .map_err(|source| {
1930 Error::runtime_state_source("Runtime::run_compiled", ErrorPhase::Execution, source)
1931 })?;
1932 if metadata.dtype() != actual.dtype() {
1933 return Err(Error::PlaceholderDtypeMismatch {
1934 expected: metadata.dtype(),
1935 actual: actual.dtype(),
1936 });
1937 }
1938 if metadata.shape().len() != actual.shape().len() {
1939 return Err(Error::PlaceholderRankMismatch {
1940 expected: metadata.shape().len(),
1941 actual: actual.shape().len(),
1942 });
1943 }
1944 let mut expected_shape: RuntimeShapeScratch = actual.shape().iter().copied().collect();
1945 let mut exact_mismatch = false;
1946 for (axis, (extent, actual_size)) in metadata.shape().iter().zip(actual.shape()).enumerate()
1947 {
1948 match extent {
1949 ShapeExtent::Exact(expression) => {
1950 let expected = expression.eval(&input_shapes).map_err(|source| {
1951 Error::runtime_state_source(
1952 "Runtime::run_compiled",
1953 ErrorPhase::Execution,
1954 source,
1955 )
1956 })?;
1957 expected_shape[axis] = expected;
1958 exact_mismatch |= expected != *actual_size;
1959 }
1960 ShapeExtent::UpperBound(expression) => {
1961 let bound = expression.eval(&input_shapes).map_err(|source| {
1962 Error::runtime_state_source(
1963 "Runtime::run_compiled",
1964 ErrorPhase::Execution,
1965 source,
1966 )
1967 })?;
1968 if *actual_size > bound {
1969 return Err(Error::PlaceholderShapeBoundExceeded {
1970 axis,
1971 bound,
1972 actual: *actual_size,
1973 });
1974 }
1975 }
1976 ShapeExtent::Unknown => {}
1977 }
1978 }
1979 if exact_mismatch {
1980 return Err(Error::PlaceholderShapeMismatch {
1981 expected: expected_shape.into_vec(),
1982 actual: actual.shape().to_vec(),
1983 });
1984 }
1985 }
1986 Ok(())
1987}
1988
1989fn validate_exec_input_count(program: &ExecProgram, actual: usize) -> Result<()> {
1990 let expected = program.input_slots.len();
1991 if actual != expected {
1992 return Err(Error::runtime_state(
1993 "Runtime::run_compiled",
1994 ErrorPhase::Execution,
1995 format!("expected {expected} inputs for execution program, got {actual}"),
1996 ));
1997 }
1998 Ok(())
1999}
2000
2001fn prepare_error(source: Arc<PrepareError>) -> Error {
2002 Error::runtime_state_source(
2003 "Runtime::run_compiled",
2004 ErrorPhase::Execution,
2005 SharedPrepareError(source),
2006 )
2007}
2008
2009#[derive(Clone, Debug)]
2010struct SharedPrepareError(Arc<PrepareError>);
2011
2012impl fmt::Display for SharedPrepareError {
2013 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2014 write!(formatter, "{}", self.0)
2015 }
2016}
2017
2018impl StdError for SharedPrepareError {
2019 fn source(&self) -> Option<&(dyn StdError + 'static)> {
2020 Some(self.0.as_ref())
2021 }
2022}
2023
2024#[cfg(test)]
2025mod tests {
2026 use std::any::Any;
2027 use std::error::Error as StdError;
2028 use std::hash::Hasher;
2029 use std::num::NonZeroU64;
2030 use std::sync::atomic::{AtomicBool, Ordering};
2031 use std::sync::{Arc, TryLockError, Weak};
2032
2033 use tenferro_cpu::CpuBackend;
2034 use tenferro_ops::dim_expr::DimExpr;
2035 use tenferro_ops::ext_op::ExtensionOp;
2036 use tenferro_ops::SymDim;
2037 use tenferro_tensor::{
2038 BackendSession, BackendSessionHost, DType, Tensor, TensorBackend, TensorRead, TypedTensor,
2039 };
2040
2041 use crate::exec::{ExecInstruction, ExecOp, ExecProgram, ExecSlot};
2042 use crate::runtime::{
2043 CoreCapabilityBundle, EngineId, ErasedExecutionContext, EventDomainDriver, EventDomainId,
2044 ExecutableEngineContract, ExecutionContextIdentity, HardwareClassId, InputIngressContract,
2045 InputSignature, PreparedOperation, PreparedOperationBinding, PreparedOperationExecutor,
2046 PreparedOperationPlan, ProviderDeviceIdentity, ProviderId, RegistrationIdentity,
2047 RuntimeCacheOwner, RuntimeEpoch, RuntimeId, SpecializationProjection,
2048 SpecializationRequirements, StorageClass,
2049 };
2050 use crate::{Error, ErrorPhase, ExtensionCacheStore, Result};
2051
2052 use super::{
2053 DuplicateTransferDestinationError, ErasedTensorBackendExecutor, LocatedExecSlot,
2054 TensorBackendExecutor,
2055 };
2056 use crate::runtime::schedule::{
2057 EventCompletion, EventSlotId, ExecutionLocation, ScheduledTransfer,
2058 };
2059
2060 const LOCK_PROBE_FAMILY: &str = "runtime.lock-probe.v1";
2061 const REENTRANT_PROBE_FAMILY: &str = "runtime.reentrant-probe.v1";
2062
2063 #[derive(Clone, Debug)]
2064 struct LockProbeOp;
2065
2066 impl ExtensionOp for LockProbeOp {
2067 fn family_id(&self) -> &'static str {
2068 LOCK_PROBE_FAMILY
2069 }
2070
2071 fn payload_hash(&self, _hasher: &mut dyn Hasher) {}
2072
2073 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
2074 other.as_any().downcast_ref::<Self>().is_some()
2075 }
2076
2077 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
2078 Arc::new(self.clone())
2079 }
2080
2081 fn as_any(&self) -> &dyn Any {
2082 self
2083 }
2084
2085 fn input_count(&self) -> usize {
2086 1
2087 }
2088
2089 fn output_count(&self) -> usize {
2090 1
2091 }
2092
2093 fn infer_output_meta(
2094 &self,
2095 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
2096 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
2097 Ok(vec![(ctx.input_dtype(0)?, ctx.input_shape(0)?.to_vec())])
2098 }
2099 }
2100
2101 #[derive(Clone, Debug)]
2102 struct ReentrantProbeOp;
2103
2104 impl ExtensionOp for ReentrantProbeOp {
2105 fn family_id(&self) -> &'static str {
2106 REENTRANT_PROBE_FAMILY
2107 }
2108
2109 fn payload_hash(&self, _hasher: &mut dyn Hasher) {}
2110
2111 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
2112 other.as_any().downcast_ref::<Self>().is_some()
2113 }
2114
2115 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
2116 Arc::new(self.clone())
2117 }
2118
2119 fn as_any(&self) -> &dyn Any {
2120 self
2121 }
2122
2123 fn input_count(&self) -> usize {
2124 1
2125 }
2126
2127 fn output_count(&self) -> usize {
2128 1
2129 }
2130
2131 fn infer_output_meta(
2132 &self,
2133 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
2134 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
2135 Ok(vec![(ctx.input_dtype(0)?, ctx.input_shape(0)?.to_vec())])
2136 }
2137 }
2138
2139 #[derive(Debug)]
2140 struct LockProbePreparedOperation {
2141 binding: PreparedOperationBinding,
2142 specialization: SpecializationProjection,
2143 executor: Weak<TensorBackendExecutor<CpuBackend>>,
2144 observed_unlocked_state: Arc<AtomicBool>,
2145 }
2146
2147 impl PreparedOperation for LockProbePreparedOperation {
2148 fn binding(&self) -> &PreparedOperationBinding {
2149 &self.binding
2150 }
2151
2152 fn specialization(&self) -> &SpecializationProjection {
2153 &self.specialization
2154 }
2155
2156 fn retained_bytes(&self) -> usize {
2157 0
2158 }
2159 }
2160
2161 impl PreparedOperationExecutor for LockProbePreparedOperation {
2162 fn execute(
2163 &self,
2164 context: &mut ErasedExecutionContext<'_>,
2165 _extension_caches: &mut ExtensionCacheStore,
2166 inputs: &[TensorRead<'_>],
2167 ) -> Result<Vec<Tensor>> {
2168 let executor = self.executor.upgrade().expect("executor still alive");
2169 let unlocked = match executor.state.try_lock() {
2170 Ok(_guard) => true,
2171 Err(TryLockError::WouldBlock) => false,
2172 Err(TryLockError::Poisoned(_)) => false,
2173 };
2174 self.observed_unlocked_state
2175 .store(unlocked, Ordering::SeqCst);
2176 let backend = context
2177 .downcast_mut::<CpuBackend>(self.binding.context_identity())
2178 .map_err(|source| {
2179 Error::runtime_state_source("lock_probe", ErrorPhase::Execution, source)
2180 })?;
2181 Ok(vec![backend.with_backend_session(|exec| {
2182 exec.to_contiguous_read(inputs[0].clone())
2183 })?])
2184 }
2185 }
2186
2187 #[derive(Debug)]
2188 struct ReentrantProbePreparedOperation {
2189 binding: PreparedOperationBinding,
2190 specialization: SpecializationProjection,
2191 executor: Weak<TensorBackendExecutor<CpuBackend>>,
2192 observed_reentrant_error: Arc<AtomicBool>,
2193 }
2194
2195 #[derive(Debug)]
2196 struct SessionProbePreparedOperation {
2197 binding: PreparedOperationBinding,
2198 specialization: SpecializationProjection,
2199 observed_session: Arc<AtomicBool>,
2200 }
2201
2202 impl PreparedOperation for SessionProbePreparedOperation {
2203 fn binding(&self) -> &PreparedOperationBinding {
2204 &self.binding
2205 }
2206
2207 fn specialization(&self) -> &SpecializationProjection {
2208 &self.specialization
2209 }
2210
2211 fn retained_bytes(&self) -> usize {
2212 0
2213 }
2214 }
2215
2216 impl PreparedOperationExecutor for SessionProbePreparedOperation {
2217 fn execute(
2218 &self,
2219 _context: &mut ErasedExecutionContext<'_>,
2220 _extension_caches: &mut ExtensionCacheStore,
2221 _inputs: &[TensorRead<'_>],
2222 ) -> Result<Vec<Tensor>> {
2223 Err(Error::unsupported(
2224 "session_probe",
2225 ErrorPhase::Execution,
2226 "session probe must use the scheduler-owned session path",
2227 ))
2228 }
2229
2230 fn supports_session(&self) -> bool {
2231 true
2232 }
2233
2234 fn execute_in_session(
2235 &self,
2236 session: &mut dyn BackendSession,
2237 _extension_caches: &mut ExtensionCacheStore,
2238 inputs: &[TensorRead<'_>],
2239 ) -> Result<Vec<Tensor>> {
2240 self.observed_session.store(true, Ordering::SeqCst);
2241 Ok(vec![session.to_contiguous_read(inputs[0].clone())?])
2242 }
2243 }
2244
2245 impl ReentrantProbePreparedOperation {
2246 fn probe_reentrant_call(&self, input: Tensor) {
2247 let executor = self.executor.upgrade().expect("executor still alive");
2248 let nested = ErasedTensorBackendExecutor::execute(
2249 executor.as_ref(),
2250 &passthrough_program(),
2251 &[],
2252 vec![input],
2253 );
2254 let observed = nested.is_err_and(|error| {
2255 error
2256 .to_string()
2257 .contains("reentrant tensor backend executor call would deadlock")
2258 });
2259 self.observed_reentrant_error
2260 .store(observed, Ordering::SeqCst);
2261 }
2262 }
2263
2264 impl PreparedOperation for ReentrantProbePreparedOperation {
2265 fn binding(&self) -> &PreparedOperationBinding {
2266 &self.binding
2267 }
2268
2269 fn specialization(&self) -> &SpecializationProjection {
2270 &self.specialization
2271 }
2272
2273 fn retained_bytes(&self) -> usize {
2274 0
2275 }
2276 }
2277
2278 impl PreparedOperationExecutor for ReentrantProbePreparedOperation {
2279 fn execute(
2280 &self,
2281 context: &mut ErasedExecutionContext<'_>,
2282 _extension_caches: &mut ExtensionCacheStore,
2283 inputs: &[TensorRead<'_>],
2284 ) -> Result<Vec<Tensor>> {
2285 let backend = context
2286 .downcast_mut::<CpuBackend>(self.binding.context_identity())
2287 .map_err(|source| {
2288 Error::runtime_state_source("reentrant_probe", ErrorPhase::Execution, source)
2289 })?;
2290 let materialized =
2291 backend.with_backend_session(|exec| exec.to_contiguous_read(inputs[0].clone()))?;
2292 self.probe_reentrant_call(materialized.clone());
2293 Ok(vec![materialized])
2294 }
2295 }
2296
2297 fn lock_probe_program() -> ExecProgram {
2298 ExecProgram {
2299 instructions: vec![ExecInstruction {
2300 op: ExecOp::Extension(Arc::new(LockProbeOp)),
2301 semantic_operation_index: Some(0),
2302 input_slots: vec![0],
2303 output_slots: vec![1],
2304 dtype: DType::F64,
2305 output_shapes: vec![vec![DimExpr::InputDim {
2306 input_idx: 0,
2307 axis: 0,
2308 }]]
2309 .into(),
2310 output_extents: vec![vec![]].into(),
2311 last_use: vec![false],
2312 }],
2313 input_slots: vec![0],
2314 output_slots: vec![1],
2315 n_slots: 2,
2316 shape_guards: vec![],
2317 }
2318 }
2319
2320 fn partial_session_probe_program() -> ExecProgram {
2321 let output_shape = || {
2322 vec![DimExpr::InputDim {
2323 input_idx: 0,
2324 axis: 0,
2325 }]
2326 };
2327 ExecProgram {
2328 instructions: vec![
2329 ExecInstruction {
2330 op: ExecOp::Extension(Arc::new(LockProbeOp)),
2331 semantic_operation_index: Some(0),
2332 input_slots: vec![0],
2333 output_slots: vec![1],
2334 dtype: DType::F64,
2335 output_shapes: vec![output_shape()].into(),
2336 output_extents: vec![vec![]].into(),
2337 last_use: vec![false],
2338 },
2339 ExecInstruction {
2340 op: ExecOp::Extension(Arc::new(LockProbeOp)),
2341 semantic_operation_index: Some(1),
2342 input_slots: vec![1],
2343 output_slots: vec![2],
2344 dtype: DType::F64,
2345 output_shapes: vec![output_shape()].into(),
2346 output_extents: vec![vec![]].into(),
2347 last_use: vec![false],
2348 },
2349 ],
2350 input_slots: vec![0],
2351 output_slots: vec![2],
2352 n_slots: 3,
2353 shape_guards: vec![],
2354 }
2355 }
2356
2357 fn reentrant_probe_program() -> ExecProgram {
2358 ExecProgram {
2359 instructions: vec![ExecInstruction {
2360 op: ExecOp::Extension(Arc::new(ReentrantProbeOp)),
2361 semantic_operation_index: Some(0),
2362 input_slots: vec![0],
2363 output_slots: vec![1],
2364 dtype: DType::F64,
2365 output_shapes: vec![vec![DimExpr::InputDim {
2366 input_idx: 0,
2367 axis: 0,
2368 }]]
2369 .into(),
2370 output_extents: vec![vec![]].into(),
2371 last_use: vec![false],
2372 }],
2373 input_slots: vec![0],
2374 output_slots: vec![1],
2375 n_slots: 2,
2376 shape_guards: vec![],
2377 }
2378 }
2379
2380 fn passthrough_program() -> ExecProgram {
2381 ExecProgram {
2382 instructions: vec![],
2383 input_slots: vec![0],
2384 output_slots: vec![0],
2385 n_slots: 1,
2386 shape_guards: vec![],
2387 }
2388 }
2389
2390 fn f64_zeros(shape: Vec<usize>) -> Tensor {
2391 Tensor::F64(TypedTensor::zeros(shape).unwrap())
2392 }
2393
2394 fn nz(value: u64) -> NonZeroU64 {
2395 NonZeroU64::new(value).unwrap_or(NonZeroU64::MIN)
2396 }
2397
2398 fn probe_binding() -> PreparedOperationBinding {
2399 PreparedOperationBinding::new(
2400 RuntimeId::from_nonzero(nz(1)),
2401 RuntimeEpoch::from_nonzero(nz(2)),
2402 EngineId::new("tenferro.cpu").unwrap(),
2403 RegistrationIdentity::new(nz(3), nz(4)),
2404 ExecutionContextIdentity::of::<CpuBackend>(),
2405 HardwareClassId::new("tenferro.cpu.host").unwrap(),
2406 )
2407 }
2408
2409 fn probe_specialization() -> SpecializationProjection {
2410 SpecializationRequirements::polymorphic(0)
2411 .project(&InputSignature::new(Vec::new()))
2412 .unwrap()
2413 }
2414
2415 #[test]
2416 fn tensor_backend_executor_releases_state_lock_during_extension_execution() {
2417 let executor = Arc::new(TensorBackendExecutor::<CpuBackend>::new(CpuBackend::new()));
2418 let observed_unlocked_state = Arc::new(AtomicBool::new(false));
2419 let prepared = Arc::new(LockProbePreparedOperation {
2420 binding: probe_binding(),
2421 specialization: probe_specialization(),
2422 executor: Arc::downgrade(&executor),
2423 observed_unlocked_state: Arc::clone(&observed_unlocked_state),
2424 });
2425 let operations = vec![PreparedOperationPlan::executable(
2426 prepared.clone(),
2427 prepared,
2428 )];
2429
2430 let output = ErasedTensorBackendExecutor::execute(
2431 executor.as_ref(),
2432 &lock_probe_program(),
2433 &operations,
2434 vec![f64_zeros(vec![2])],
2435 )
2436 .expect("extension executes");
2437
2438 assert_eq!(output[0].shape(), &[2]);
2439 assert!(
2440 observed_unlocked_state.load(Ordering::SeqCst),
2441 "executor state lock must not be held while extension runtime callbacks execute"
2442 );
2443 }
2444
2445 #[test]
2446 fn scheduled_execution_cleanup_error_preserves_primary_error() {
2447 let primary = Error::runtime_state(
2448 "primary-execution",
2449 ErrorPhase::Execution,
2450 "primary execution failure",
2451 );
2452 let cleanup = Error::runtime_state(
2453 "event-domain-cleanup",
2454 ErrorPhase::Execution,
2455 "cleanup failure",
2456 );
2457 let primary_display = primary.to_string();
2458
2459 let combined = super::scheduled_execution_cleanup_error(primary, cleanup);
2460 let primary_error = combined.primary().expect("primary execution error");
2461 let cleanup_error = combined.suppressed().expect("cleanup error");
2462 assert_eq!(primary_error.to_string(), primary_display);
2463 assert_eq!(
2464 cleanup_error.to_string(),
2465 "event-domain-cleanup (Execution): runtime state failure: cleanup failure"
2466 );
2467 assert_eq!(
2468 StdError::source(&combined)
2469 .expect("primary error in the standard source chain")
2470 .to_string(),
2471 primary_display
2472 );
2473 }
2474
2475 #[test]
2476 fn duplicate_scheduled_transfer_destination_reports_typed_fields() {
2477 let domain = EventDomainId::runtime_created_for_test(
2478 RuntimeId::from_nonzero(nz(1)),
2479 RuntimeEpoch::from_nonzero(nz(1)),
2480 RegistrationIdentity::new(nz(1), nz(1)),
2481 );
2482 let source = ExecutionLocation::new(
2483 EngineId::new("tenferro-test.transfer-source").expect("source engine"),
2484 ProviderDeviceIdentity::new(
2485 ProviderId::new("tenferro-test.transfer").expect("provider id"),
2486 "source",
2487 )
2488 .expect("source provider target"),
2489 domain,
2490 StorageClass::new("tenferro-test.transfer-source").expect("source storage"),
2491 );
2492 let destination = ExecutionLocation::new(
2493 EngineId::new("tenferro-test.transfer-destination").expect("destination engine"),
2494 ProviderDeviceIdentity::new(
2495 ProviderId::new("tenferro-test.transfer").expect("provider id"),
2496 "destination",
2497 )
2498 .expect("destination provider target"),
2499 domain,
2500 StorageClass::new("tenferro-test.transfer-destination").expect("destination storage"),
2501 );
2502 let transfer = ScheduledTransfer::new(
2503 3,
2504 source,
2505 destination.clone(),
2506 [],
2507 EventCompletion::new(domain, EventSlotId::new(0), 0),
2508 );
2509 let mut located = vec![
2510 Vec::new(),
2511 Vec::new(),
2512 Vec::new(),
2513 vec![LocatedExecSlot {
2514 location: destination.clone(),
2515 value: ExecSlot::Owned(f64_zeros(vec![1])),
2516 }],
2517 ];
2518
2519 let error = super::execute_scheduled_transfer(&transfer, &mut located)
2520 .expect_err("duplicate transfer destination");
2521 let Error::RuntimeStateSource { source, .. } = error else {
2522 panic!("duplicate transfer destination must retain a typed source");
2523 };
2524 let duplicate = source
2525 .downcast_ref::<DuplicateTransferDestinationError>()
2526 .expect("typed duplicate transfer destination source");
2527 assert_eq!(duplicate.value_slot, 3);
2528 assert_eq!(duplicate.destination, destination);
2529 }
2530
2531 #[test]
2532 fn tensor_backend_executor_reentrant_call_returns_error_instead_of_deadlocking() {
2533 let executor = Arc::new(TensorBackendExecutor::<CpuBackend>::new(CpuBackend::new()));
2534 let observed_reentrant_error = Arc::new(AtomicBool::new(false));
2535 let prepared = Arc::new(ReentrantProbePreparedOperation {
2536 binding: probe_binding(),
2537 specialization: probe_specialization(),
2538 executor: Arc::downgrade(&executor),
2539 observed_reentrant_error: Arc::clone(&observed_reentrant_error),
2540 });
2541 let operations = vec![PreparedOperationPlan::executable(
2542 prepared.clone(),
2543 prepared,
2544 )];
2545
2546 let output = ErasedTensorBackendExecutor::execute(
2547 executor.as_ref(),
2548 &reentrant_probe_program(),
2549 &operations,
2550 vec![f64_zeros(vec![2])],
2551 )
2552 .expect("outer extension executes");
2553
2554 assert_eq!(output[0].shape(), &[2]);
2555 assert!(
2556 observed_reentrant_error.load(Ordering::SeqCst),
2557 "same-thread reentrant executor call must fail immediately instead of deadlocking"
2558 );
2559 }
2560
2561 #[test]
2562 fn tensor_backend_executor_dispatches_session_capable_extension_in_one_session_path() {
2563 let executor = TensorBackendExecutor::<CpuBackend>::new(CpuBackend::new());
2564 let observed_session = Arc::new(AtomicBool::new(false));
2565 let prepared = Arc::new(SessionProbePreparedOperation {
2566 binding: probe_binding(),
2567 specialization: probe_specialization(),
2568 observed_session: Arc::clone(&observed_session),
2569 });
2570 let operations = vec![PreparedOperationPlan::executable(
2571 prepared.clone(),
2572 prepared,
2573 )];
2574
2575 let output = ErasedTensorBackendExecutor::execute(
2576 &executor,
2577 &lock_probe_program(),
2578 &operations,
2579 vec![f64_zeros(vec![2])],
2580 )
2581 .expect("session-capable extension executes");
2582
2583 assert_eq!(output[0].shape(), &[2]);
2584 assert!(observed_session.load(Ordering::SeqCst));
2585 }
2586
2587 #[test]
2588 fn tensor_backend_executor_batches_session_capable_region_after_boundary() {
2589 let executor = Arc::new(TensorBackendExecutor::<CpuBackend>::new(CpuBackend::new()));
2590 let observed_unlocked_state = Arc::new(AtomicBool::new(false));
2591 let observed_session = Arc::new(AtomicBool::new(false));
2592 let ordinary = Arc::new(LockProbePreparedOperation {
2593 binding: probe_binding(),
2594 specialization: probe_specialization(),
2595 executor: Arc::downgrade(&executor),
2596 observed_unlocked_state: Arc::clone(&observed_unlocked_state),
2597 });
2598 let session = Arc::new(SessionProbePreparedOperation {
2599 binding: probe_binding(),
2600 specialization: probe_specialization(),
2601 observed_session: Arc::clone(&observed_session),
2602 });
2603 let operations = vec![
2604 PreparedOperationPlan::executable(ordinary.clone(), ordinary),
2605 PreparedOperationPlan::executable(session.clone(), session),
2606 ];
2607
2608 let output = ErasedTensorBackendExecutor::execute(
2609 executor.as_ref(),
2610 &partial_session_probe_program(),
2611 &operations,
2612 vec![f64_zeros(vec![2])],
2613 )
2614 .expect("mixed session regions execute");
2615
2616 assert_eq!(output[0].shape(), &[2]);
2617 assert!(observed_unlocked_state.load(Ordering::SeqCst));
2618 assert!(observed_session.load(Ordering::SeqCst));
2619
2620 let values = ErasedTensorBackendExecutor::execute_values(
2621 executor.as_ref(),
2622 &partial_session_probe_program(),
2623 &operations,
2624 vec![f64_zeros(vec![2])],
2625 )
2626 .expect("mixed session regions execute in value mode");
2627 assert_eq!(values[0].shape(), &[2]);
2628 }
2629
2630 #[test]
2631 fn tensor_backend_executor_bridge_does_not_require_clone_source_contract() {
2632 type ContractConstructor<B> = fn(
2633 ProviderDeviceIdentity,
2634 CoreCapabilityBundle,
2635 B,
2636 Arc<dyn EventDomainDriver>,
2637 InputIngressContract,
2638 Option<Arc<dyn RuntimeCacheOwner>>,
2639 ) -> ExecutableEngineContract;
2640
2641 fn factory_accepts_backend_without_clone_bound<B>()
2642 where
2643 B: TensorBackend + Send + Sync + 'static,
2644 {
2645 let _factory: fn(B) -> Arc<dyn ErasedTensorBackendExecutor> =
2646 super::erased_tensor_backend_executor::<B>;
2647 }
2648
2649 fn contract_accepts_backend_without_clone_bound<B>()
2650 where
2651 B: TensorBackend + Send + Sync + 'static,
2652 {
2653 let _constructor: ContractConstructor<B> = ExecutableEngineContract::new::<B>;
2654 }
2655
2656 factory_accepts_backend_without_clone_bound::<CpuBackend>();
2657 contract_accepts_backend_without_clone_bound::<CpuBackend>();
2658 }
2659}