Skip to main content

tenferro_cpu/
engine.rs

1//! The CPU execution engine: a caller-held value that owns one
2//! [`threadteam::Pool`] (or none) and the workspace every operation on it
3//! writes.
4//!
5//! tenferro never builds threads. The caller builds the pool with
6//! [`Pool::builder`](crate::Pool::builder) and hands it to [`CpuEngine::new`]; a
7//! [`CpuEngine::serial`] engine has no pool at all. Every operation takes the
8//! engine through `&mut`, so one computation uses it at a time without run-time
9//! locks. An operation starts on the calling thread with one placement token
10//! minted from the engine's pool; the kernels split that token and enter the pool
11//! only after a provider has chosen more than one task for it.
12
13use std::num::NonZeroUsize;
14use std::sync::Arc;
15
16use tenferro_tensor::{AllocationDomainId, SharedTensorAllocationDomain};
17
18use crate::buffer_pool::{BufferPool, BufferPoolStats};
19use crate::contraction::ExecutionResources;
20use crate::{CacheStats, CpuPlanCache};
21
22/// Typed refusals reported by a [`CpuEngine`] before an operation runs.
23///
24/// They reach callers as the source of a [`crate::Error::RuntimeState`].
25///
26/// # Examples
27///
28/// ```
29/// use tenferro_cpu::CpuEngineError;
30///
31/// assert!(CpuEngineError::Broken.to_string().contains("panic"));
32/// ```
33#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
34#[non_exhaustive]
35pub enum CpuEngineError {
36    /// An earlier operation panicked while it was using the engine.
37    ///
38    /// The engine's workspace may be half updated, so later operations are
39    /// refused; replace the engine with a new one.
40    #[error("the CPU engine is broken by an earlier panic; build a new engine")]
41    Broken,
42}
43
44/// A caller-held CPU execution engine.
45///
46/// The engine owns one [`threadteam::Pool`] (or none for [`CpuEngine::serial`]) and the
47/// workspace operations write: the buffer pool that recycles outputs, the
48/// contraction scratch, and a handle to a [`CpuPlanCache`]. It implements
49/// [`tenferro_tensor::Engine`], so every operation takes it as `&mut`.
50///
51/// # Examples
52///
53/// ```
54/// use tenferro_cpu::CpuEngine;
55/// use tenferro_tensor::{Tensor, TensorElementwise, TensorRead};
56///
57/// let mut engine = CpuEngine::serial();
58/// let a = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0])?;
59/// let b = Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0])?;
60/// let c = engine.add_read(TensorRead::from_tensor(&a), TensorRead::from_tensor(&b))?;
61/// assert_eq!(c.as_slice::<f64>()?, &[4.0, 6.0]);
62/// # Ok::<(), tenferro_tensor::Error>(())
63/// ```
64pub struct CpuEngine {
65    pub(crate) resources: ExecutionResources,
66    pub(crate) buffers: BufferPool,
67    pub(crate) plans: CpuPlanCache,
68    pub(crate) batch_policy: crate::CpuBatchPolicy,
69    pub(crate) allocation_domain: Option<Arc<dyn SharedTensorAllocationDomain>>,
70    runtime_clears: u64,
71    pub(crate) broken: bool,
72    /// Runtime engine id this engine runs prepared programs as.
73    pub(crate) runtime_engine_id: tenferro_runtime::EngineId,
74    /// Extension caches the runtime lends to extension executors during a run.
75    pub(crate) extension_caches: tenferro_runtime::ExtensionCacheStore,
76}
77
78impl std::fmt::Debug for CpuEngine {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.debug_struct("CpuEngine")
81            .field("num_threads", &self.num_threads())
82            .field("has_team", &self.has_team())
83            .field("batch_policy", &self.batch_policy)
84            .field("allocation_domain", &self.allocation_domain())
85            .field("broken", &self.broken)
86            .finish_non_exhaustive()
87    }
88}
89
90impl CpuEngine {
91    fn with_resources(resources: ExecutionResources) -> Self {
92        Self {
93            resources,
94            buffers: BufferPool::new(),
95            plans: CpuPlanCache::new(),
96            batch_policy: crate::CpuBatchPolicy::default(),
97            allocation_domain: None,
98            runtime_clears: 0,
99            broken: false,
100            runtime_engine_id: crate::runtime_adapter::default_runtime_engine_id(),
101            extension_caches: tenferro_runtime::ExtensionCacheStore::default(),
102        }
103    }
104
105    /// An engine that computes on `pool`, which it takes over.
106    ///
107    /// Every operation may use all workers of the pool; a caller that wants
108    /// fewer threads builds a smaller pool. [`Pool::builder`](crate::Pool::builder)
109    /// reserves the worker stack the providers need by default.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// let pool = tenferro_cpu::Pool::builder().threads(4).build()?;
115    /// let engine = tenferro_cpu::CpuEngine::new(pool);
116    /// assert_eq!(engine.num_threads(), 4);
117    /// # Ok::<(), Box<dyn std::error::Error>>(())
118    /// ```
119    pub fn new(pool: crate::Pool) -> Self {
120        Self::with_resources(ExecutionResources::new(Some(pool)))
121    }
122
123    /// An engine with no pool: every operation runs on the calling thread.
124    ///
125    /// A serial engine never starts or enters a pool, so it is safe to use from
126    /// inside any other pool.
127    ///
128    /// # Examples
129    ///
130    /// ```
131    /// let engine = tenferro_cpu::CpuEngine::serial();
132    /// assert_eq!(engine.num_threads(), 1);
133    /// ```
134    pub fn serial() -> Self {
135        Self::with_resources(ExecutionResources::new(None))
136    }
137
138    /// Use `plans` as this engine's plan cache, shared with every other engine
139    /// that holds a clone of it.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use tenferro_cpu::{CpuEngine, CpuPlanCache};
145    ///
146    /// let plans = CpuPlanCache::new();
147    /// let engine = CpuEngine::serial().with_plan_cache(plans.clone());
148    /// assert!(engine.plan_cache().shares_with(&plans));
149    /// ```
150    #[must_use]
151    pub fn with_plan_cache(mut self, plans: CpuPlanCache) -> Self {
152        self.plans = plans;
153        self
154    }
155
156    /// The plan cache this engine reads plans from.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// let engine = tenferro_cpu::CpuEngine::serial();
162    /// assert_eq!(engine.plan_cache().stats().entries, 0);
163    /// ```
164    pub fn plan_cache(&self) -> &CpuPlanCache {
165        &self.plans
166    }
167
168    /// Run prepared programs under a runtime engine id other than the default
169    /// [`crate::runtime_engine_id`]; it must match the id this engine kind was
170    /// registered under.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use tenferro_runtime::EngineId;
176    /// let id = EngineId::new("example.cpu.v1")?;
177    /// let engine = tenferro_cpu::CpuEngine::serial().with_runtime_engine_id(id.clone());
178    /// assert_eq!(tenferro_runtime::ExecutionEngine::target(&engine).engine_id(), &id);
179    /// # Ok::<(), Box<dyn std::error::Error>>(())
180    /// ```
181    #[must_use]
182    pub fn with_runtime_engine_id(mut self, id: tenferro_runtime::EngineId) -> Self {
183        self.runtime_engine_id = id;
184        self
185    }
186
187    /// Bind outputs and managed inputs to a shared allocation domain.
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// let engine = tenferro_cpu::CpuEngine::serial();
193    /// assert!(engine.allocation_domain().is_none());
194    /// ```
195    #[must_use]
196    pub fn with_allocation_domain(mut self, domain: Arc<dyn SharedTensorAllocationDomain>) -> Self {
197        self.allocation_domain = Some(domain);
198        self
199    }
200
201    /// The shared allocation domain this engine is bound to, if any.
202    ///
203    /// # Examples
204    ///
205    /// ```
206    /// assert_eq!(tenferro_cpu::CpuEngine::serial().allocation_domain(), None);
207    /// ```
208    pub fn allocation_domain(&self) -> Option<AllocationDomainId> {
209        self.allocation_domain.as_ref().map(|domain| domain.id())
210    }
211
212    /// The shared allocator this engine is bound to, if any.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// assert!(tenferro_cpu::CpuEngine::serial().shared_allocation_domain().is_none());
218    /// ```
219    pub fn shared_allocation_domain(&self) -> Option<&Arc<dyn SharedTensorAllocationDomain>> {
220        self.allocation_domain.as_ref()
221    }
222
223    /// This engine with a different default batch policy.
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// use tenferro_cpu::{CpuBatchPolicy, CpuBatchStrategy, CpuEngine};
229    ///
230    /// let engine = CpuEngine::serial()
231    ///     .with_batch_policy(CpuBatchPolicy::new(CpuBatchStrategy::Sequential));
232    /// assert_eq!(engine.batch_policy().strategy, CpuBatchStrategy::Sequential);
233    /// ```
234    #[must_use]
235    pub fn with_batch_policy(mut self, policy: crate::CpuBatchPolicy) -> Self {
236        self.batch_policy = policy;
237        self
238    }
239
240    /// Replace the default batch policy, returning the previous one.
241    ///
242    /// For a one-operation override use [`crate::with_batch_policy`].
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// use tenferro_cpu::{CpuBatchPolicy, CpuBatchStrategy, CpuEngine};
248    ///
249    /// let mut engine = CpuEngine::serial();
250    /// let previous = engine.set_batch_policy(CpuBatchPolicy::new(CpuBatchStrategy::Outer));
251    /// assert_eq!(previous.strategy, CpuBatchStrategy::Auto);
252    /// ```
253    pub fn set_batch_policy(&mut self, policy: crate::CpuBatchPolicy) -> crate::CpuBatchPolicy {
254        std::mem::replace(&mut self.batch_policy, policy)
255    }
256
257    /// The default batch policy of this engine.
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use tenferro_cpu::{CpuBatchStrategy, CpuEngine};
263    /// assert_eq!(CpuEngine::serial().batch_policy().strategy, CpuBatchStrategy::Auto);
264    /// ```
265    #[must_use]
266    pub fn batch_policy(&self) -> crate::CpuBatchPolicy {
267        self.batch_policy
268    }
269
270    /// The number of workers operations may use: the pool size, or one.
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// assert_eq!(tenferro_cpu::CpuEngine::serial().num_threads(), 1);
276    /// ```
277    pub fn num_threads(&self) -> usize {
278        self.thread_budget().get()
279    }
280
281    /// Whether the engine runs its parallel work on a reserved
282    /// `threadteam::Team`.
283    ///
284    /// `true` for an engine built from a pool of two or more workers whose
285    /// members were reserved. `false` for [`CpuEngine::serial`], for a
286    /// one-worker pool, and for a pool whose members could not be reserved
287    /// within about ten seconds of construction (the engine then runs serially
288    /// and [`CpuEngine::num_threads`] reports one). Benchmarks should assert
289    /// this after construction.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// assert!(!tenferro_cpu::CpuEngine::serial().has_team());
295    /// let pool = tenferro_cpu::Pool::builder().threads(2).build()?;
296    /// assert!(tenferro_cpu::CpuEngine::new(pool).has_team());
297    /// # Ok::<(), Box<dyn std::error::Error>>(())
298    /// ```
299    pub fn has_team(&self) -> bool {
300        self.resources.has_team()
301    }
302
303    pub(crate) fn thread_budget(&self) -> NonZeroUsize {
304        self.resources.thread_budget()
305    }
306
307    pub(crate) fn dispatch_costs(&self) -> &threadteam::DispatchCosts {
308        self.resources.dispatch_costs()
309    }
310
311    /// Whether an earlier operation broke this engine (see
312    /// [`CpuEngineError::Broken`]).
313    ///
314    /// # Examples
315    ///
316    /// ```
317    /// assert!(!tenferro_cpu::CpuEngine::serial().is_broken());
318    /// ```
319    pub fn is_broken(&self) -> bool {
320        self.broken
321    }
322
323    /// The provider execution bundle for this engine: a placement token over
324    /// every worker of its pool, its workspace and its batch policy.
325    ///
326    /// A downstream kernel that calls a provider (cpueinsum, tcontract,
327    /// tlinalg) directly passes this bundle by `&mut`, so it uses the same
328    /// threads, budget and batch policy as tenferro. The bundle is not `Clone`:
329    /// it is the engine's whole thread budget for that one call, and the provider
330    /// splits it. Getting it applies the same checks as an operation.
331    ///
332    /// # Errors
333    ///
334    /// Returns [`crate::Error::RuntimeState`] with a [`CpuEngineError`] source
335    /// when the engine is broken.
336    ///
337    /// # Examples
338    ///
339    /// ```
340    /// let mut engine = tenferro_cpu::CpuEngine::serial();
341    /// let exec = engine.exec()?;
342    /// assert_eq!(exec.budget(), 1);
343    /// # Ok::<(), tenferro_tensor::Error>(())
344    /// ```
345    pub fn exec(&mut self) -> crate::Result<cpueinsum::Exec<'_>> {
346        self.check_entry("CpuEngine::exec")?;
347        Ok(self.resources.exec(self.batch_policy))
348    }
349
350    /// Borrow the provider bundle, the buffer pool and the N-ary contraction
351    /// scratch for one downstream call.
352    ///
353    /// # Errors
354    ///
355    /// Returns the entry refusals of [`CpuEngine::exec`], or `op`'s own error.
356    ///
357    /// # Examples
358    ///
359    /// ```
360    /// let mut engine = tenferro_cpu::CpuEngine::serial();
361    /// let len = engine.with_contraction_exec(|_, buffers, workspaces| {
362    ///     workspaces.with_scratch::<f64, _>(4, buffers.max_retained_capacity_bytes(), |scratch| {
363    ///         Ok(scratch.len())
364    ///     })
365    /// })?;
366    /// assert_eq!(len, 4);
367    /// # Ok::<(), tenferro_tensor::Error>(())
368    /// ```
369    #[doc(hidden)]
370    pub fn with_contraction_exec<R>(
371        &mut self,
372        op: impl FnOnce(
373            &mut cpueinsum::Exec<'_>,
374            &mut BufferPool,
375            &crate::ContractionWorkspaces,
376        ) -> crate::Result<R>,
377    ) -> crate::Result<R> {
378        self.check_entry("CpuEngine::with_contraction_exec")?;
379        let _retention = self
380            .resources
381            .retention_guard(self.buffers.max_retained_capacity_bytes());
382        let mut exec = self.resources.exec(self.batch_policy);
383        let nary = &self.resources.nary;
384        guard_unwind(&mut self.buffers, &mut self.broken, |buffers| {
385            op(&mut exec, buffers, nary)
386        })
387    }
388
389    /// Borrow the provider bundle and buffer pool for a linalg provider call.
390    ///
391    /// The provider chooses its own execution through the bundle; this
392    /// callback itself runs on the calling thread.
393    ///
394    /// # Errors
395    ///
396    /// Returns the entry refusals of [`CpuEngine::exec`], or `op`'s own error.
397    ///
398    /// # Examples
399    ///
400    /// ```
401    /// use tenferro_cpu::{CpuBatchStrategy, CpuEngine};
402    ///
403    /// let mut engine = CpuEngine::serial();
404    /// let strategy = engine.with_linalg_pool(|exec, _| Ok(exec.batch_policy().strategy))?;
405    /// assert_eq!(strategy, CpuBatchStrategy::Auto);
406    /// # Ok::<(), tenferro_tensor::Error>(())
407    /// ```
408    #[doc(hidden)]
409    pub fn with_linalg_pool<R>(
410        &mut self,
411        op: impl FnOnce(&mut cpueinsum::Exec<'_>, &mut BufferPool) -> crate::Result<R>,
412    ) -> crate::Result<R> {
413        self.check_entry("CpuEngine::with_linalg_pool")?;
414        let mut exec = self.resources.exec(self.batch_policy);
415        guard_unwind(&mut self.buffers, &mut self.broken, |buffers| {
416            op(&mut exec, buffers)
417        })
418    }
419
420    /// Refuse an operation that cannot run on this engine now.
421    pub(crate) fn check_entry(&self, op: &'static str) -> crate::Result<()> {
422        if self.broken {
423            return Err(crate::Error::runtime_state_source(
424                op,
425                CpuEngineError::Broken,
426            ));
427        }
428        Ok(())
429    }
430
431    /// Ledger roots of every placement token the engine minted since the last
432    /// call, so a test can read `threadteam::ledger` for the operation it just
433    /// ran (`max_live`, `installs`).
434    ///
435    /// Available with the `ledger` cargo feature, which is test support.
436    #[cfg(feature = "ledger")]
437    #[doc(hidden)]
438    pub fn take_ledger_roots(&self) -> Vec<u64> {
439        self.resources.take_ledger_roots()
440    }
441
442    /// Pool entries (installs from a plain thread or a foreign pool) made by
443    /// the tokens minted since the last `take_ledger_*` call, which it consumes.
444    ///
445    /// Zero means every operation since then stayed on the calling thread.
446    /// Available with the `ledger` cargo feature, which is test support.
447    #[cfg(feature = "ledger")]
448    #[doc(hidden)]
449    pub fn take_ledger_installs(&self) -> usize {
450        self.take_ledger_roots()
451            .into_iter()
452            .map(threadteam::ledger::installs)
453            .sum()
454    }
455
456    /// Largest number of tasks live at once under any token minted since the
457    /// last `take_ledger_*` call, which it consumes. It never exceeds
458    /// [`CpuEngine::num_threads`].
459    ///
460    /// Available with the `ledger` cargo feature, which is test support.
461    #[cfg(feature = "ledger")]
462    #[doc(hidden)]
463    pub fn take_ledger_max_live(&self) -> usize {
464        self.take_ledger_roots()
465            .into_iter()
466            .map(threadteam::ledger::max_live)
467            .max()
468            .unwrap_or(0)
469    }
470
471    /// Number of retained buffers in the engine's buffer pool.
472    ///
473    /// # Examples
474    ///
475    /// ```
476    /// assert_eq!(tenferro_cpu::CpuEngine::serial().buffer_pool_len(), 0);
477    /// ```
478    pub fn buffer_pool_len(&self) -> usize {
479        self.buffers.len()
480    }
481
482    /// Retained-buffer statistics of the engine's buffer pool.
483    ///
484    /// # Examples
485    ///
486    /// ```
487    /// assert_eq!(tenferro_cpu::CpuEngine::serial().buffer_pool_stats().buffers, 0);
488    /// ```
489    pub fn buffer_pool_stats(&self) -> BufferPoolStats {
490        self.buffers.stats()
491    }
492
493    /// The buffer pool as cache statistics (entries and retained bytes).
494    ///
495    /// # Examples
496    ///
497    /// ```
498    /// assert_eq!(tenferro_cpu::CpuEngine::serial().buffer_pool_cache_stats().entries, 0);
499    /// ```
500    pub fn buffer_pool_cache_stats(&self) -> CacheStats {
501        let stats = self.buffers.stats();
502        CacheStats {
503            entries: stats.buffers,
504            retained_bytes: stats.capacity_bytes,
505            ..CacheStats::default()
506        }
507    }
508
509    /// The retention limit of recycled buffers, in retained capacity bytes.
510    ///
511    /// # Examples
512    ///
513    /// ```
514    /// let mut engine = tenferro_cpu::CpuEngine::serial();
515    /// engine.set_buffer_pool_limit_bytes(0);
516    /// assert_eq!(engine.buffer_pool_limit_bytes(), 0);
517    /// ```
518    pub fn buffer_pool_limit_bytes(&self) -> usize {
519        self.buffers.max_retained_capacity_bytes()
520    }
521
522    /// Bound the bytes of recycled buffers and contraction scratch retained
523    /// between operations; zero disables retention.
524    ///
525    /// Destinations larger than the retention limit cannot be recycled, so
526    /// repeated calls allocate again and may pay first-touch costs. The default
527    /// is 100 MiB; raise it when a run keeps producing outputs above that size.
528    ///
529    /// # Examples
530    ///
531    /// ```
532    /// let mut engine = tenferro_cpu::CpuEngine::serial();
533    /// engine.set_buffer_pool_limit_bytes(1 << 20);
534    /// assert_eq!(engine.buffer_pool_limit_bytes(), 1 << 20);
535    /// ```
536    pub fn set_buffer_pool_limit_bytes(&mut self, max_retained_capacity_bytes: usize) {
537        self.buffers
538            .set_max_retained_capacity_bytes(max_retained_capacity_bytes);
539        self.resources.nary.trim_to(max_retained_capacity_bytes);
540        if self.resources.retained_bytes() > max_retained_capacity_bytes {
541            self.resources.trim();
542        }
543    }
544
545    /// Drop every retained buffer and contraction scratch.
546    ///
547    /// # Examples
548    ///
549    /// ```
550    /// let mut engine = tenferro_cpu::CpuEngine::serial();
551    /// engine.reset_buffer_pool();
552    /// assert_eq!(engine.buffer_pool_len(), 0);
553    /// ```
554    pub fn reset_buffer_pool(&mut self) {
555        self.buffers.clear();
556        self.resources.nary.clear_all();
557        self.resources.trim();
558    }
559
560    /// Aggregate statistics of everything this engine retains between
561    /// operations: buffers, contraction scratch and its plan cache.
562    ///
563    /// # Examples
564    ///
565    /// ```
566    /// assert_eq!(tenferro_cpu::CpuEngine::serial().runtime_cache_stats().entries, 0);
567    /// ```
568    #[doc(hidden)]
569    pub fn runtime_cache_stats(&self) -> CacheStats {
570        let (workspace_entries, nary_bytes) = self.resources.nary.total_stats();
571        let workspace_bytes = nary_bytes.saturating_add(self.resources.retained_bytes());
572        let buffers = self.buffers.cache_stats();
573        let plans = self.plans.stats();
574        CacheStats {
575            entries: buffers
576                .entries
577                .saturating_add(plans.entries)
578                .saturating_add(workspace_entries),
579            retained_bytes: buffers
580                .retained_bytes
581                .saturating_add(plans.retained_bytes)
582                .saturating_add(workspace_bytes),
583            hits: plans.hits,
584            misses: plans.misses,
585            evictions: plans.evictions,
586            clears: self.runtime_clears,
587        }
588    }
589
590    /// Drop everything this engine retains between operations, including its
591    /// plan cache (and therefore the plans of every engine sharing it).
592    ///
593    /// # Examples
594    ///
595    /// ```
596    /// let mut engine = tenferro_cpu::CpuEngine::serial();
597    /// engine.clear_runtime_caches();
598    /// assert_eq!(engine.runtime_cache_stats().clears, 1);
599    /// ```
600    #[doc(hidden)]
601    pub fn clear_runtime_caches(&mut self) {
602        self.reset_buffer_pool();
603        self.plans.clear();
604        self.runtime_clears = self.runtime_clears.saturating_add(1);
605    }
606}
607
608/// Run one operation's use of the buffer pool, recovering the pool's
609/// accounting and marking the engine broken if it unwinds.
610///
611/// Buffers checked out during the operation are counted as in flight. A normal
612/// exit forgets that accounting (the buffers left as outputs); an unwind
613/// replenishes the pool with replacements for the buffers it lost and marks the
614/// engine broken, since other workspace may be half updated.
615pub(crate) fn guard_unwind<R>(
616    buffers: &mut BufferPool,
617    broken: &mut bool,
618    op: impl FnOnce(&mut BufferPool) -> R,
619) -> R {
620    match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| op(&mut *buffers))) {
621        Ok(value) => value,
622        Err(payload) => {
623            // The engine is never used again after a panic, so the buffers the
624            // operation had checked out need no accounting repair.
625            *broken = true;
626            std::panic::resume_unwind(payload)
627        }
628    }
629}
630
631impl CpuEngine {
632    /// Split borrows for the operation helpers: the provider bundle minted for
633    /// this operation (it borrows only the pool resources), the buffer pool, the
634    /// plan cache and the broken flag.
635    pub(crate) fn parts(
636        &mut self,
637    ) -> (
638        cpueinsum::Exec<'_>,
639        &mut BufferPool,
640        &CpuPlanCache,
641        &mut bool,
642    ) {
643        (
644            self.resources.exec(self.batch_policy),
645            &mut self.buffers,
646            &self.plans,
647            &mut self.broken,
648        )
649    }
650}
651
652#[cfg(test)]
653mod tests;