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;