Skip to main content

tenferro_cpu/
exec_session.rs

1use crate::buffer_pool::{BufferPool, PoolScalar};
2use crate::engine::guard_unwind;
3use crate::{CpuEngine, CpuPlanCache, Tensor, TensorRead, TensorValue, TensorWrite};
4use num_complex::{Complex32, Complex64};
5use tenferro_tensor::backend::{ElementwiseFusionPlan, Engine};
6use tenferro_tensor::{
7    ActivationOp, CompareDir, ContractionScalar, DType, DotGeneralConfig, ElementwiseReadOp,
8    GatherConfig, PadConfig, ScatterConfig, SliceConfig, TensorView, TypedTensor,
9};
10use tenferro_tensor::{
11    DotGeneralAccumulation, SessionCachedDot, TensorAnalytic, TensorBuffer, TensorDeviceTransfer,
12    TensorDot, TensorElementwise, TensorFusion, TensorIndexing, TensorReduction, TensorStructural,
13};
14
15use super::backend::elementwise_read_into_fallback_with_pool;
16use super::{
17    activation, analytic, copy_tensor_read_into, elementwise, indexing,
18    materialize_tensor_read_in_domain, reduction, softmax, structural,
19};
20
21/// Native-engine marker for [`CpuEngine`]; private to this crate so no other
22/// crate can create a token that claims to be a CPU engine.
23pub(super) struct CpuEngineMarker;
24
25/// Route an eager `dot_general` request to a slot of the engine's plan cache.
26///
27/// A prepared program keys a contraction plan by its instruction index, because
28/// a compiled program has exactly one such index per operation. An eager call
29/// has none, so this derives one from the request's geometry: the dtype, the two
30/// operand shapes, the four axis lists and the conjugation flags, all of which
31/// the plan key already names.
32///
33/// The slot only *routes*. [`crate::contraction::BinaryCache`] still compares the
34/// full key - including both operand layouts, which this fingerprint deliberately
35/// leaves out because reading them here would cost the very allocation the cache
36/// is meant to avoid - before reusing a plan, so two requests that collide
37/// rebuild instead of sharing a plan that does not apply. A collision therefore
38/// costs a miss, never a wrong answer.
39fn eager_dot_cache_slot(
40    capacity: usize,
41    lhs: &TensorRead<'_>,
42    rhs: &TensorRead<'_>,
43    config: &DotGeneralConfig,
44    lhs_conj: bool,
45    rhs_conj: bool,
46) -> Option<usize> {
47    use std::hash::{Hash, Hasher};
48
49    if capacity == 0 {
50        return None;
51    }
52    let mut hasher = std::collections::hash_map::DefaultHasher::new();
53    lhs.dtype().hash(&mut hasher);
54    lhs.shape().hash(&mut hasher);
55    rhs.shape().hash(&mut hasher);
56    config.lhs_contracting_dims.hash(&mut hasher);
57    config.rhs_contracting_dims.hash(&mut hasher);
58    config.lhs_batch_dims.hash(&mut hasher);
59    config.rhs_batch_dims.hash(&mut hasher);
60    lhs_conj.hash(&mut hasher);
61    rhs_conj.hash(&mut hasher);
62    Some(hasher.finish() as usize % capacity)
63}
64
65fn pooled_zero_tensor<T>(
66    buffers: &mut BufferPool,
67    shape: impl tenferro_tensor::IntoShapeVec,
68) -> crate::Result<TypedTensor<T>>
69where
70    T: PoolScalar + Clone + 'static,
71{
72    let shape = shape.into_shape_vec();
73    let element_count =
74        tenferro_tensor::validate::checked_shape_product("dot_general", "output", &shape)?;
75    TypedTensor::from_vec_col_major(shape, T::pool_acquire_zeroed(buffers, element_count))
76}
77
78fn allocate_dot_output(
79    buffers: &mut BufferPool,
80    dtype: DType,
81    shape: impl tenferro_tensor::IntoShapeVec,
82) -> crate::Result<Tensor> {
83    let shape = shape.into_shape_vec();
84    match dtype {
85        DType::F32 => pooled_zero_tensor(buffers, &shape).map(Tensor::from_typed::<f32>),
86        DType::F64 => pooled_zero_tensor(buffers, &shape).map(Tensor::from_typed::<f64>),
87        DType::C32 => pooled_zero_tensor(buffers, &shape)
88            .map(Tensor::from_typed::<tenferro_tensor::Complex32>),
89        DType::C64 => pooled_zero_tensor(buffers, &shape)
90            .map(Tensor::from_typed::<tenferro_tensor::Complex64>),
91        dtype => Err(crate::Error::unsupported_dtype(
92            "dot_general",
93            dtype,
94            crate::cpu_contraction_unsupported_dtype_message(dtype),
95        )),
96    }
97}
98
99fn flatten_compact_read(
100    input: TensorRead<'_>,
101    element_count: usize,
102) -> crate::Result<TensorRead<'_>> {
103    macro_rules! flatten {
104        ($variant:ident, $view:expr) => {
105            Ok(TensorRead::from_view(TensorView::$variant(
106                $view.reshape_view(&[element_count])?,
107            )))
108        };
109    }
110    /// The typed tensor behind a read adapter's tensor, or the refusal this adapter reports.
111    fn read_refusal(tensor: &Tensor) -> crate::Error {
112        crate::Error::unsupported_dtype(
113            "flatten_compact_read",
114            tensor.dtype(),
115            "an externally defined payload is not a compact runtime read",
116        )
117    }
118
119    match input {
120        TensorRead::Tensor(tensor) => match tensor.dtype() {
121            DType::F32 => flatten!(
122                F32,
123                tensor
124                    .as_typed::<f32>()
125                    .ok_or_else(|| read_refusal(tensor))?
126                    .as_view()
127            ),
128            DType::F64 => flatten!(
129                F64,
130                tensor
131                    .as_typed::<f64>()
132                    .ok_or_else(|| read_refusal(tensor))?
133                    .as_view()
134            ),
135            DType::I32 => flatten!(
136                I32,
137                tensor
138                    .as_typed::<i32>()
139                    .ok_or_else(|| read_refusal(tensor))?
140                    .as_view()
141            ),
142            DType::I64 => flatten!(
143                I64,
144                tensor
145                    .as_typed::<i64>()
146                    .ok_or_else(|| read_refusal(tensor))?
147                    .as_view()
148            ),
149            DType::Bool => flatten!(
150                Bool,
151                tensor
152                    .as_typed::<bool>()
153                    .ok_or_else(|| read_refusal(tensor))?
154                    .as_view()
155            ),
156            DType::C32 => flatten!(
157                C32,
158                tensor
159                    .as_typed::<Complex32>()
160                    .ok_or_else(|| read_refusal(tensor))?
161                    .as_view()
162            ),
163            DType::C64 => flatten!(
164                C64,
165                tensor
166                    .as_typed::<Complex64>()
167                    .ok_or_else(|| read_refusal(tensor))?
168                    .as_view()
169            ),
170            // A caller-owned payload has no compact runtime read.
171            DType::External(_) => Err(read_refusal(tensor)),
172        },
173        TensorRead::View(TensorView::F32(view)) => flatten!(F32, view),
174        TensorRead::View(TensorView::F64(view)) => flatten!(F64, view),
175        TensorRead::View(TensorView::I32(view)) => flatten!(I32, view),
176        TensorRead::View(TensorView::I64(view)) => flatten!(I64, view),
177        TensorRead::View(TensorView::Bool(view)) => flatten!(Bool, view),
178        TensorRead::View(TensorView::C32(view)) => flatten!(C32, view),
179        TensorRead::View(TensorView::C64(view)) => flatten!(C64, view),
180    }
181}
182
183impl CpuEngine {
184    /// Replace this engine's effective batch policy, returning the previous one.
185    pub(crate) fn replace_batch_policy(
186        &mut self,
187        policy: crate::CpuBatchPolicy,
188    ) -> crate::CpuBatchPolicy {
189        self.set_batch_policy(policy)
190    }
191
192    /// Run an operation with the one placement token minted for it: every
193    /// worker of the engine's pool, or the serial token of a serial engine.
194    /// Kernels split the token and take it by value, last.
195    fn run_native_with_exec<R>(
196        &mut self,
197        op: impl FnOnce(threadteam::Exec<'_>, &mut BufferPool) -> crate::Result<R>,
198    ) -> crate::Result<R> {
199        self.check_entry("CpuEngine")?;
200        let (exec, buffers, _, broken) = self.parts();
201        guard_unwind(buffers, broken, |buffers| op(exec.into_token(), buffers))
202    }
203
204    fn run_native_with_plans<R>(
205        &mut self,
206        op: impl FnOnce(threadteam::Exec<'_>, &mut BufferPool, &CpuPlanCache) -> crate::Result<R>,
207    ) -> crate::Result<R> {
208        self.check_entry("CpuEngine")?;
209        let (exec, buffers, plans, broken) = self.parts();
210        guard_unwind(buffers, broken, |buffers| {
211            op(exec.into_token(), buffers, plans)
212        })
213    }
214
215    /// Run a contraction-provider call with the engine's bundle, buffers and
216    /// plans; the provider splits the bundle's token itself.
217    fn run_contraction<R>(
218        &mut self,
219        op: impl FnOnce(&mut cpueinsum::Exec<'_>, &mut BufferPool, &CpuPlanCache) -> crate::Result<R>,
220    ) -> crate::Result<R> {
221        self.check_entry("dot_general")?;
222        let _retention = self
223            .resources
224            .retention_guard(self.buffers.max_retained_capacity_bytes());
225        let mut exec = self.resources.exec(self.batch_policy);
226        let plans = &self.plans;
227        guard_unwind(&mut self.buffers, &mut self.broken, |buffers| {
228            op(&mut exec, buffers, plans)
229        })
230    }
231}
232
233impl TensorDeviceTransfer for CpuEngine {
234    fn download_to_host(&mut self, tensor: TensorRead<'_>) -> crate::Result<Tensor> {
235        self.check_entry("CpuEngine::download_to_host")?;
236        if tensor.backend_family().is_some() {
237            return Err(crate::Error::runtime_state(
238                "CpuEngine::download_to_host",
239                "CPU backend received a backend buffer; download the tensor to host with its owning backend before CPU execution",
240            ));
241        }
242        tensor.tensor_view().duplicate()
243    }
244
245    fn upload_host_tensor(&mut self, tensor: TensorRead<'_>) -> crate::Result<Tensor> {
246        self.check_entry("CpuEngine::upload_host_tensor")?;
247        if tensor.backend_family().is_some() {
248            return Err(crate::Error::runtime_state(
249                "CpuEngine::upload_host_tensor",
250                "CPU backend upload_host_tensor expects a host tensor; download backend buffers to host before CPU execution",
251            ));
252        }
253        tensor.tensor_view().duplicate()
254    }
255}
256
257/// Delegation for operations whose kernels take the operation's placement
258/// token (last) and allocate their outputs from the session pool.
259macro_rules! delegate_with_pool {
260    ($name:ident($($arg:ident : $ty:ty),*) => $callee:path) => {
261        fn $name(&mut self, $($arg: $ty),*) -> crate::Result<Tensor> {
262            self.run_native_with_exec(|exec, buffers| $callee(buffers, $($arg),*, exec))
263        }
264    };
265}
266
267impl TensorElementwise for CpuEngine {
268    // Elementwise — direct delegation within the current session scope.
269    fn elementwise_read_into(
270        &mut self,
271        op: ElementwiseReadOp,
272        inputs: &[TensorRead<'_>],
273        out: TensorWrite<'_>,
274    ) -> crate::Result<()> {
275        self.run_native_with_exec(|exec, buffers| {
276            // The one-shot fast path or the fallback consumes the token, never both.
277            tenferro_internal_cpu_kernels::elementwise_read_into_with_exec(
278                op,
279                inputs,
280                out,
281                |inputs, out, exec| {
282                    elementwise_read_into_fallback_with_pool(buffers, op, inputs, out, exec)
283                },
284                exec,
285            )
286        })
287    }
288
289    fn add_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
290        self.run_native_with_exec(|exec, buffers| {
291            elementwise::add_read_with_pool(buffers, lhs, rhs, exec)
292        })
293    }
294
295    delegate_with_pool!(sub_read(lhs: TensorRead<'_>, rhs: TensorRead<'_>) => elementwise::sub_read_with_pool);
296    delegate_with_pool!(mul_read(lhs: TensorRead<'_>, rhs: TensorRead<'_>) => elementwise::mul_read_with_pool);
297    delegate_with_pool!(neg_read(input: TensorRead<'_>) => elementwise::neg_read_with_pool);
298    delegate_with_pool!(conj_read(input: TensorRead<'_>) => elementwise::conj_read_with_pool);
299    delegate_with_pool!(div_read(lhs: TensorRead<'_>, rhs: TensorRead<'_>) => elementwise::div_read_with_pool);
300    delegate_with_pool!(rem(lhs: &Tensor, rhs: &Tensor) => elementwise::rem_with_pool);
301    delegate_with_pool!(rem_read(lhs: TensorRead<'_>, rhs: TensorRead<'_>) => elementwise::rem_read_with_pool);
302    delegate_with_pool!(abs_read(input: TensorRead<'_>) => elementwise::abs_read_with_pool);
303    delegate_with_pool!(sign_read(input: TensorRead<'_>) => elementwise::sign_read_with_pool);
304    delegate_with_pool!(maximum_read(lhs: TensorRead<'_>, rhs: TensorRead<'_>) => elementwise::maximum_read_with_pool);
305    delegate_with_pool!(minimum_read(lhs: TensorRead<'_>, rhs: TensorRead<'_>) => elementwise::minimum_read_with_pool);
306    delegate_with_pool!(compare_read(lhs: TensorRead<'_>, rhs: TensorRead<'_>, dir: &CompareDir) => elementwise::compare_read_with_pool);
307    delegate_with_pool!(select_read(pred: TensorRead<'_>, on_true: TensorRead<'_>, on_false: TensorRead<'_>) => elementwise::select_read_with_pool);
308    delegate_with_pool!(clamp_read(input: TensorRead<'_>, lower: TensorRead<'_>, upper: TensorRead<'_>) => elementwise::clamp_read_with_pool);
309}
310
311impl TensorAnalytic for CpuEngine {
312    // Analytic
313    delegate_with_pool!(exp_read(input: TensorRead<'_>) => analytic::exp_read_with_pool);
314    delegate_with_pool!(log_read(input: TensorRead<'_>) => analytic::log_read_with_pool);
315    delegate_with_pool!(sin_read(input: TensorRead<'_>) => analytic::sin_read_with_pool);
316    delegate_with_pool!(cos_read(input: TensorRead<'_>) => analytic::cos_read_with_pool);
317    delegate_with_pool!(tanh_read(input: TensorRead<'_>) => analytic::tanh_read_with_pool);
318    delegate_with_pool!(sqrt_read(input: TensorRead<'_>) => analytic::sqrt_read_with_pool);
319    delegate_with_pool!(rsqrt_read(input: TensorRead<'_>) => analytic::rsqrt_read_with_pool);
320    delegate_with_pool!(pow_read(lhs: TensorRead<'_>, rhs: TensorRead<'_>) => analytic::pow_read_with_pool);
321    delegate_with_pool!(expm1_read(input: TensorRead<'_>) => analytic::expm1_read_with_pool);
322    delegate_with_pool!(log1p_read(input: TensorRead<'_>) => analytic::log1p_read_with_pool);
323    delegate_with_pool!(erf_read(input: TensorRead<'_>) => analytic::erf_read_with_pool);
324}
325
326impl TensorStructural for CpuEngine {
327    // Structural
328    fn to_contiguous_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
329        // Preserve the host-compact entry policy. Managed copies must enter
330        // the configured execution scope for provider mapping and allocation,
331        // just as views enter it for strided materialization.
332        if input.as_tensor().is_none() {
333            // Only a view counts as staging: an owned read is already compact,
334            // and the copy below is this method's contract, not a hidden one.
335            self.resources.count_read_staging();
336        }
337        let domain = self.shared_allocation_domain().cloned();
338        if matches!(input, TensorRead::Tensor(_)) && input.backend_family().is_none() {
339            self.check_entry("CpuEngine::to_contiguous_read")?;
340            // An owned compact read copies on the host and fans out nowhere.
341            return materialize_tensor_read_in_domain(
342                &mut self.buffers,
343                "CpuEngine::to_contiguous_read",
344                input,
345                domain.as_deref(),
346                threadteam::Exec::serial(),
347            );
348        }
349        self.run_native_with_exec(|exec, buffers| {
350            materialize_tensor_read_in_domain(
351                buffers,
352                "CpuEngine::to_contiguous_read",
353                input,
354                domain.as_deref(),
355                exec,
356            )
357        })
358    }
359
360    fn copy_read_into(&mut self, src: TensorRead<'_>, dst: TensorWrite<'_>) -> crate::Result<()> {
361        self.run_native_with_exec(|exec, _| {
362            copy_tensor_read_into("CpuEngine::copy_read_into", src, dst, exec)
363        })
364    }
365
366    fn transpose_read(&mut self, input: TensorRead<'_>, perm: &[usize]) -> crate::Result<Tensor> {
367        self.run_native_with_exec(|exec, buffers| {
368            structural::transpose_read_with_pool(buffers, input, perm, exec)
369        })
370    }
371
372    fn reshape_read(&mut self, input: TensorRead<'_>, shape: &[usize]) -> crate::Result<Tensor> {
373        self.check_entry("CpuEngine::reshape_read")?;
374        // INVARIANT: a compact input keeps the serial host copy and its typed
375        // refusal for an external payload, without the engine entry, a token or
376        // a pool buffer, unless the copy can pay for a fork. The pre-entry gate
377        // (`reshape_may_use_pool`) asks the same predicate as the in-entry gate
378        // with the cheapest dispatch cost any token could charge, so a `false`
379        // there is final; otherwise the token decides (`reshape_uses_pool`).
380        // `run_native_with_exec` (not a fresh run) keeps the input placement
381        // that the copy puts on the output, as the reshape contract requires.
382        if let TensorRead::Tensor(tensor) = &input {
383            if !structural::reshape_may_use_pool(
384                tensor,
385                self.thread_budget(),
386                self.dispatch_costs(),
387            )? {
388                return structural::reshape(tensor, shape);
389            }
390            return self.run_native_with_exec(|exec, buffers| {
391                if let TensorRead::Tensor(tensor) = &input {
392                    if !structural::reshape_uses_pool(tensor, &exec)? {
393                        return structural::reshape(tensor, shape);
394                    }
395                }
396                structural::reshape_read_with_pool(buffers, input, shape, exec)
397            });
398        }
399        self.run_native_with_exec(|exec, buffers| {
400            structural::reshape_read_with_pool(buffers, input, shape, exec)
401        })
402    }
403
404    fn broadcast_in_dim_read(
405        &mut self,
406        input: TensorRead<'_>,
407        shape: &[usize],
408        dims: &[usize],
409    ) -> crate::Result<Tensor> {
410        self.run_native_with_exec(|exec, buffers| {
411            structural::broadcast_in_dim_read_with_pool(buffers, input, shape, dims, exec)
412        })
413    }
414
415    fn tril_read(
416        &mut self,
417        input: tenferro_tensor::TensorRead<'_>,
418        k: i64,
419    ) -> crate::Result<Tensor> {
420        self.run_native_with_exec(|exec, buffers| {
421            structural::tril_read_with_pool(buffers, input, k, exec)
422        })
423    }
424
425    fn triu_read(
426        &mut self,
427        input: tenferro_tensor::TensorRead<'_>,
428        k: i64,
429    ) -> crate::Result<Tensor> {
430        self.run_native_with_exec(|exec, buffers| {
431            structural::triu_read_with_pool(buffers, input, k, exec)
432        })
433    }
434
435    fn extract_diagonal_read(
436        &mut self,
437        input: tenferro_tensor::TensorRead<'_>,
438        axis_a: usize,
439        axis_b: usize,
440    ) -> crate::Result<Tensor> {
441        self.run_native_with_exec(|exec, buffers| {
442            structural::extract_diagonal_read_with_pool(buffers, input, axis_a, axis_b, exec)
443        })
444    }
445
446    fn embed_diagonal_read(
447        &mut self,
448        input: tenferro_tensor::TensorRead<'_>,
449        axis_a: usize,
450        axis_b: usize,
451    ) -> crate::Result<Tensor> {
452        self.run_native_with_exec(|exec, buffers| {
453            structural::embed_diagonal_read_with_pool(buffers, input, axis_a, axis_b, exec)
454        })
455    }
456
457    delegate_with_pool!(cast(input: &Tensor, to: crate::DType) => structural::cast_with_pool);
458    delegate_with_pool!(extract_diagonal(input: &Tensor, axis_a: usize, axis_b: usize) => structural::extract_diagonal_with_pool);
459    delegate_with_pool!(embed_diagonal(input: &Tensor, axis_a: usize, axis_b: usize) => structural::embed_diagonal_with_pool);
460    delegate_with_pool!(tril(input: &Tensor, k: i64) => structural::tril_with_pool);
461    delegate_with_pool!(triu(input: &Tensor, k: i64) => structural::triu_with_pool);
462}
463
464impl TensorReduction for CpuEngine {
465    // Reduction
466
467    fn reduce_sum_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
468        self.run_native_with_exec(|exec, buffers| {
469            reduction::reduce_sum_read(buffers, input, axes, exec)
470        })
471    }
472
473    fn reduce_sum_squares_read(
474        &mut self,
475        input: TensorRead<'_>,
476        axes: &[usize],
477    ) -> crate::Result<Tensor> {
478        self.run_native_with_exec(|exec, buffers| {
479            reduction::reduce_sum_squares_read(buffers, input, axes, exec)
480        })
481    }
482
483    fn reduce_prod_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
484        self.run_native_with_exec(|exec, buffers| {
485            reduction::reduce_prod_read(buffers, input, axes, exec)
486        })
487    }
488
489    fn reduce_max_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
490        self.run_native_with_exec(|exec, buffers| {
491            reduction::reduce_max_read(buffers, input, axes, exec)
492        })
493    }
494
495    fn reduce_min_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
496        self.run_native_with_exec(|exec, buffers| {
497            reduction::reduce_min_read(buffers, input, axes, exec)
498        })
499    }
500}
501
502impl CpuEngine {
503    fn execute_dot_allocated(
504        &mut self,
505        cache_slot: Option<usize>,
506        lhs: TensorRead<'_>,
507        rhs: TensorRead<'_>,
508        config: &DotGeneralConfig,
509        lhs_conj: bool,
510        rhs_conj: bool,
511    ) -> crate::Result<Tensor> {
512        let dtype = lhs.dtype();
513        if rhs.dtype() != dtype {
514            return Err(crate::Error::dtype_mismatch(
515                "dot_general",
516                dtype,
517                rhs.dtype(),
518            ));
519        }
520        let output_shape = tenferro_tensor::backend::dot_general_output_shape(
521            lhs.shape(),
522            rhs.shape(),
523            config,
524            "dot_general",
525        )?;
526        let accumulation = DotGeneralAccumulation {
527            lhs_conj,
528            rhs_conj,
529            alpha: ContractionScalar::one(dtype)?,
530            beta: ContractionScalar::zero(dtype)?,
531        };
532        if matches!(dtype, DType::F32 | DType::F64 | DType::C32 | DType::C64) {
533            return self.run_contraction(|exec, buffers, plans| {
534                crate::contraction::dot_fresh(
535                    exec,
536                    buffers,
537                    plans,
538                    cache_slot,
539                    lhs,
540                    rhs,
541                    config,
542                    accumulation,
543                    output_shape,
544                )
545            });
546        }
547        let mut output = allocate_dot_output(&mut self.buffers, dtype, output_shape)?;
548        self.dot_general_read_into_accum_cached(
549            cache_slot,
550            lhs,
551            rhs,
552            config,
553            accumulation,
554            TensorWrite::from_tensor(&mut output),
555        )?;
556        Ok(output)
557    }
558}
559
560impl TensorDot for CpuEngine {
561    fn dot_general_read(
562        &mut self,
563        lhs: TensorRead<'_>,
564        rhs: TensorRead<'_>,
565        config: &DotGeneralConfig,
566    ) -> crate::Result<Tensor> {
567        let slot = eager_dot_cache_slot(
568            self.plans.contraction_capacity(),
569            &lhs,
570            &rhs,
571            config,
572            false,
573            false,
574        );
575        self.execute_dot_allocated(slot, lhs, rhs, config, false, false)
576    }
577
578    fn dot_general_with_conj_read(
579        &mut self,
580        lhs: TensorRead<'_>,
581        rhs: TensorRead<'_>,
582        config: &DotGeneralConfig,
583        lhs_conj: bool,
584        rhs_conj: bool,
585    ) -> crate::Result<Tensor> {
586        // The contraction lane dispatches on the read's view and consumes its
587        // strides, so a conjugated operand needs no staging copy here (#2214).
588        let slot = eager_dot_cache_slot(
589            self.plans.contraction_capacity(),
590            &lhs,
591            &rhs,
592            config,
593            lhs_conj,
594            rhs_conj,
595        );
596        self.execute_dot_allocated(slot, lhs, rhs, config, lhs_conj, rhs_conj)
597    }
598
599    fn dot_general_read_into(
600        &mut self,
601        lhs: TensorRead<'_>,
602        rhs: TensorRead<'_>,
603        config: &DotGeneralConfig,
604        out: TensorWrite<'_>,
605    ) -> crate::Result<()> {
606        let accumulation = DotGeneralAccumulation::overwrite(lhs.dtype())?;
607        self.dot_general_read_into_accum(lhs, rhs, config, accumulation, out)
608    }
609
610    fn dot_general_read_into_accum(
611        &mut self,
612        lhs: TensorRead<'_>,
613        rhs: TensorRead<'_>,
614        config: &DotGeneralConfig,
615        accumulation: DotGeneralAccumulation,
616        out: TensorWrite<'_>,
617    ) -> crate::Result<()> {
618        let slot = eager_dot_cache_slot(
619            self.plans.contraction_capacity(),
620            &lhs,
621            &rhs,
622            config,
623            accumulation.lhs_conj,
624            accumulation.rhs_conj,
625        );
626        self.dot_general_read_into_accum_cached(slot, lhs, rhs, config, accumulation, out)
627    }
628
629    fn dot_general_with_conj(
630        &mut self,
631        lhs: &Tensor,
632        rhs: &Tensor,
633        config: &DotGeneralConfig,
634        lhs_conj: bool,
635        rhs_conj: bool,
636    ) -> crate::Result<Tensor> {
637        let lhs = TensorRead::from_tensor(lhs);
638        let rhs = TensorRead::from_tensor(rhs);
639        let slot = eager_dot_cache_slot(
640            self.plans.contraction_capacity(),
641            &lhs,
642            &rhs,
643            config,
644            lhs_conj,
645            rhs_conj,
646        );
647        self.execute_dot_allocated(slot, lhs, rhs, config, lhs_conj, rhs_conj)
648    }
649}
650
651impl SessionCachedDot for CpuEngine {
652    fn dot_general_cached(
653        &mut self,
654        cache_slot: Option<usize>,
655        lhs: &Tensor,
656        rhs: &Tensor,
657        config: &DotGeneralConfig,
658    ) -> crate::Result<Tensor> {
659        self.execute_dot_allocated(
660            cache_slot,
661            TensorRead::from_tensor(lhs),
662            TensorRead::from_tensor(rhs),
663            config,
664            false,
665            false,
666        )
667    }
668
669    fn dot_general_read_cached(
670        &mut self,
671        cache_slot: Option<usize>,
672        lhs: TensorRead<'_>,
673        rhs: TensorRead<'_>,
674        config: &DotGeneralConfig,
675    ) -> crate::Result<Tensor> {
676        // The caller's slot reaches the same plan cache (keyed by layout, config
677        // and conjugation), so a view operand is consumed instead of staged
678        // (#2214). Do not copy CUDA's policy of ignoring the slot.
679        self.execute_dot_allocated(cache_slot, lhs, rhs, config, false, false)
680    }
681
682    fn dot_general_with_conj_cached(
683        &mut self,
684        cache_slot: Option<usize>,
685        lhs: &Tensor,
686        rhs: &Tensor,
687        config: &DotGeneralConfig,
688        lhs_conj: bool,
689        rhs_conj: bool,
690    ) -> crate::Result<Tensor> {
691        self.execute_dot_allocated(
692            cache_slot,
693            TensorRead::from_tensor(lhs),
694            TensorRead::from_tensor(rhs),
695            config,
696            lhs_conj,
697            rhs_conj,
698        )
699    }
700
701    fn dot_general_with_conj_read_cached(
702        &mut self,
703        cache_slot: Option<usize>,
704        lhs: TensorRead<'_>,
705        rhs: TensorRead<'_>,
706        config: &DotGeneralConfig,
707        lhs_conj: bool,
708        rhs_conj: bool,
709    ) -> crate::Result<Tensor> {
710        self.execute_dot_allocated(cache_slot, lhs, rhs, config, lhs_conj, rhs_conj)
711    }
712
713    fn dot_general_read_into_accum_cached(
714        &mut self,
715        cache_slot: Option<usize>,
716        lhs: TensorRead<'_>,
717        rhs: TensorRead<'_>,
718        config: &DotGeneralConfig,
719        accumulation: DotGeneralAccumulation,
720        out: TensorWrite<'_>,
721    ) -> crate::Result<()> {
722        if matches!(
723            lhs.dtype(),
724            DType::F32 | DType::F64 | DType::C32 | DType::C64
725        ) {
726            return self.run_contraction(|exec, buffers, plans| {
727                crate::contraction::dot_into(
728                    exec,
729                    buffers,
730                    plans,
731                    cache_slot,
732                    lhs,
733                    rhs,
734                    config,
735                    accumulation,
736                    out,
737                )
738            });
739        }
740        // Only floating and complex contractions have a CPU numerical route;
741        // integer and Bool operands are rejected before the output is touched.
742        Err(crate::Error::unsupported_dtype(
743            "dot_general",
744            lhs.dtype(),
745            crate::cpu_contraction_unsupported_dtype_message(lhs.dtype()),
746        ))
747    }
748}
749
750impl TensorIndexing for CpuEngine {
751    // Indexing
752    fn gather(
753        &mut self,
754        operand: &Tensor,
755        start_indices: &Tensor,
756        config: &GatherConfig,
757    ) -> crate::Result<Tensor> {
758        self.run_native_with_plans(|exec, buffers, cache| {
759            indexing::gather_with_pool(buffers, cache, operand, start_indices, config, exec)
760        })
761    }
762    fn scatter(
763        &mut self,
764        operand: &Tensor,
765        indices: &Tensor,
766        updates: &Tensor,
767        config: &ScatterConfig,
768    ) -> crate::Result<Tensor> {
769        self.run_native_with_plans(|exec, buffers, cache| {
770            indexing::scatter_with_pool(buffers, cache, operand, indices, updates, config, exec)
771        })
772    }
773    fn dynamic_update_slice_read(
774        &mut self,
775        operand: tenferro_tensor::TensorRead<'_>,
776        update: tenferro_tensor::TensorRead<'_>,
777        starts: tenferro_tensor::TensorRead<'_>,
778    ) -> crate::Result<Tensor> {
779        self.run_native_with_plans(|exec, buffers, cache| {
780            indexing::dynamic_update_slice_read_with_pool(
781                buffers, cache, operand, update, starts, exec,
782            )
783        })
784    }
785    fn scatter_read(
786        &mut self,
787        operand: tenferro_tensor::TensorRead<'_>,
788        indices: tenferro_tensor::TensorRead<'_>,
789        updates: tenferro_tensor::TensorRead<'_>,
790        config: &ScatterConfig,
791    ) -> crate::Result<Tensor> {
792        self.run_native_with_plans(|exec, buffers, cache| {
793            indexing::scatter_read_with_pool(
794                buffers, cache, operand, indices, updates, config, exec,
795            )
796        })
797    }
798    fn slice(&mut self, input: &Tensor, config: &SliceConfig) -> crate::Result<Tensor> {
799        self.run_native_with_exec(|exec, buffers| {
800            indexing::try_slice_with_pool(buffers, input, config, exec)
801        })
802    }
803    fn slice_read(
804        &mut self,
805        input: tenferro_tensor::TensorRead<'_>,
806        config: &SliceConfig,
807    ) -> crate::Result<Tensor> {
808        self.run_native_with_exec(|exec, buffers| {
809            indexing::slice_read_with_pool(buffers, input, config, exec)
810        })
811    }
812    fn dynamic_slice_read(
813        &mut self,
814        input: tenferro_tensor::TensorRead<'_>,
815        starts: tenferro_tensor::TensorRead<'_>,
816        slice_sizes: &[usize],
817    ) -> crate::Result<Tensor> {
818        self.run_native_with_plans(|exec, buffers, cache| {
819            indexing::dynamic_slice_read_with_pool(buffers, cache, input, starts, slice_sizes, exec)
820        })
821    }
822    fn dynamic_slice(
823        &mut self,
824        input: &Tensor,
825        starts: &Tensor,
826        slice_sizes: &[usize],
827    ) -> crate::Result<Tensor> {
828        self.run_native_with_plans(|exec, buffers, cache| {
829            indexing::dynamic_slice_with_pool(buffers, cache, input, starts, slice_sizes, exec)
830        })
831    }
832    fn dynamic_update_slice(
833        &mut self,
834        operand: &Tensor,
835        update: &Tensor,
836        starts: &Tensor,
837    ) -> crate::Result<Tensor> {
838        self.run_native_with_plans(|exec, buffers, cache| {
839            indexing::dynamic_update_slice_with_pool(buffers, cache, operand, update, starts, exec)
840        })
841    }
842    fn pad(&mut self, input: &Tensor, config: &PadConfig) -> crate::Result<Tensor> {
843        self.run_native_with_exec(|exec, buffers| {
844            indexing::try_pad_with_pool(buffers, input, config, exec)
845        })
846    }
847    fn pad_read(
848        &mut self,
849        input: tenferro_tensor::TensorRead<'_>,
850        config: &PadConfig,
851    ) -> crate::Result<Tensor> {
852        self.run_native_with_exec(|exec, buffers| {
853            indexing::pad_read_with_pool(buffers, input, config, exec)
854        })
855    }
856    fn concatenate(&mut self, inputs: &[&Tensor], axis: usize) -> crate::Result<Tensor> {
857        self.run_native_with_exec(|exec, buffers| {
858            indexing::try_concatenate_with_pool(buffers, inputs, axis, exec)
859        })
860    }
861    fn concatenate_read(
862        &mut self,
863        inputs: &[tenferro_tensor::TensorRead<'_>],
864        axis: usize,
865    ) -> crate::Result<Tensor> {
866        self.run_native_with_exec(|exec, buffers| {
867            indexing::concatenate_read_with_pool(buffers, inputs, axis, exec)
868        })
869    }
870    fn reverse(&mut self, input: &Tensor, axes: &[usize]) -> crate::Result<Tensor> {
871        self.run_native_with_exec(|exec, buffers| {
872            indexing::reverse_with_pool(buffers, input, axes, exec)
873        })
874    }
875    fn reverse_read(
876        &mut self,
877        input: tenferro_tensor::TensorRead<'_>,
878        axes: &[usize],
879    ) -> crate::Result<Tensor> {
880        self.run_native_with_exec(|exec, buffers| {
881            indexing::reverse_read_with_pool(buffers, input, axes, exec)
882        })
883    }
884}
885
886/// Typed view canonicalization runs on the engine.
887impl<T, R> tenferro_tensor::TensorViewCanonicalization<T, R> for CpuEngine
888where
889    T: tenferro_tensor::TensorScalar + PoolScalar,
890    R: tenferro_tensor::TensorRank,
891    R::Shape: Send + Sync,
892    R::Strides: Send + Sync,
893{
894    fn to_contiguous(
895        &mut self,
896        view: &tenferro_tensor::TypedTensorView<'_, T, R>,
897    ) -> crate::Result<TypedTensor<T, R>> {
898        self.run_native_with_exec(|exec, buffers| {
899            crate::structural::typed_materialize_view_with_pool(
900                buffers,
901                view,
902                "CpuEngine::to_contiguous",
903                exec,
904            )
905        })
906    }
907
908    fn copy_into(
909        &mut self,
910        src: &tenferro_tensor::TypedTensorView<'_, T, R>,
911        dst: &mut tenferro_tensor::TypedTensorViewMut<'_, T, R>,
912    ) -> crate::Result<()> {
913        self.run_native_with_exec(|exec, _| {
914            crate::structural::typed_copy_view_into(src, dst, "CpuEngine::copy_into", exec)
915        })
916    }
917}
918
919impl TensorBuffer for CpuEngine {
920    fn reclaim_buffer(&mut self, tensor: Tensor) {
921        crate::backend::reclaim_tensor(&mut self.buffers, tensor);
922    }
923}
924
925impl TensorFusion for CpuEngine {
926    fn execute_elementwise_fusion(
927        &mut self,
928        inputs: &[&Tensor],
929        plan: &ElementwiseFusionPlan,
930    ) -> crate::Result<Option<Vec<Tensor>>> {
931        self.run_native_with_exec(|exec, buffers| {
932            tenferro_cpu_fused::elementwise_fusion_with_pool(buffers, inputs, plan, exec)
933        })
934    }
935
936    fn execute_broadcast_multiply(
937        &mut self,
938        lhs: TensorRead<'_>,
939        lhs_shape: &[usize],
940        lhs_dims: &[usize],
941        rhs: TensorRead<'_>,
942        rhs_shape: &[usize],
943        rhs_dims: &[usize],
944    ) -> crate::Result<Option<Tensor>> {
945        self.run_native_with_exec(|exec, buffers| {
946            elementwise::broadcast_multiply_read_with_pool(
947                buffers, lhs, lhs_shape, lhs_dims, rhs, rhs_shape, rhs_dims, exec,
948            )
949        })
950    }
951
952    fn execute_broadcast_multiply_value(
953        &mut self,
954        lhs: TensorRead<'_>,
955        lhs_shape: &[usize],
956        lhs_dims: &[usize],
957        rhs: TensorRead<'_>,
958        rhs_shape: &[usize],
959        rhs_dims: &[usize],
960    ) -> crate::Result<Option<TensorValue>> {
961        self.run_native_with_exec(|exec, buffers| {
962            elementwise::broadcast_multiply_value_with_pool_and_tag(
963                buffers,
964                lhs,
965                lhs_shape,
966                lhs_dims,
967                rhs,
968                rhs_shape,
969                rhs_dims,
970                |_| {},
971                exec,
972            )
973        })
974    }
975}
976
977impl Engine for CpuEngine {
978    fn gather_read(
979        &mut self,
980        operand: tenferro_tensor::TensorRead<'_>,
981        start_indices: tenferro_tensor::TensorRead<'_>,
982        config: &GatherConfig,
983    ) -> crate::Result<Tensor> {
984        self.run_native_with_plans(|exec, buffers, cache| {
985            indexing::gather_read_with_pool(buffers, cache, operand, start_indices, config, exec)
986        })
987    }
988    fn vdot_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
989        tenferro_tensor::backend::validate_vdot_read(&lhs, &rhs)?;
990        crate::blas1::validate_cpu_read("Engine::vdot_read", &lhs)?;
991        crate::blas1::validate_cpu_read("Engine::vdot_read", &rhs)?;
992        let element_count = tenferro_tensor::validate::checked_shape_product(
993            "Engine::vdot_read",
994            "shape",
995            lhs.shape(),
996        )?;
997        let (lhs, rhs, rank) = if lhs.shape().len() > 2
998            && lhs.is_col_major_contiguous()?
999            && rhs.is_col_major_contiguous()?
1000        {
1001            (
1002                flatten_compact_read(lhs, element_count)?,
1003                flatten_compact_read(rhs, element_count)?,
1004                1,
1005            )
1006        } else {
1007            let rank = lhs.shape().len();
1008            (lhs, rhs, rank)
1009        };
1010        let axes = (0..rank).collect::<tinyvec::TinyVec<[usize; 4]>>();
1011        let config = DotGeneralConfig {
1012            lhs_contracting_dims: axes.clone(),
1013            rhs_contracting_dims: axes,
1014            lhs_batch_dims: Default::default(),
1015            rhs_batch_dims: Default::default(),
1016        };
1017        self.execute_dot_allocated(None, lhs, rhs, &config, true, false)
1018    }
1019
1020    fn norm_squared_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
1021        tenferro_tensor::backend::validate_norm_squared_read(&input)?;
1022        crate::blas1::validate_cpu_read("Engine::norm_squared_read", &input)?;
1023        self.run_native_with_exec(|exec, buffers| {
1024            reduction::norm_squared_read(buffers, input, exec)
1025        })
1026    }
1027
1028    fn fused_activation_read(
1029        &mut self,
1030        op: ActivationOp,
1031        input: TensorRead<'_>,
1032    ) -> crate::Result<Option<Tensor>> {
1033        crate::blas1::validate_cpu_read(op.label(), &input)?;
1034        self.run_native_with_exec(|exec, buffers| {
1035            activation::fused_activation_read_with_pool(buffers, op, input, exec)
1036        })
1037    }
1038
1039    fn softmax_read(
1040        &mut self,
1041        input: TensorRead<'_>,
1042        axis: usize,
1043    ) -> crate::Result<Option<Tensor>> {
1044        crate::blas1::validate_cpu_read("softmax", &input)?;
1045        self.run_native_with_exec(|exec, buffers| {
1046            softmax::softmax_read_with_pool(buffers, input, axis, exec)
1047        })
1048    }
1049
1050    fn axpby_read_into_accum(
1051        &mut self,
1052        alpha: ContractionScalar,
1053        x: TensorRead<'_>,
1054        beta: ContractionScalar,
1055        y: TensorWrite<'_>,
1056    ) -> crate::Result<()> {
1057        tenferro_tensor::backend::validate_axpby_read_into_accum(alpha, &x, beta, &y)?;
1058        self.run_native_with_exec(|exec, buffers| {
1059            crate::blas1::axpby_read_into_accum(buffers, alpha, x, beta, y, exec)
1060        })
1061    }
1062
1063    fn native_engine(&mut self) -> Option<tenferro_tensor::NativeSessionRef<'_>> {
1064        // SAFETY: `CpuEngineMarker` is private to this crate, and this is the
1065        // only place a token carrying it is created; it always points to a
1066        // `CpuEngine`, exclusively borrowed for the token lifetime.
1067        Some(unsafe { tenferro_tensor::NativeSessionRef::new::<CpuEngineMarker, _>(self) })
1068    }
1069}
1070
1071#[cfg(test)]
1072mod tests;