Skip to main content

tenferro_cpu/
batch_policy.rs

1//! Overridable batch execution strategy for batched CPU operations (#2142).
2//!
3//! A batched CPU operation (a strided batched contraction or a
4//! packed-LU batch) splits its independent items over the thread budget the
5//! lower library receives on `tcontract_exec::Exec`. The vocabulary for that
6//! split lives in `tcontract_exec` and is re-exported here so callers keep a
7//! `tenferro_cpu` spelling:
8//!
9//! * [`CpuBatchPolicy`] is `tcontract_exec::BatchPolicy` (a [`CpuBatchStrategy`]
10//!   plus the [`CpuBatchThresholds`] the `Auto` arm uses),
11//! * [`CpuBatchStrategy`] is `tcontract_exec::BatchStrategy`,
12//! * [`CpuBatchThresholds`] is `tcontract_exec::WorkModel`.
13//!
14//! Two variants were renamed when the vocabulary moved into `tcontract-exec`:
15//! the old `OuterParallel` is [`CpuBatchStrategy::Outer`], and the old
16//! `ProviderItems` is [`CpuBatchStrategy::Inner`]. `Sequential` and `Auto` kept
17//! their names. The old vendor-dimension cutoff is deliberately not restored: it
18//! was an inert knob once the decision moved into the lower library.
19//!
20//! # Precedence
21//!
22//! A batched operation resolves its policy as the innermost scoped override,
23//! then the [`crate::CpuEngine`] default:
24//!
25//! 1. the policy of an enclosing [`with_batch_policy`] scope, if any;
26//! 2. otherwise [`crate::CpuEngine::batch_policy`], whose default is
27//!    [`CpuBatchStrategy::Auto`].
28//!
29//! Wrapping one call in [`with_batch_policy`] therefore expresses a
30//! per-operation choice. A non-CPU engine is rejected with a typed
31//! [`tenferro_tensor::ErrorKind::Unsupported`] error; there is no process-global
32//! or thread-local policy outside the scope callback, and each scope restores
33//! the previous policy on return, error and unwind.
34//!
35//! # Route-specific `Auto`
36//!
37//! `Auto` is resolved inside the lower library and keeps that library's legacy
38//! rule until the upstream shared model lands (tcontract-rs#123 step 3):
39//! `tlinalg`'s 64-extent cutoff that counts RHS columns in the linalg lane,
40//! and `cpueinsum`'s width rule in the contraction lane. `CpuBatchThresholds`
41//! feeds those rules where the lower library reads the policy's model; a forced
42//! strategy ignores the model and never falls back.
43//!
44//! # The BLAS linalg lane
45//!
46//! `tlinalg-blas` takes no `Exec` and has no tenferro lanes, so the BLAS linalg
47//! lane refuses a forced [`CpuBatchStrategy::Outer`],
48//! [`CpuBatchStrategy::Inner`] or `Split` before any write with a typed
49//! unsupported error. `Auto` and `Sequential` are accepted; the lane is already
50//! serial and the vendor call owns its own threading.
51
52/// How the items of one batched operation are split over a thread budget.
53///
54/// This is `tcontract_exec::BatchStrategy`. The old `OuterParallel` variant is
55/// [`CpuBatchStrategy::Outer`] and the old `ProviderItems` variant is
56/// [`CpuBatchStrategy::Inner`]; `Sequential` and `Auto` kept their names.
57///
58/// # Examples
59///
60/// ```rust
61/// use tenferro_cpu::CpuBatchStrategy;
62///
63/// assert_eq!(CpuBatchStrategy::default(), CpuBatchStrategy::Auto);
64/// ```
65pub use cpueinsum::BatchStrategy as CpuBatchStrategy;
66
67/// A batch strategy together with the [`CpuBatchThresholds`] `Auto` applies.
68///
69/// This is `tcontract_exec::BatchPolicy`.
70///
71/// # Examples
72///
73/// ```rust
74/// use tenferro_cpu::{CpuBatchPolicy, CpuBatchStrategy};
75///
76/// let policy = CpuBatchPolicy::new(CpuBatchStrategy::Outer);
77/// assert_eq!(policy.strategy, CpuBatchStrategy::Outer);
78/// ```
79pub use cpueinsum::BatchPolicy as CpuBatchPolicy;
80
81/// The cost and fan-out model [`CpuBatchStrategy::Auto`] applies.
82///
83/// This is `tcontract_exec::WorkModel`; the old vendor-dimension cutoff is not
84/// part of it.
85///
86/// # Examples
87///
88/// ```rust
89/// use tenferro_cpu::CpuBatchThresholds;
90///
91/// assert_eq!(CpuBatchThresholds::default().min_items, 2);
92/// ```
93pub use cpueinsum::WorkModel as CpuBatchThresholds;
94
95/// Run `f` on `engine` with `policy` as the effective CPU batch policy.
96///
97/// This is the scoped override of the precedence described in the [module
98/// docs](self): wrapping a single call expresses a per-operation choice. The
99/// previous policy is restored when `f` returns, returns an error or unwinds,
100/// and scopes nest with the innermost winning. `f` runs on the caller's own
101/// engine, so a forwarding engine keeps its own overrides inside the scope.
102/// Nothing here mutates process-global state.
103///
104/// # Examples
105///
106/// ```rust
107/// use tenferro_cpu::{with_batch_policy, with_cpu_engine, CpuBatchPolicy, CpuBatchStrategy, CpuEngine};
108///
109/// let mut engine = CpuEngine::serial();
110/// let observed = with_batch_policy(
111///     &mut engine,
112///     CpuBatchPolicy::new(CpuBatchStrategy::Outer),
113///     |engine| {
114///         with_cpu_engine(engine, |cpu| {
115///             cpu.with_linalg_pool(|context, _| Ok(context.batch_policy().strategy))
116///         })
117///         .expect("a CPU engine")
118///     },
119/// )??;
120/// assert_eq!(observed, CpuBatchStrategy::Outer);
121/// assert_eq!(engine.batch_policy().strategy, CpuBatchStrategy::Auto);
122/// # Ok::<(), Box<dyn std::error::Error>>(())
123/// ```
124///
125/// # Errors
126///
127/// Returns [`tenferro_tensor::Error::Unsupported`] without running `f` when
128/// `engine` is neither a [`crate::CpuEngine`] nor an engine that forwards one
129/// through [`tenferro_tensor::Engine::native_engine`] (a CUDA or other
130/// custom engine). `f`'s own result is returned unchanged inside `Ok`.
131///
132/// # Panics
133///
134/// A panic in `f` propagates after the previous policy is restored.
135pub fn with_batch_policy<R>(
136    engine: &mut dyn tenferro_tensor::Engine,
137    policy: CpuBatchPolicy,
138    f: impl FnOnce(&mut dyn tenferro_tensor::Engine) -> R,
139) -> tenferro_tensor::Result<R> {
140    let Some(previous) = crate::with_cpu_engine(engine, |cpu| cpu.replace_batch_policy(policy))
141    else {
142        return Err(tenferro_tensor::Error::unsupported(
143            "tenferro_cpu::with_batch_policy",
144            "the engine is not a CPU engine and does not forward one; CPU batch policies apply \
145             only to CpuEngine",
146        ));
147    };
148    // `f` runs on the caller's own engine, so a wrapping engine keeps its
149    // overrides inside the scope. The policy is restored before an unwind
150    // continues; nothing observes the engine between the panic and the
151    // restore, so asserting unwind safety is sound.
152    let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&mut *engine)));
153    // INVARIANT: the same engine yielded a CPU engine above and native tokens
154    // are stable for an engine's lifetime, so this visit runs.
155    let _ = crate::with_cpu_engine(engine, |cpu| cpu.replace_batch_policy(previous));
156    match outcome {
157        Ok(value) => Ok(value),
158        Err(payload) => std::panic::resume_unwind(payload),
159    }
160}