tensor4all_tensorbackend/explicit.rs
1//! Opt-in explicit/concrete execution frontend.
2//!
3//! This is the second, opt-in frontend of
4//! [tensor4all-rs#859](https://github.com/tensor4all/tensor4all-rs/issues/859): a
5//! caller-supplied [`CpuExecutionContext`] plus one concrete session that a whole
6//! stage or batch reuses, instead of the process-global convenience surface in
7//! the compatibility entry point. The compatibility frontend keeps its own signatures,
8//! defaults and behaviour; nothing here reroutes it.
9//!
10//! Properties of this frontend, from the coexistence record
11//! (`docs/design/859-dual-frontend-coexistence.md`):
12//!
13//! - **One explicit entry.** [`CpuExecutionContext::with_concrete_session`] lends the
14//! context's engine for the whole closure, so a stage borrows it once rather than once
15//! per operation, and the engine's prepared plans and buffer pool stay warm.
16//! - **No process-global selector.** No route here selects an execution context or
17//! backend from a default context, a thread-local override or an environment
18//! variable. (Upstream profiling hooks may still read their own environment flags;
19//! those do not choose where the work runs.)
20//! - **No eager, trace or AD records.** These routes reach only concrete
21//! `Tensor`/engine operations; they never construct `EagerTensor`,
22//! semantic nodes or gradient slots, and they take no eager owner lock. Tracked
23//! values are not accepted at all, so no detach can happen implicitly.
24//! - **No implicit fallback.** An unsupported dtype or shape returns the
25//! backend's typed error. Nothing here retries on the global or CPU-eager route.
26//! - **Explicit conversion at the boundary.** Crossing between this frontend and
27//! the compatibility frontend is [`LogicalTensor`](crate::LogicalTensor), which
28//! carries dtype, shape and column-major data and no backend identity.
29//!
30//! The session is a borrow of one [`tenferro_cpu::CpuEngine`], not a value
31//! representation: the concrete values stay the ordinary tenferro tensors.
32//!
33//! # Examples
34//!
35//! ```
36//! use tensor4all_tensorbackend::{CpuExecutionContext, explicit::Session};
37//! use tenferro::Tensor;
38//! use tensor4all_tensorbackend::CpuBackend;
39//!
40//! let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
41//! let x = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
42//!
43//! let product = context.with_concrete_session(|session: &mut Session<'_>| {
44//! session.qr(&x).map(|(q, _r)| q)
45//! })?;
46//! assert_eq!(product.expect("QR succeeds").shape(), &[2, 2]);
47//! # Ok::<(), Box<dyn std::error::Error>>(())
48//! ```
49
50use tenferro::Tensor as NativeTensor;
51use tenferro::TensorSessionOpsExt;
52use tenferro_einsum::{ConcreteEinsumPlan, EinsumSubscripts, TensorEinsumExt};
53use tenferro_linalg::TensorLinalgExt;
54
55use tenferro_tensor::Engine;
56
57use crate::context::{CpuExecutionContext, CpuExecutionContextError};
58use crate::einsum_ids::{
59 build_binary_einsum_ids, checked_native_einsum_labels, common_dtype, convert_native_tensor_in,
60};
61
62/// One explicit concrete session over a caller-held CPU engine.
63///
64/// The session borrows an engine for the whole of its life. Inside
65/// [`CpuExecutionContext::with_concrete_session`] that is the context's engine,
66/// borrowed for the closure; a caller that owns an engine ([`crate::CpuBackend`])
67/// builds the same session with [`Session::new`] and keeps it across as many
68/// operations as it wants. Every route on it runs through that one engine, so a
69/// stage does not pay an entry per operation.
70///
71/// # Examples
72///
73/// ```
74/// use tensor4all_tensorbackend::CpuExecutionContext;
75/// use tenferro::Tensor;
76/// use tensor4all_tensorbackend::CpuBackend;
77///
78/// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
79/// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
80/// let b = Tensor::from_vec_col_major(vec![2, 2], vec![0.0_f64, 1.0, 1.0, 0.0])?;
81/// let (ab, a_again) = context.with_concrete_session(
82/// |session| -> Result<_, Box<dyn std::error::Error + Send + Sync>> {
83/// let ab = session.contraction(&a, &[1], &b, &[0])?;
84/// // One session, two operations.
85/// let a_again = session.permute(&a, &[1, 0])?;
86/// Ok((ab, a_again))
87/// },
88/// )??;
89/// // `a` is `[[1, 3], [2, 4]]` column-major and `b` swaps the axes, so `a * b`
90/// // is `[[3, 1], [4, 2]]`.
91/// assert_eq!(ab.as_slice::<f64>()?, &[3.0, 4.0, 1.0, 2.0]);
92/// assert_eq!(a_again.as_slice::<f64>()?, &[1.0, 3.0, 2.0, 4.0]);
93/// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
94/// ```
95pub struct Session<'session> {
96 engine: &'session mut dyn Engine,
97}
98
99impl std::fmt::Debug for Session<'_> {
100 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 formatter
102 .debug_struct("explicit::Session")
103 .finish_non_exhaustive()
104 }
105}
106
107/// Failure reported by this frontend's compatibility bridges.
108///
109/// The original tenferro or tenferro-ad diagnostic is retained as the source.
110#[derive(Debug, thiserror::Error)]
111pub enum ConversionError {
112 /// Materializing a tracked value into a plain concrete value failed.
113 #[error("materializing a tracked value failed: {source}")]
114 Detach {
115 /// Original tenferro diagnostic.
116 #[source]
117 source: Box<dyn std::error::Error + Send + Sync + 'static>,
118 },
119 /// Adopting a plain concrete value into an eager runtime failed.
120 #[error("adopting a concrete value failed: {source}")]
121 Lift {
122 /// Original tenferro or tenferro-ad diagnostic.
123 #[source]
124 source: Box<dyn std::error::Error + Send + Sync + 'static>,
125 },
126}
127
128/// Materialize a tracked eager value into a plain concrete value on `engine`.
129///
130/// This is the explicit AD to plain boundary of the coexistence contract: the caller names
131/// the conversion, so tracking never disappears silently, and `no_grad` on an eager value is
132/// not this operation and does not select the concrete representation. The value keeps its
133/// dtype, logical shape and column-major layout; the result is an ordinary concrete tensor
134/// that this frontend's routes accept. Materializing a lazy or prepared value needs an
135/// engine to evaluate it, so the caller passes the one it wants the copy to run on.
136///
137/// # Errors
138///
139/// Returns [`ConversionError::Detach`] when the tracked value cannot be materialized, for
140/// example because it is a lazy or prepared value that has not been evaluated.
141///
142/// # Examples
143///
144/// ```
145/// use tensor4all_tensorbackend::{explicit, CpuExecutionContext};
146/// use tenferro::Tensor;
147/// use tenferro_ad::EagerTensor;
148/// use tensor4all_tensorbackend::CpuBackend;
149///
150/// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
151/// let plain = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0])?;
152/// let tracked = explicit::lift(&context, plain)?;
153/// let back = context.with_backend(|backend| explicit::detach(&tracked, backend.engine_mut()))?;
154/// assert_eq!(back.as_slice::<f64>()?, &[1.0, 2.0]);
155/// # Ok::<(), Box<dyn std::error::Error>>(())
156/// ```
157pub fn detach(
158 eager: &tenferro_ad::EagerTensor,
159 engine: &mut dyn Engine,
160) -> Result<NativeTensor, ConversionError> {
161 eager
162 .to_tensor(engine)
163 .map_err(|source| ConversionError::Detach {
164 source: Box::new(source),
165 })
166}
167
168/// Adopt a plain concrete value into `context`'s eager runtime.
169///
170/// This is the explicit plain to AD boundary: the result is a tracked value in a graph owned
171/// by `context`'s own eager runtime, and nothing about it is inferred from the caller's
172/// ambient `no_grad` or capture state. The value is moved into the runtime, because a
173/// concrete tensor has one owner. Use [`detach`] for the opposite direction.
174///
175/// # Errors
176///
177/// Returns [`ConversionError::Lift`] when the context's eager runtime cannot be initialized
178/// or when the value cannot be adopted, for example because it is not a concrete host value.
179///
180/// # Examples
181///
182/// ```
183/// use tensor4all_tensorbackend::{explicit, CpuExecutionContext};
184/// use tenferro::Tensor;
185/// use tensor4all_tensorbackend::CpuBackend;
186///
187/// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
188/// let plain = Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0])?;
189/// let tracked = explicit::lift(&context, plain)?;
190/// // The adopted value is tracked, and its runtime is the context's own.
191/// let back = context.with_backend(|backend| explicit::detach(&tracked, backend.engine_mut()))?;
192/// assert_eq!(back.as_slice::<f64>()?, &[3.0, 4.0]);
193/// # Ok::<(), Box<dyn std::error::Error>>(())
194/// ```
195pub fn lift(
196 context: &CpuExecutionContext,
197 tensor: NativeTensor,
198) -> Result<tenferro_ad::EagerTensor, ConversionError> {
199 let runtime = context
200 .eager_runtime()
201 .map_err(|source| ConversionError::Lift {
202 source: Box::new(source),
203 })?;
204 tenferro_ad::EagerTensor::from_tensor_in(tensor, runtime).map_err(|source| {
205 ConversionError::Lift {
206 source: Box::new(source),
207 }
208 })
209}
210
211/// One prepared N-ary einsum, reusable across operations of a session.
212///
213/// Preparing validates the labels and plans the contraction once; every
214/// [`PreparedEinsum::execute`] and [`PreparedEinsum::execute_into`] reuses that plan, so
215/// a stage keeps one plan across as many evaluations as it needs instead of preparing
216/// per call. The plan borrows its operands, so it cannot outlive them, and it holds no
217/// execution state: the session stays the caller's.
218///
219/// # Examples
220///
221/// ```
222/// use tensor4all_tensorbackend::{explicit::PreparedEinsum, CpuExecutionContext};
223/// use tenferro::Tensor;
224/// use tensor4all_tensorbackend::CpuBackend;
225/// use tenferro_tensor::{TensorRead, TensorWrite};
226///
227/// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
228/// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
229/// let b = Tensor::from_vec_col_major(vec![2, 1], vec![5.0_f64, 6.0])?;
230/// let reads = [TensorRead::from_tensor(&a), TensorRead::from_tensor(&b)];
231/// let plan = PreparedEinsum::prepare(&reads, &[&[0, 1], &[1, 2]], &[0, 2])?;
232///
233/// // Reused across a stage, including into a caller-provided destination.
234/// let mut out = Tensor::from_vec_col_major(vec![2, 1], vec![0.0_f64, 0.0])?;
235/// context.with_concrete_session(|session| -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
236/// let product = plan.execute(session)?;
237/// assert_eq!(product.as_slice::<f64>()?, &[23.0, 34.0]);
238/// plan.execute_into(session, TensorWrite::Tensor(&mut out))?;
239/// Ok(())
240/// })??;
241/// assert_eq!(out.as_slice::<f64>()?, &[23.0, 34.0]);
242/// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
243/// ```
244pub struct PreparedEinsum<'operands> {
245 plan: ConcreteEinsumPlan,
246 operands: Vec<tenferro_tensor::TensorRead<'operands>>,
247}
248
249impl std::fmt::Debug for PreparedEinsum<'_> {
250 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 formatter
252 .debug_struct("explicit::PreparedEinsum")
253 .field("operands", &self.operands.len())
254 .finish_non_exhaustive()
255 }
256}
257
258impl<'operands> PreparedEinsum<'operands> {
259 /// Plan an N-ary einsum over borrowed operands.
260 ///
261 /// # Errors
262 ///
263 /// Returns [`tenferro_einsum::Error::InvalidSubscripts`] for malformed labels, a
264 /// label list whose length does not match the operand's rank, or a label-list count
265 /// that does not match the operand count, and [`tenferro_einsum::Error`] from the
266 /// contraction planner when the expression cannot be planned.
267 pub fn prepare(
268 operands: &[tenferro_tensor::TensorRead<'operands>],
269 input_ids: &[&[usize]],
270 output_ids: &[usize],
271 ) -> tenferro_einsum::Result<Self> {
272 if operands.len() != input_ids.len() {
273 return Err(tenferro_einsum::Error::InvalidSubscripts {
274 message: format!(
275 "einsum needs one label list per operand: {} operands, {} label lists",
276 operands.len(),
277 input_ids.len()
278 ),
279 });
280 }
281 let invalid = |error: anyhow::Error| tenferro_einsum::Error::InvalidSubscripts {
282 message: format!("{error}"),
283 };
284 let inputs = input_ids
285 .iter()
286 .map(|ids| checked_native_einsum_labels(ids))
287 .collect::<anyhow::Result<Vec<_>>>()
288 .map_err(invalid)?;
289 let output = checked_native_einsum_labels(output_ids).map_err(invalid)?;
290 let subscripts = EinsumSubscripts { inputs, output };
291 let plan = ConcreteEinsumPlan::prepare_read_subscripts(operands, &subscripts)?;
292 Ok(Self {
293 plan,
294 operands: operands.to_vec(),
295 })
296 }
297
298 /// Evaluate the prepared plan on `session`, allocating the result.
299 ///
300 /// # Errors
301 ///
302 /// Returns [`tenferro_einsum::Error::Tensor`] when the session rejects the operands
303 /// or the contraction itself fails, and
304 /// [`tenferro_einsum::Error::Validation`] when the operands no longer match the
305 /// planned expression.
306 pub fn execute(&self, session: &mut Session<'_>) -> tenferro_einsum::Result<NativeTensor> {
307 self.plan
308 .execute_read(self.operands.as_slice(), session.engine())
309 }
310
311 /// Evaluate the prepared plan into a caller-provided destination.
312 ///
313 /// The destination is fully written and is never zero-filled first. Operands and
314 /// destination must already share one dtype; this route does not convert into a
315 /// caller-owned output.
316 ///
317 /// # Errors
318 ///
319 /// Returns [`tenferro_einsum::Error::Tensor`] when the session rejects the operands,
320 /// the destination or the contraction, and
321 /// [`tenferro_einsum::Error::Validation`] when the operands no longer match the
322 /// planned expression.
323 pub fn execute_into(
324 &self,
325 session: &mut Session<'_>,
326 out: tenferro_tensor::TensorWrite<'_>,
327 ) -> tenferro_einsum::Result<()> {
328 self.plan
329 .execute_read_into(self.operands.as_slice(), session.engine(), out)
330 }
331}
332
333/// Promote a heterogeneous operand set to the dtype both frontends contract in.
334///
335/// Returns an empty vector when every operand already has the common dtype, so the
336/// common case allocates nothing.
337///
338/// # Errors
339///
340/// Returns [`tenferro_einsum::Error::Tensor`] when an operand cannot be converted.
341fn promote_operands(
342 engine: &mut dyn Engine,
343 operands: &[&NativeTensor],
344) -> tenferro_einsum::Result<Vec<NativeTensor>> {
345 let dtypes = operands
346 .iter()
347 .map(|tensor| tensor.dtype())
348 .collect::<Vec<_>>();
349 let target = common_dtype(&dtypes);
350 if operands.iter().all(|tensor| tensor.dtype() == target) {
351 return Ok(Vec::new());
352 }
353 operands
354 .iter()
355 .map(|tensor| {
356 convert_native_tensor_in(engine, tensor, target).map_err(tenferro_einsum::Error::Tensor)
357 })
358 .collect()
359}
360
361impl<'session> Session<'session> {
362 /// Wrap a borrowed engine as this frontend's session.
363 ///
364 /// A caller that owns an engine - a [`crate::CpuBackend`], an engine it built
365 /// itself, or one borrowed for a stage - builds the same [`Session`] the
366 /// callback-scoped [`CpuExecutionContext::with_concrete_session`] hands out, so a
367 /// long stage can keep one session across many operations without entering a
368 /// context. The engine stays borrowed for the whole life of the returned session:
369 /// this constructor never enters, replaces or reroutes an engine.
370 ///
371 /// # Examples
372 ///
373 /// ```
374 /// use tensor4all_tensorbackend::{explicit::Session, CpuBackend};
375 /// use tenferro::Tensor;
376 ///
377 /// let mut backend = CpuBackend::with_threads(1)?;
378 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
379 ///
380 /// let mut session = Session::new(backend.engine_mut());
381 /// // One session, two operations.
382 /// let transposed = session.permute(&a, &[1, 0])?;
383 /// let total = session.sum(&transposed)?;
384 /// assert_eq!(total.as_slice::<f64>()?, &[10.0]);
385 /// # Ok::<(), Box<dyn std::error::Error>>(())
386 /// ```
387 pub fn new(engine: &'session mut dyn Engine) -> Self {
388 Self { engine }
389 }
390
391 /// The engine behind this explicit session.
392 ///
393 /// Exposed so a prepared plan can evaluate itself on the caller's engine; it is
394 /// not a second entry point, and it never consults another backend.
395 pub fn engine(&mut self) -> &mut dyn Engine {
396 self.engine
397 }
398
399 /// Reshape a concrete tensor without changing its column-major linearization.
400 ///
401 /// # Errors
402 ///
403 /// Returns [`tenferro_tensor::Error::Validation`] when the element counts
404 /// differ or the requested layout is invalid,
405 /// [`tenferro_tensor::Error::UnsupportedDType`] for a dtype the backend cannot
406 /// reshape, and [`tenferro_tensor::Error::BackendSource`] when the operation
407 /// itself fails. It never retries on another route.
408 ///
409 /// # Examples
410 ///
411 /// ```
412 /// use tensor4all_tensorbackend::CpuExecutionContext;
413 /// use tenferro::Tensor;
414 /// use tensor4all_tensorbackend::CpuBackend;
415 ///
416 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
417 /// let x = Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0])?;
418 /// let reshaped = context.with_concrete_session(|session| session.reshape(&x, &[2, 2]))??;
419 /// assert_eq!(reshaped.as_slice::<f64>()?, &[1.0, 2.0, 3.0, 4.0]);
420 /// # Ok::<(), Box<dyn std::error::Error>>(())
421 /// ```
422 pub fn reshape(
423 &mut self,
424 tensor: &NativeTensor,
425 shape: &[usize],
426 ) -> tenferro_tensor::Result<NativeTensor> {
427 tensor.reshape(shape, self.engine)
428 }
429
430 /// Permute the axes of a concrete tensor.
431 ///
432 /// # Errors
433 ///
434 /// Returns [`tenferro_tensor::Error::Validation`] when `perm` is not a
435 /// permutation of the tensor's axes, and
436 /// [`tenferro_tensor::Error::BackendSource`] when the operation itself fails.
437 ///
438 /// # Examples
439 ///
440 /// ```
441 /// use tensor4all_tensorbackend::CpuExecutionContext;
442 /// use tenferro::Tensor;
443 /// use tensor4all_tensorbackend::CpuBackend;
444 ///
445 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
446 /// let x = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
447 /// let transposed = context.with_concrete_session(|session| session.permute(&x, &[1, 0]))??;
448 /// assert_eq!(transposed.as_slice::<f64>()?, &[1.0, 3.0, 2.0, 4.0]);
449 /// # Ok::<(), Box<dyn std::error::Error>>(())
450 /// ```
451 pub fn permute(
452 &mut self,
453 tensor: &NativeTensor,
454 perm: &[usize],
455 ) -> tenferro_tensor::Result<NativeTensor> {
456 tensor.transpose(perm, self.engine)
457 }
458
459 /// Binary contraction along the given axes, preserving the remaining axes in
460 /// order.
461 ///
462 /// The operand identity and axis building is the same code path the
463 /// compatibility frontend uses, so both frontends contract identically; only
464 /// the session differs.
465 ///
466 /// # Errors
467 ///
468 /// Returns [`tenferro_einsum::Error::InvalidSubscripts`] when the axis lists are
469 /// malformed or of different length, and [`tenferro_einsum::Error::Tensor`] when the
470 /// backend rejects the operands, when an operand cannot be promoted, or when the
471 /// contraction itself fails.
472 ///
473 /// Operands are promoted to a common dtype exactly as the compatibility frontend
474 /// promotes them: a heterogeneous set contracts in the promoted dtype, and a set
475 /// that already shares one dtype is used as is.
476 ///
477 /// # Examples
478 ///
479 /// ```
480 /// use tensor4all_tensorbackend::CpuExecutionContext;
481 /// use tenferro::Tensor;
482 /// use tensor4all_tensorbackend::CpuBackend;
483 ///
484 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
485 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
486 /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![5.0_f64, 6.0])?;
487 /// let product =
488 /// context.with_concrete_session(|session| session.contraction(&a, &[1], &b, &[0]))??;
489 /// assert_eq!(product.as_slice::<f64>()?, &[23.0, 34.0]);
490 /// # Ok::<(), Box<dyn std::error::Error>>(())
491 /// ```
492 pub fn contraction(
493 &mut self,
494 lhs: &NativeTensor,
495 lhs_axes: &[usize],
496 rhs: &NativeTensor,
497 rhs_axes: &[usize],
498 ) -> tenferro_einsum::Result<NativeTensor> {
499 let (lhs_ids, rhs_ids, output_ids) =
500 build_binary_einsum_ids(lhs.shape().len(), lhs_axes, rhs.shape().len(), rhs_axes)
501 .map_err(|error| tenferro_einsum::Error::InvalidSubscripts {
502 message: format!("{error}"),
503 })?;
504 let subscripts = EinsumSubscripts {
505 inputs: vec![lhs_ids, rhs_ids],
506 output: output_ids,
507 };
508 let operands = [lhs, rhs];
509 let promoted = promote_operands(self.engine, &operands)?;
510 let operands = if promoted.is_empty() {
511 operands.to_vec()
512 } else {
513 promoted.iter().collect::<Vec<_>>()
514 };
515 operands.einsum_subscripts(&subscripts, self.engine)
516 }
517
518 /// N-ary einsum from integer labels, evaluated on this session.
519 ///
520 /// The labels are validated by the same helper the compatibility frontend
521 /// uses; the evaluation is tenferro-einsum's session-direct einsum, so it does
522 /// not compile a semantic graph or start a runtime worker.
523 ///
524 /// # Errors
525 ///
526 /// Returns [`tenferro_einsum::Error::InvalidSubscripts`] for malformed labels, a
527 /// label list whose length does not match the operand's rank, or a label-list
528 /// count that does not match the operand count, and
529 /// [`tenferro_einsum::Error::Tensor`] when the backend rejects the operands, when an
530 /// operand cannot be promoted, or when the contraction itself fails.
531 ///
532 /// Operands are promoted to a common dtype exactly as the compatibility frontend
533 /// promotes them: a heterogeneous set contracts in the promoted dtype, and a set
534 /// that already shares one dtype is used as is.
535 ///
536 /// # Examples
537 ///
538 /// ```
539 /// use tensor4all_tensorbackend::CpuExecutionContext;
540 /// use tenferro::Tensor;
541 /// use tensor4all_tensorbackend::CpuBackend;
542 ///
543 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
544 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
545 /// let b = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0])?;
546 /// let product = context.with_concrete_session(|session| {
547 /// session.einsum(&[&a, &b], &[&[0, 1], &[1, 2]], &[0, 2])
548 /// })??;
549 /// assert_eq!(product.as_slice::<f64>()?, &[1.0, 2.0, 3.0, 4.0]);
550 /// # Ok::<(), Box<dyn std::error::Error>>(())
551 /// ```
552 pub fn einsum(
553 &mut self,
554 operands: &[&NativeTensor],
555 input_ids: &[&[usize]],
556 output_ids: &[usize],
557 ) -> tenferro_einsum::Result<NativeTensor> {
558 if operands.len() != input_ids.len() {
559 return Err(tenferro_einsum::Error::InvalidSubscripts {
560 message: format!(
561 "einsum needs one label list per operand: {} operands, {} label lists",
562 operands.len(),
563 input_ids.len()
564 ),
565 });
566 }
567 let inputs = input_ids
568 .iter()
569 .map(|ids| checked_native_einsum_labels(ids))
570 .collect::<anyhow::Result<Vec<_>>>()
571 .map_err(|error| tenferro_einsum::Error::InvalidSubscripts {
572 message: format!("{error}"),
573 })?;
574 let subscripts = EinsumSubscripts {
575 inputs,
576 output: checked_native_einsum_labels(output_ids).map_err(|error| {
577 tenferro_einsum::Error::InvalidSubscripts {
578 message: format!("{error}"),
579 }
580 })?,
581 };
582 let promoted = promote_operands(self.engine, operands)?;
583 let operands: Vec<&NativeTensor> = if promoted.is_empty() {
584 operands.to_vec()
585 } else {
586 promoted.iter().collect()
587 };
588 operands.einsum_subscripts(&subscripts, self.engine)
589 }
590
591 /// Sum every element of a concrete tensor, returning the rank-0 result.
592 ///
593 /// The compatibility frontend wraps the same rank-0 tensor in a
594 /// `BackendScalar`; this route stays on the concrete value so it works without
595 /// the compatibility feature.
596 ///
597 /// # Errors
598 ///
599 /// Returns [`tenferro_tensor::Error::Validation`] for an unsupported reduction
600 /// shape and [`tenferro_tensor::Error::BackendSource`] when the reduction itself
601 /// fails.
602 ///
603 /// # Examples
604 ///
605 /// ```
606 /// use tensor4all_tensorbackend::CpuExecutionContext;
607 /// use tenferro::Tensor;
608 /// use tensor4all_tensorbackend::CpuBackend;
609 ///
610 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
611 /// let x = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
612 /// let total = context.with_concrete_session(|session| session.sum(&x))??;
613 /// assert_eq!(total.shape(), &[] as &[usize]);
614 /// assert_eq!(total.as_slice::<f64>()?, &[10.0]);
615 /// # Ok::<(), Box<dyn std::error::Error>>(())
616 /// ```
617 pub fn sum(&mut self, tensor: &NativeTensor) -> tenferro_tensor::Result<NativeTensor> {
618 if tensor.shape().is_empty() {
619 return tensor.duplicate();
620 }
621 let axes = (0..tensor.shape().len()).collect::<Vec<_>>();
622 tensor.reduce_sum(Some(&axes), self.engine)
623 }
624
625 /// Conjugate a concrete tensor.
626 ///
627 /// A real tensor is returned unchanged; a complex tensor is conjugated
628 /// elementwise.
629 ///
630 /// # Errors
631 ///
632 /// Returns [`tenferro_tensor::Error::Unsupported`] for a dtype the backend cannot
633 /// conjugate and [`tenferro_tensor::Error::BackendSource`] when the operation
634 /// itself fails.
635 ///
636 /// # Examples
637 ///
638 /// ```
639 /// use tensor4all_tensorbackend::CpuExecutionContext;
640 /// use tenferro::Tensor;
641 /// use tensor4all_tensorbackend::CpuBackend;
642 /// use num_complex::Complex64;
643 ///
644 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
645 /// let x = Tensor::from_vec_col_major(
646 /// vec![2],
647 /// vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, -4.0)],
648 /// )?;
649 /// let conjugated = context.with_concrete_session(|session| session.conj(&x))??;
650 /// assert_eq!(
651 /// conjugated.as_slice::<Complex64>()?,
652 /// &[Complex64::new(1.0, -2.0), Complex64::new(3.0, 4.0)]
653 /// );
654 /// # Ok::<(), Box<dyn std::error::Error>>(())
655 /// ```
656 pub fn conj(&mut self, tensor: &NativeTensor) -> tenferro_tensor::Result<NativeTensor> {
657 tensor.conj(self.engine)
658 }
659
660 /// N-ary einsum over borrowed read inputs, without materializing them.
661 ///
662 /// Same contract as [`Session::einsum`], except that the operands are
663 /// [`TensorRead`](tenferro_tensor::TensorRead) views: a non-contiguous or lazily
664 /// represented operand stays borrowed until the contract evaluates it.
665 ///
666 /// # Errors
667 ///
668 /// Returns [`tenferro_einsum::Error::InvalidSubscripts`] for malformed labels or a
669 /// label list whose length does not match the operand's rank, and
670 /// [`tenferro_einsum::Error::Tensor`] when the backend rejects the operands or the
671 /// contraction itself fails.
672 ///
673 /// # Examples
674 ///
675 /// ```
676 /// use tensor4all_tensorbackend::CpuExecutionContext;
677 /// use tenferro::Tensor;
678 /// use tensor4all_tensorbackend::CpuBackend;
679 /// use tenferro_tensor::TensorRead;
680 ///
681 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
682 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
683 /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![5.0_f64, 6.0])?;
684 /// let reads = [TensorRead::from_tensor(&a), TensorRead::from_tensor(&b)];
685 /// let product = context
686 /// .with_concrete_session(|session| session.einsum_reads(&reads, &[&[0, 1], &[1, 2]], &[0, 2]))??;
687 /// assert_eq!(product.as_slice::<f64>()?, &[23.0, 34.0]);
688 /// # Ok::<(), Box<dyn std::error::Error>>(())
689 /// ```
690 pub fn einsum_reads(
691 &mut self,
692 operands: &[tenferro_tensor::TensorRead<'_>],
693 input_ids: &[&[usize]],
694 output_ids: &[usize],
695 ) -> tenferro_einsum::Result<NativeTensor> {
696 if operands.len() != input_ids.len() {
697 return Err(tenferro_einsum::Error::InvalidSubscripts {
698 message: format!(
699 "einsum needs one label list per operand: {} operands, {} label lists",
700 operands.len(),
701 input_ids.len()
702 ),
703 });
704 }
705 let invalid = |error: anyhow::Error| tenferro_einsum::Error::InvalidSubscripts {
706 message: format!("{error}"),
707 };
708 let inputs = input_ids
709 .iter()
710 .map(|ids| checked_native_einsum_labels(ids))
711 .collect::<anyhow::Result<Vec<_>>>()
712 .map_err(invalid)?;
713 let output = checked_native_einsum_labels(output_ids).map_err(invalid)?;
714 let subscripts = EinsumSubscripts { inputs, output };
715 let plan = ConcreteEinsumPlan::prepare_read_subscripts(operands, &subscripts)?;
716 plan.execute_read(operands, self.engine)
717 }
718
719 /// Binary contraction into a caller-provided destination.
720 ///
721 /// The destination is fully written and is never zero-filled first. The operands
722 /// and the destination must already share one dtype: this route does not convert
723 /// into a caller-owned output, and a mismatch is rejected typed by the backend.
724 ///
725 /// # Errors
726 ///
727 /// Returns [`tenferro_einsum::Error::InvalidSubscripts`] when the axis lists are
728 /// malformed or of different length, and [`tenferro_einsum::Error::Tensor`] when the
729 /// backend rejects the operands, the destination or the contraction.
730 ///
731 /// # Examples
732 ///
733 /// ```
734 /// use tensor4all_tensorbackend::CpuExecutionContext;
735 /// use tenferro::Tensor;
736 /// use tensor4all_tensorbackend::CpuBackend;
737 /// use tenferro_tensor::TensorWrite;
738 ///
739 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
740 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
741 /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![5.0_f64, 6.0])?;
742 /// let mut out = Tensor::from_vec_col_major(vec![2, 1], vec![0.0_f64, 0.0])?;
743 /// context.with_concrete_session(|session| {
744 /// session.contraction_into(&a, &[1], &b, &[0], TensorWrite::Tensor(&mut out))
745 /// })??;
746 /// assert_eq!(out.as_slice::<f64>()?, &[23.0, 34.0]);
747 /// # Ok::<(), Box<dyn std::error::Error>>(())
748 /// ```
749 pub fn contraction_into(
750 &mut self,
751 lhs: &NativeTensor,
752 lhs_axes: &[usize],
753 rhs: &NativeTensor,
754 rhs_axes: &[usize],
755 out: tenferro_tensor::TensorWrite<'_>,
756 ) -> tenferro_einsum::Result<()> {
757 let (lhs_ids, rhs_ids, output_ids) =
758 build_binary_einsum_ids(lhs.shape().len(), lhs_axes, rhs.shape().len(), rhs_axes)
759 .map_err(|error| tenferro_einsum::Error::InvalidSubscripts {
760 message: format!("{error}"),
761 })?;
762 let subscripts = EinsumSubscripts {
763 inputs: vec![lhs_ids, rhs_ids],
764 output: output_ids,
765 };
766 let inputs = [
767 tenferro_tensor::TensorRead::from_tensor(lhs),
768 tenferro_tensor::TensorRead::from_tensor(rhs),
769 ];
770 let plan = ConcreteEinsumPlan::prepare_read_subscripts(&inputs[..], &subscripts)?;
771 plan.execute_read_into(&inputs[..], self.engine, out)
772 }
773
774 /// Solve `A X = B` for column-major matrices on this session.
775 ///
776 /// Same kernel, conversion and validation as
777 /// [`solve_matrix`](crate::solve_matrix), entered through this session instead of
778 /// the process-global one.
779 ///
780 /// # Errors
781 ///
782 /// Returns [`BackendLinalgError`](crate::BackendLinalgError) under the same
783 /// conditions as [`solve_matrix`](crate::solve_matrix).
784 ///
785 /// # Examples
786 ///
787 /// ```
788 /// use tensor4all_tensorbackend::{CpuExecutionContext, Matrix};
789 /// use tensor4all_tensorbackend::CpuBackend;
790 ///
791 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
792 /// let a = Matrix::from_col_major_vec(2, 2, vec![2.0_f64, 0.0, 0.0, 4.0]);
793 /// let b = Matrix::from_col_major_vec(2, 1, vec![6.0_f64, 8.0]);
794 /// let x = context.with_concrete_session(|session| session.solve_matrix(&a, &b))??;
795 /// assert_eq!(x.as_col_major_slice(), &[3.0, 2.0]);
796 /// # Ok::<(), Box<dyn std::error::Error>>(())
797 /// ```
798 #[cfg(feature = "global-defaults")]
799 pub fn solve_matrix<T>(
800 &mut self,
801 a: &crate::matrix::Matrix<T>,
802 b: &crate::matrix::Matrix<T>,
803 ) -> Result<crate::matrix::Matrix<T>, crate::backend::BackendLinalgError>
804 where
805 T: crate::backend::BackendLinalgScalar + Copy,
806 NativeTensor: From<tenferro::TypedTensor<T>>,
807 {
808 crate::solve_matrix_in(self.engine, a, b)
809 }
810
811 /// Solve a triangular system for column-major matrices on this session.
812 ///
813 /// Same kernel, conversion and validation as
814 /// [`triangular_solve_matrix`](crate::triangular_solve_matrix), entered through this
815 /// session instead of the process-global one. `left_side`, `lower`, `transpose_a`
816 /// and `unit_diagonal` have the same meaning as on that entry.
817 ///
818 /// # Errors
819 ///
820 /// Returns [`BackendLinalgError`](crate::BackendLinalgError) under the same
821 /// conditions as [`triangular_solve_matrix`](crate::triangular_solve_matrix).
822 ///
823 /// # Examples
824 ///
825 /// ```
826 /// use tensor4all_tensorbackend::{CpuExecutionContext, Matrix};
827 /// use tensor4all_tensorbackend::CpuBackend;
828 ///
829 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
830 /// let a = Matrix::from_col_major_vec(2, 2, vec![2.0_f64, 1.0, 0.0, 4.0]);
831 /// let b = Matrix::from_col_major_vec(2, 1, vec![4.0_f64, 4.0]);
832 /// let x = context.with_concrete_session(|session| {
833 /// session.triangular_solve_matrix(&a, &b, true, true, false, false)
834 /// })??;
835 /// assert_eq!(x.as_col_major_slice(), &[2.0, 0.5]);
836 /// # Ok::<(), Box<dyn std::error::Error>>(())
837 /// ```
838 #[cfg(feature = "global-defaults")]
839 #[allow(clippy::too_many_arguments)]
840 pub fn triangular_solve_matrix<T>(
841 &mut self,
842 a: &crate::matrix::Matrix<T>,
843 b: &crate::matrix::Matrix<T>,
844 left_side: bool,
845 lower: bool,
846 transpose_a: bool,
847 unit_diagonal: bool,
848 ) -> Result<crate::matrix::Matrix<T>, crate::backend::BackendLinalgError>
849 where
850 T: crate::backend::BackendLinalgScalar + Copy,
851 NativeTensor: From<tenferro::TypedTensor<T>>,
852 {
853 crate::triangular_solve_matrix_in(
854 self.engine,
855 a,
856 b,
857 left_side,
858 lower,
859 transpose_a,
860 unit_diagonal,
861 )
862 }
863
864 /// Complete-pivoting LU decomposition of a column-major matrix on this session.
865 ///
866 /// Same kernel and conversion as
867 /// [`full_piv_lu_matrix`](crate::full_piv_lu_matrix), entered through this session
868 /// instead of the process-global one.
869 ///
870 /// # Errors
871 ///
872 /// Returns [`BackendLinalgError`](crate::BackendLinalgError) under the same
873 /// conditions as [`full_piv_lu_matrix`](crate::full_piv_lu_matrix).
874 ///
875 /// # Examples
876 ///
877 /// ```
878 /// use tensor4all_tensorbackend::{CpuExecutionContext, Matrix};
879 /// use tensor4all_tensorbackend::CpuBackend;
880 ///
881 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
882 /// let a = Matrix::from_col_major_vec(2, 2, vec![0.0_f64, 1.0, 2.0, 3.0]);
883 /// let factors = context.with_concrete_session(|session| session.full_piv_lu_matrix(&a))??;
884 /// assert_eq!(factors.p.nrows(), 2);
885 /// assert_eq!(factors.q.ncols(), 2);
886 /// # Ok::<(), Box<dyn std::error::Error>>(())
887 /// ```
888 #[cfg(feature = "global-defaults")]
889 pub fn full_piv_lu_matrix<T>(
890 &mut self,
891 a: &crate::matrix::Matrix<T>,
892 ) -> Result<crate::backend::FullPivLuMatrixResult<T>, crate::backend::BackendLinalgError>
893 where
894 T: crate::backend::BackendLinalgScalar + Copy,
895 {
896 crate::full_piv_lu_matrix_in(self.engine, a)
897 }
898
899 /// Thin/economy QR of a typed tensor on this session.
900 ///
901 /// Same kernel and conversion as [`qr_backend`](crate::qr_backend), entered through
902 /// this session instead of the process-global one.
903 ///
904 /// # Errors
905 ///
906 /// Returns [`BackendLinalgError`](crate::BackendLinalgError) under the same
907 /// conditions as [`qr_backend`](crate::qr_backend).
908 ///
909 /// # Examples
910 ///
911 /// ```
912 /// use tensor4all_tensorbackend::CpuExecutionContext;
913 /// use tenferro::TypedTensor;
914 /// use tensor4all_tensorbackend::CpuBackend;
915 ///
916 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
917 /// let a = TypedTensor::<f64>::from_vec_col_major(vec![2, 2], vec![1.0, 2.0, 3.0, 4.0])?;
918 /// let (q, r) = context.with_concrete_session(|session| session.qr_backend(a))??;
919 /// assert_eq!(q.shape(), &[2, 2]);
920 /// assert_eq!(r.shape(), &[2, 2]);
921 /// # Ok::<(), Box<dyn std::error::Error>>(())
922 /// ```
923 #[cfg(feature = "global-defaults")]
924 pub fn qr_backend<T>(
925 &mut self,
926 a: tenferro::TypedTensor<T>,
927 ) -> Result<
928 (tenferro::TypedTensor<T>, tenferro::TypedTensor<T>),
929 crate::backend::BackendLinalgError,
930 >
931 where
932 T: crate::backend::BackendLinalgScalar,
933 {
934 use tenferro::TensorScalar;
935
936 let (shape, data) = a.into_vec_col_major().map_err(|error| {
937 crate::backend::BackendLinalgError::from(anyhow::anyhow!(
938 "QR input host access failed: {error}"
939 ))
940 })?;
941 let tensor: NativeTensor =
942 <T as TensorScalar>::into_tensor(shape, data).map_err(|error| {
943 crate::backend::BackendLinalgError::from(anyhow::anyhow!(
944 "QR input tensor construction failed: {error}"
945 ))
946 })?;
947 crate::qr_backend_in(self.engine, tensor)
948 }
949
950 /// Thin/economy SVD of a typed tensor on this session.
951 ///
952 /// Same kernel and conversion as [`svd_backend`](crate::svd_backend), entered through
953 /// this session instead of the process-global one.
954 ///
955 /// # Errors
956 ///
957 /// Returns [`BackendLinalgError`](crate::BackendLinalgError) under the same
958 /// conditions as [`svd_backend`](crate::svd_backend).
959 ///
960 /// # Examples
961 ///
962 /// ```
963 /// use tensor4all_tensorbackend::CpuExecutionContext;
964 /// use tenferro::TypedTensor;
965 /// use tensor4all_tensorbackend::CpuBackend;
966 ///
967 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
968 /// let a = TypedTensor::<f64>::from_vec_col_major(vec![2, 2], vec![3.0, 0.0, 0.0, 2.0])?;
969 /// let factors = context.with_concrete_session(|session| session.svd_backend(&a))??;
970 /// assert_eq!(factors.s().as_slice()?, &[3.0, 2.0]);
971 /// # Ok::<(), Box<dyn std::error::Error>>(())
972 /// ```
973 #[cfg(feature = "global-defaults")]
974 pub fn svd_backend<T>(
975 &mut self,
976 a: &tenferro::TypedTensor<T>,
977 ) -> Result<crate::backend::SvdResult<T>, crate::backend::BackendLinalgError>
978 where
979 T: crate::backend::BackendLinalgScalar,
980 {
981 use tenferro::TensorScalar;
982
983 let tensor: NativeTensor = <T as TensorScalar>::into_tensor(
984 a.shape().to_vec(),
985 a.host_data()
986 .map_err(|error| {
987 crate::backend::BackendLinalgError::from(anyhow::anyhow!(
988 "SVD input host access failed: {error}"
989 ))
990 })?
991 .to_vec(),
992 )
993 .map_err(|error| {
994 crate::backend::BackendLinalgError::from(anyhow::anyhow!(
995 "SVD input tensor construction failed: {error}"
996 ))
997 })?;
998 crate::svd_backend_in(self.engine, tensor)
999 }
1000
1001 /// Matrix multiplication `A * B` on this session.
1002 ///
1003 /// The same kernel and shape validation as
1004 /// [`mat_mul`](crate::mat_mul), entered through this session instead of the
1005 /// process-global one, and returning the shared
1006 /// [`Matrix`](crate::Matrix) container.
1007 ///
1008 /// # Errors
1009 ///
1010 /// Returns [`MatrixMulError`](crate::MatrixMulError) when the shapes disagree or
1011 /// the session operation fails.
1012 ///
1013 /// # Examples
1014 ///
1015 /// ```
1016 /// use tensor4all_tensorbackend::{CpuExecutionContext, Matrix};
1017 /// use tensor4all_tensorbackend::CpuBackend;
1018 ///
1019 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1020 /// let a = Matrix::from_col_major_vec(2, 2, vec![1.0_f64, 2.0, 3.0, 4.0]);
1021 /// let b = Matrix::from_col_major_vec(2, 2, vec![1.0_f64, 0.0, 0.0, 1.0]);
1022 /// let c = context.with_concrete_session(|session| session.mat_mul(&a, &b))??;
1023 /// assert_eq!(c.as_col_major_slice(), &[1.0, 2.0, 3.0, 4.0]);
1024 /// # Ok::<(), Box<dyn std::error::Error>>(())
1025 /// ```
1026 #[cfg(feature = "global-defaults")]
1027 pub fn mat_mul<T: crate::matrix::BlasMul>(
1028 &mut self,
1029 a: &crate::matrix::Matrix<T>,
1030 b: &crate::matrix::Matrix<T>,
1031 ) -> Result<crate::matrix::Matrix<T>, crate::matrix::MatrixMulError> {
1032 crate::mat_mul_in(self.engine, a, b)
1033 }
1034
1035 /// Thin/economy QR decomposition.
1036 ///
1037 /// # Errors
1038 ///
1039 /// Returns [`tenferro_tensor::Error::Validation`] for an unsupported dtype or
1040 /// rank, and [`tenferro_tensor::Error::BackendSource`] when the factorization
1041 /// itself fails.
1042 ///
1043 /// # Examples
1044 ///
1045 /// ```
1046 /// use tensor4all_tensorbackend::CpuExecutionContext;
1047 /// use tenferro::Tensor;
1048 /// use tensor4all_tensorbackend::CpuBackend;
1049 ///
1050 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1051 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
1052 /// let (q, r) = context.with_concrete_session(|session| session.qr(&a))??;
1053 /// // `A = Q R` is reproduced to rounding, which is the correctness property
1054 /// // of a factorization.
1055 /// let reconstructed =
1056 /// context.with_concrete_session(|session| session.contraction(&q, &[1], &r, &[0]))??;
1057 /// for (got, want) in reconstructed.as_slice::<f64>()?.iter().zip([1.0, 2.0, 3.0, 4.0]) {
1058 /// assert!((got - want).abs() < 1e-12, "got {got}, want {want}");
1059 /// }
1060 /// # Ok::<(), Box<dyn std::error::Error>>(())
1061 /// ```
1062 pub fn qr(
1063 &mut self,
1064 tensor: &NativeTensor,
1065 ) -> tenferro_tensor::Result<(NativeTensor, NativeTensor)> {
1066 tensor.qr(self.engine)
1067 }
1068
1069 /// Thin/economy SVD.
1070 ///
1071 /// # Errors
1072 ///
1073 /// Returns [`tenferro_tensor::Error::Validation`] for an unsupported dtype or
1074 /// rank, and [`tenferro_tensor::Error::BackendSource`] when the factorization
1075 /// itself fails.
1076 ///
1077 /// # Examples
1078 ///
1079 /// ```
1080 /// use tensor4all_tensorbackend::CpuExecutionContext;
1081 /// use tenferro::Tensor;
1082 /// use tensor4all_tensorbackend::CpuBackend;
1083 ///
1084 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1085 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![3.0_f64, 0.0, 0.0, 2.0])?;
1086 /// let (u, s, vt) = context.with_concrete_session(|session| session.svd(&a))??;
1087 /// // The singular values of diag(3, 2) are 3 and 2.
1088 /// assert_eq!(s.as_slice::<f64>()?, &[3.0, 2.0]);
1089 /// assert_eq!(u.shape(), &[2, 2]);
1090 /// assert_eq!(vt.shape(), &[2, 2]);
1091 /// # Ok::<(), Box<dyn std::error::Error>>(())
1092 /// ```
1093 pub fn svd(
1094 &mut self,
1095 tensor: &NativeTensor,
1096 ) -> tenferro_tensor::Result<(NativeTensor, NativeTensor, NativeTensor)> {
1097 tensor.svd(self.engine)
1098 }
1099
1100 /// Solve `A X = B` for a square `A`.
1101 ///
1102 /// # Errors
1103 ///
1104 /// Returns [`tenferro_tensor::Error::Validation`] when `lhs` is not square, when
1105 /// the operands have mismatched shapes, or for an unsupported dtype, and
1106 /// [`tenferro_tensor::Error::BackendSource`] when the solve itself fails.
1107 ///
1108 /// # Examples
1109 ///
1110 /// ```
1111 /// use tensor4all_tensorbackend::CpuExecutionContext;
1112 /// use tenferro::Tensor;
1113 /// use tensor4all_tensorbackend::CpuBackend;
1114 ///
1115 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1116 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0])?;
1117 /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![6.0_f64, 8.0])?;
1118 /// let x = context.with_concrete_session(|session| session.solve(&a, &b))??;
1119 /// assert_eq!(x.as_slice::<f64>()?, &[3.0, 2.0]);
1120 /// # Ok::<(), Box<dyn std::error::Error>>(())
1121 /// ```
1122 pub fn solve(
1123 &mut self,
1124 lhs: &NativeTensor,
1125 rhs: &NativeTensor,
1126 ) -> tenferro_tensor::Result<NativeTensor> {
1127 lhs.solve(rhs, self.engine)
1128 }
1129
1130 /// Solve `A X = B` where `A` is triangular.
1131 ///
1132 /// - `left_side`: solve `A X = B` when true, `X A = B` when false;
1133 /// - `lower`: `A` is lower triangular;
1134 /// - `transpose_a`: solve with `A` transposed;
1135 /// - `unit_diagonal`: treat the diagonal of `A` as one, ignoring the stored
1136 /// values.
1137 ///
1138 /// # Errors
1139 ///
1140 /// Returns [`tenferro_tensor::Error::Validation`] for a shape mismatch or an
1141 /// unsupported dtype, and [`tenferro_tensor::Error::BackendSource`] when the solve
1142 /// itself fails.
1143 ///
1144 /// # Examples
1145 ///
1146 /// ```
1147 /// use tensor4all_tensorbackend::CpuExecutionContext;
1148 /// use tenferro::Tensor;
1149 /// use tensor4all_tensorbackend::CpuBackend;
1150 ///
1151 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1152 /// // Lower triangular, column-major.
1153 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 1.0, 0.0, 4.0])?;
1154 /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 4.0])?;
1155 /// let x = context.with_concrete_session(|session| {
1156 /// session.triangular_solve(&a, &b, true, true, false, false)
1157 /// })??;
1158 /// assert_eq!(x.as_slice::<f64>()?, &[2.0, 0.5]);
1159 /// # Ok::<(), Box<dyn std::error::Error>>(())
1160 /// ```
1161 pub fn triangular_solve(
1162 &mut self,
1163 lhs: &NativeTensor,
1164 rhs: &NativeTensor,
1165 left_side: bool,
1166 lower: bool,
1167 transpose_a: bool,
1168 unit_diagonal: bool,
1169 ) -> tenferro_tensor::Result<NativeTensor> {
1170 lhs.triangular_solve(
1171 rhs,
1172 left_side,
1173 lower,
1174 transpose_a,
1175 unit_diagonal,
1176 self.engine,
1177 )
1178 }
1179
1180 /// Full-pivoting LU decomposition `P A Q = L U`, returning
1181 /// `(P, L, U, Q, parity)`.
1182 ///
1183 /// # Errors
1184 ///
1185 /// Returns [`tenferro_tensor::Error::Validation`] for a non-square `A` or an
1186 /// unsupported dtype, and [`tenferro_tensor::Error::BackendSource`] when the
1187 /// factorization itself fails.
1188 ///
1189 /// # Examples
1190 ///
1191 /// ```
1192 /// use tensor4all_tensorbackend::CpuExecutionContext;
1193 /// use tenferro::Tensor;
1194 /// use tensor4all_tensorbackend::CpuBackend;
1195 ///
1196 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1197 /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0])?;
1198 /// let (p, l, u, q, _parity) = context
1199 /// .with_concrete_session(|session| session.full_piv_lu(&a))?
1200 /// .expect("full-pivoting LU");
1201 /// for factor in [&p, &l, &u, &q] {
1202 /// assert_eq!(factor.shape(), &[2, 2]);
1203 /// }
1204 /// // Column-major, so index 0 is (0, 0), index 1 is (1, 0) and index 2 is (0, 1).
1205 /// let l = l.as_slice::<f64>()?;
1206 /// let u = u.as_slice::<f64>()?;
1207 /// assert_eq!(l[2], 0.0, "L is lower triangular");
1208 /// assert_eq!(u[1], 0.0, "U is upper triangular");
1209 /// # Ok::<(), Box<dyn std::error::Error>>(())
1210 /// ```
1211 pub fn full_piv_lu(
1212 &mut self,
1213 tensor: &NativeTensor,
1214 ) -> tenferro_tensor::Result<(
1215 NativeTensor,
1216 NativeTensor,
1217 NativeTensor,
1218 NativeTensor,
1219 NativeTensor,
1220 )> {
1221 tensor.full_piv_lu(self.engine)
1222 }
1223}
1224
1225impl CpuExecutionContext {
1226 /// Run `f` inside one explicit concrete session of this context.
1227 ///
1228 /// The context's engine is borrowed once for the whole closure, so a stage
1229 /// borrows it once and reuses its prepared plans and buffer pool. All routes on
1230 /// the [`Session`] run on that engine only: nothing here consults the
1231 /// process-global default context, and nothing falls back to it.
1232 ///
1233 /// # Examples
1234 ///
1235 /// ```
1236 /// use tensor4all_tensorbackend::CpuExecutionContext;
1237 /// use tenferro::Tensor;
1238 /// use tensor4all_tensorbackend::CpuBackend;
1239 ///
1240 /// let context = CpuExecutionContext::from_backend(CpuBackend::with_threads(1)?);
1241 /// let x = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 3.0])?;
1242 /// let doubled = context.with_concrete_session(|session| session.reshape(&x, &[2]))??;
1243 /// assert_eq!(doubled.as_slice::<f64>()?, &[1.0, 3.0]);
1244 /// # Ok::<(), Box<dyn std::error::Error>>(())
1245 /// ```
1246 ///
1247 /// # Errors
1248 ///
1249 /// Returns [`CpuExecutionContextError::Reentered`] when the calling thread is
1250 /// already inside a session of this context. A session entered from a Rayon
1251 /// worker runs inline on a serial engine that shares the context engine's
1252 /// prepared-plan cache, so a worker entry completes instead of waiting for a
1253 /// pool it belongs to (tensor4all-rs#830).
1254 ///
1255 /// # Panics
1256 ///
1257 /// Never. A panic in `f` propagates after the engine borrow is released.
1258 pub fn with_concrete_session<R>(
1259 &self,
1260 f: impl FnOnce(&mut Session<'_>) -> R,
1261 ) -> Result<R, CpuExecutionContextError> {
1262 self.with_session(|engine| f(&mut Session::new(engine)))
1263 }
1264}
1265
1266#[cfg(test)]
1267mod tests;