1use std::sync::Arc;
4
5use computegraph::GraphOperation;
6use tenferro_ops::std_tensor_op::StdTensorOp;
7use tenferro_runtime::{
8 Error, ErrorPhase, ExtensionModule, InputSignature, PrepareCapability, PrepareError,
9 PreparedOperationExecutorHandle, Result, Runtime, RuntimeConfigError,
10};
11use tenferro_tensor::{Tensor, TensorRead, TensorValue};
12
13use crate::eager::{
14 eager_capture_active, eager_grad_recording_enabled, execute_prepared_extension_on,
15 EagerRuntime, EagerSession, EagerTensor,
16};
17use tenferro_runtime::ExecutionEngine;
18
19pub use tenferro_runtime::extension::{
20 apply, ExtensionCacheKey, ExtensionCacheLimits, ExtensionCacheSelector, ExtensionCacheStore,
21 ExtensionExecutionContext, ExtensionFamilyId, ExtensionOp,
22};
23
24#[doc(hidden)]
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum EagerExtensionBackendKind {
46 Cpu,
48 #[cfg(feature = "cuda")]
50 Cuda,
51}
52
53#[doc(hidden)]
69#[derive(Clone, Debug, Eq, PartialEq)]
70pub struct EagerExtensionTarget {
71 pub engine_id: tenferro_runtime::EngineId,
73 pub backend_kind: EagerExtensionBackendKind,
75}
76
77#[cfg(test)]
78mod tests;
79
80#[doc(hidden)]
89pub fn prepare_eager_in_place_input(
90 engine: &mut dyn ExecutionEngine,
91 input: &EagerTensor,
92 family_id: &'static str,
93 module_factory: impl FnOnce(EagerExtensionTarget) -> Result<Arc<dyn ExtensionModule>>,
94) -> Result<()> {
95 if input.requires_grad || input.trace.is_some() || eager_capture_active() {
96 return Err(Error::runtime_state(
97 "eager in-place",
98 ErrorPhase::Execution,
99 "in-place execution requires an untracked value outside trace capture",
100 ));
101 }
102 let target = input.ctx.eager_extension_target(engine)?;
103 validate_eager_extension_input_signature(&input.ctx, &target, &[input.tensor_read()])?;
104 let module = module_factory(target.clone())?;
105 input
106 .ctx
107 .ensure_extension_module_for_engine(module, family_id, &target.engine_id)?;
108 Ok(())
109}
110
111#[must_use = "the adopted eager tensor carries the runtime value"]
132pub fn adopt_untracked_eager_value(
133 ctx: Arc<EagerRuntime>,
134 value: TensorValue,
135) -> Result<EagerTensor> {
136 EagerTensor::new_untracked_value_result(ctx, value)
137}
138
139pub fn apply_eager(
147 engine: &mut dyn ExecutionEngine,
148 op: Arc<dyn ExtensionOp>,
149 inputs: &[&EagerTensor],
150) -> Result<Vec<EagerTensor>> {
151 let ctx = validate_eager_extension_inputs(op.as_ref(), inputs)?;
152 ctx.with_eager_session(engine, |session| {
153 apply_eager_in_session(session, op, inputs)
154 })
155}
156
157fn apply_eager_in_session(
160 session: &mut EagerSession<'_>,
161 op: Arc<dyn ExtensionOp>,
162 inputs: &[&EagerTensor],
163) -> Result<Vec<EagerTensor>> {
164 let ctx = validate_eager_extension_inputs(op.as_ref(), inputs)?;
165 ensure_session_runtime(session, &ctx)?;
166 let std_op = StdTensorOp::Extension(Arc::clone(&op));
167 let input_reads: Vec<_> = inputs.iter().map(|tensor| tensor.tensor_read()).collect();
168 let outputs = match try_prepared_eager_extension(session, &std_op, &input_reads)? {
169 Some(outputs) => outputs,
170 None => ctx.exec_extension_outputs_read(session.engine(), &op, &input_reads)?,
171 };
172 finish_eager_extension_outputs(ctx, std_op, inputs, outputs, session)
173}
174
175fn ensure_session_runtime(session: &EagerSession<'_>, ctx: &Arc<EagerRuntime>) -> Result<()> {
176 if !Arc::ptr_eq(session.runtime(), ctx) {
177 return Err(Error::ContextMismatch {
178 lhs: session.runtime().id(),
179 rhs: ctx.id(),
180 });
181 }
182 Ok(())
183}
184
185fn execute_prepared_executor(
188 engine: &mut dyn ExecutionEngine,
189 executor: &dyn tenferro_runtime::PreparedOperationExecutor,
190 input_reads: &[TensorRead<'_>],
191) -> Result<Vec<Tensor>> {
192 if executor.supports_session() {
193 return execute_prepared_extension_on(engine, executor, input_reads);
194 }
195 let identity = engine.target().context_identity();
196 crate::eager::with_lent_extension_caches(engine, |engine, caches| {
197 let mut erased =
198 tenferro_runtime::ErasedExecutionContext::from_dyn(identity, engine.as_any_mut());
199 executor.execute(&mut erased, caches, input_reads)
200 })
201}
202
203fn try_prepared_eager_extension(
204 session: &mut EagerSession<'_>,
205 op: &StdTensorOp,
206 input_reads: &[TensorRead<'_>],
207) -> Result<Option<Vec<Tensor>>> {
208 let ctx = Arc::clone(session.runtime());
209 let Ok(target) = ctx.eager_extension_target(session.engine()) else {
212 return Ok(None);
213 };
214 let Some(executor) = prepared_eager_extension_executor(&ctx, &target, op, input_reads)? else {
215 return Ok(None);
216 };
217 Ok(Some(execute_prepared_executor(
218 session.engine(),
219 executor.as_ref(),
220 input_reads,
221 )?))
222}
223
224fn prepared_eager_extension_executor(
225 ctx: &EagerRuntime,
226 target: &EagerExtensionTarget,
227 op: &StdTensorOp,
228 input_reads: &[TensorRead<'_>],
229) -> Result<Option<PreparedOperationExecutorHandle>> {
230 let StdTensorOp::Extension(ext) = op else {
231 return Ok(None);
232 };
233 let signature = InputSignature::from_reads(input_reads).map_err(|source| {
234 Error::runtime_state_source("extension::apply_eager", ErrorPhase::Execution, source)
235 })?;
236 let PrepareCapability::Prepared(plan) =
237 ctx.runtime()
238 .prepare_extension_immediate(&target.engine_id, ext.as_ref(), &signature)?
239 else {
240 return Ok(None);
241 };
242 Ok(plan.executor().cloned())
243}
244
245#[doc(hidden)]
252pub fn apply_eager_with_extension_session(
253 engine: &mut dyn ExecutionEngine,
254 op: Arc<dyn ExtensionOp>,
255 inputs: &[&EagerTensor],
256 module: Arc<dyn ExtensionModule>,
257) -> Result<Vec<EagerTensor>> {
258 let ctx = validate_eager_extension_inputs(op.as_ref(), inputs)?;
259 ctx.install_extension_module(module)?;
260 apply_eager(engine, op, inputs)
261}
262
263#[doc(hidden)]
270pub fn apply_eager_with_targeted_extension_session(
271 engine: &mut dyn ExecutionEngine,
272 op: Arc<dyn ExtensionOp>,
273 inputs: &[&EagerTensor],
274 module_factory: impl FnOnce(
275 EagerExtensionTarget,
276 ) -> tenferro_runtime::Result<Arc<dyn ExtensionModule>>,
277) -> Result<Vec<EagerTensor>> {
278 let ctx = validate_eager_extension_inputs(op.as_ref(), inputs)?;
279 let target = ctx.eager_extension_target(engine)?;
280 let input_reads: Vec<_> = inputs.iter().map(|tensor| tensor.tensor_read()).collect();
281 validate_eager_extension_input_signature(&ctx, &target, &input_reads)?;
282 let module = module_factory(target.clone())?;
283 ctx.ensure_extension_module_for_engine(module, op.family_id(), &target.engine_id)?;
284 apply_eager(engine, op, inputs)
285}
286
287#[doc(hidden)]
294pub fn apply_eager_with_targeted_extension_in_session(
295 session: &mut EagerSession<'_>,
296 op: Arc<dyn ExtensionOp>,
297 inputs: &[&EagerTensor],
298 module_factory: impl FnOnce(
299 EagerExtensionTarget,
300 ) -> tenferro_runtime::Result<Arc<dyn ExtensionModule>>,
301) -> Result<Vec<EagerTensor>> {
302 let ctx = validate_eager_extension_inputs(op.as_ref(), inputs)?;
303 ensure_session_runtime(session, &ctx)?;
304 let target = ctx.eager_extension_target(session.engine())?;
305 let input_reads: Vec<_> = inputs.iter().map(|tensor| tensor.tensor_read()).collect();
306 validate_eager_extension_input_signature(&ctx, &target, &input_reads)?;
307 let module = module_factory(target.clone())?;
308 ctx.ensure_extension_module_for_engine(module, op.family_id(), &target.engine_id)?;
309 apply_eager_in_session(session, op, inputs)
310}
311
312pub(crate) fn validate_eager_extension_target(
313 runtime: &Runtime,
314 target: &EagerExtensionTarget,
315) -> Result<()> {
316 let snapshot = runtime.snapshot().map_err(|source| {
317 Error::runtime_state_source(
318 "extension::apply_eager_with_extension_session",
319 ErrorPhase::Execution,
320 source,
321 )
322 })?;
323 if snapshot.engine(&target.engine_id).is_none() {
324 return Err(Error::runtime_state_source(
325 "extension::apply_eager_with_extension_session",
326 ErrorPhase::Execution,
327 RuntimeConfigError::MissingEngine {
328 engine_id: target.engine_id.clone(),
329 },
330 ));
331 }
332 Ok(())
333}
334
335fn validate_eager_extension_input_signature(
336 ctx: &EagerRuntime,
337 target: &EagerExtensionTarget,
338 input_reads: &[TensorRead<'_>],
339) -> Result<()> {
340 let signature = InputSignature::from_reads(input_reads).map_err(|source| {
341 Error::runtime_state_source(
342 "extension::apply_eager_with_extension_session",
343 ErrorPhase::Execution,
344 source,
345 )
346 })?;
347 let snapshot = ctx.runtime().snapshot().map_err(|source| {
348 Error::runtime_state_source(
349 "extension::apply_eager_with_extension_session",
350 ErrorPhase::Execution,
351 source,
352 )
353 })?;
354 let engine = snapshot.engine(&target.engine_id).ok_or_else(|| {
355 Error::runtime_state_source(
356 "extension::apply_eager_with_extension_session",
357 ErrorPhase::Execution,
358 RuntimeConfigError::MissingEngine {
359 engine_id: target.engine_id.clone(),
360 },
361 )
362 })?;
363 for (input_index, entry) in signature.entries().iter().enumerate() {
364 if !engine.accepts_input_signature(entry) {
365 return Err(Error::runtime_state_source(
366 "extension::apply_eager_with_extension_session",
367 ErrorPhase::Execution,
368 PrepareError::NoInputIngress {
369 input_index,
370 placement: entry.placement().clone(),
371 },
372 ));
373 }
374 }
375 Ok(())
376}
377
378fn validate_eager_extension_inputs(
379 op: &dyn ExtensionOp,
380 inputs: &[&EagerTensor],
381) -> Result<Arc<EagerRuntime>> {
382 let Some(first) = inputs.first() else {
383 return Err(Error::invalid_argument(
384 "extension::apply_eager",
385 ErrorPhase::Execution,
386 "inputs",
387 "at least one input tensor is required",
388 ));
389 };
390 if inputs.len() != op.input_count() {
391 return Err(Error::invalid_argument(
392 "extension::apply_eager",
393 ErrorPhase::Execution,
394 "inputs",
395 format!(
396 "op family {:?} expects {} inputs, got {}",
397 op.family_id(),
398 op.input_count(),
399 inputs.len()
400 ),
401 ));
402 }
403
404 let ctx = Arc::clone(&first.ctx);
405 for tensor in inputs.iter().skip(1) {
406 if !first.same_context(tensor) {
407 return Err(Error::ContextMismatch {
408 lhs: first.ctx_id(),
409 rhs: tensor.ctx_id(),
410 });
411 }
412 }
413 Ok(ctx)
414}
415
416fn finish_eager_extension_outputs(
417 ctx: Arc<EagerRuntime>,
418 op: StdTensorOp,
419 inputs: &[&EagerTensor],
420 outputs: Vec<Tensor>,
421 session: &mut EagerSession<'_>,
422) -> Result<Vec<EagerTensor>> {
423 if outputs.len() != op.output_count() {
424 return Err(Error::Internal(format!(
425 "expected {} eager outputs for {:?}, got {}",
426 op.output_count(),
427 op,
428 outputs.len()
429 )));
430 }
431
432 if !eager_grad_recording_enabled()
433 || (!eager_capture_active() && !inputs.iter().any(|input| input.requires_grad))
434 {
435 return outputs
436 .into_iter()
437 .map(|output| EagerTensor::new_untracked_result(Arc::clone(&ctx), output))
438 .collect();
439 }
440
441 let output_refs: Vec<&Tensor> = outputs.iter().collect();
442 let recorded = session.record_outputs(&op, &output_refs, inputs)?;
443 if recorded.traces.len() != outputs.len() {
444 return Err(Error::Internal(format!(
445 "expected {} eager traces for {:?}, got {}",
446 outputs.len(),
447 op,
448 recorded.traces.len()
449 )));
450 }
451 let results = recorded
452 .traces
453 .into_iter()
454 .zip(recorded.semantic_traces)
455 .zip(outputs)
456 .map(|((trace, semantic_trace), output)| {
457 if trace.requires_grad {
458 EagerTensor::new_result_with_semantic_trace(
459 Arc::clone(&ctx),
460 trace.key,
461 output,
462 trace.requires_grad,
463 trace.trace,
464 semantic_trace,
465 )
466 } else {
467 EagerTensor::new_unregistered_result_with_semantic_trace(
468 Arc::clone(&ctx),
469 trace.key,
470 output,
471 trace.requires_grad,
472 trace.trace,
473 semantic_trace,
474 )
475 }
476 })
477 .collect::<Result<Vec<_>>>()?;
478 crate::eager::finish_residuals(&op, inputs, &results.iter().collect::<Vec<_>>())?;
479 Ok(results)
480}