|
Uni20
Asynchronous tensor and runtime library.
|
For the wider operation taxonomy and current Async support, see Tensor Operations, Semantics, and Async Support.
Shape construction initializes owned numerical elements to zero on both host and CUDA storage. Other host element types are value-initialized; CUDA zero initialization requires a supported numerical representation. A custom accessor still interprets stored values according to its own semantics.
This is an API guarantee. Partially populating an ordinary numerical tensor is valid: every untouched stored element remains zero.
| Construction | Initial stored numerical values |
|---|---|
DenseMatrix<T>(rows, cols) for fixed-precision numeric T | Zero, including native complex types. |
DenseMatrix<T>(rows, cols, p) for mpreal or complex<mpreal> | Actual zeros at precision p. |
DenseMatrix<T>(rows, cols, p) for fixed-precision numeric T | Zero; p must equal native_precision<T>(). No precision metadata is stored. |
DenseMatrix<T>(uninitialized, rows, cols) (with trailing p when required) | Unspecified native values or unset runtime-precision scalars; write before reading. |
For host tensors, precision_of(tensor) and at_precision(tensor, p) work with both fixed and runtime scalar types. Fixed types validate their native precision without storing extra metadata. See the generic precision contract.
The uni20::uninitialized tag is the first constructor argument. It is also available with explicit extents, mapping builders, strides, storage contexts, and placement. Each element must receive a value before that value is consumed. The tag does not bypass required construction or destruction of nontrivial objects. It does not change the Tensor type or add runtime initialization state.
The tensor layer passes StorageInitialization::Zero or Uninitialized to its storage factory. Low-level HostBuffer(size) and CudaBuffer(size) allocation does not promise numerical zeros; do not infer Tensor construction behavior from those raw-buffer APIs. Initialization tests live in test_tensor_initialization.cpp and test_tensor_precision.cpp.
Positive-rank default construction remains empty and allocation-free. Default rank-zero construction allocates its one element and initializes it to zero; ScalarTensor<double>(uni20::uninitialized) requests an overwrite destination.
reset_shape(extents) and replace(extents, placement) discard the old values and zero-initialize replacement storage. Their tag-first overloads request uninitialized replacement storage. prepare_output chooses these overloads when an overwrite operation needs new storage; it retains existing storage and values when shape and placement already match. Generic output types without an uninitialized overload still use their ordinary construction/reset capability. Copy construction, view materialization, and storage adoption preserve source values; materialization and computed outputs avoid a preliminary zero-fill.
BlockTensor construction follows the same policy for its stored legal blocks. The tag changes payload initialization without changing symmetry metadata, stored keys, or placement. Packed alignment padding remains defined zero in both modes. See block tensor storage.
CUDA initialization is ordered through the existing buffer completion ledger. A subsequent consumer on another stream waits for initialization through normal access acquisition; construction does not introduce a device-wide barrier.
Explicitly uninitialized storage may contain signalling NaNs for diagnostics. UNI20_FILL_UNINITIALIZED_SNAN=AUTO enables this in Debug and DebugOpt; ON and OFF override it independently of NDEBUG and sanitizer selection. NaN filling does not provide a usable initial numerical value or guarantee a floating-point trap. See initialization diagnostics.
Uni20 represents uniform and identity-like values as compact readable tensors:
The C++ full factory puts the fill value first so its scalar type can be inferred; Python bindings may provide Python-specific argument ordering independently.
These objects model ImmediateTensorView, but their accessors return generated values rather than references into a dense element allocation. Consequently they do not model MutableImmediateTensorView, StridedImmediateTensorView, or reusable dense OwningTensor. Their GeneratedLayout maps logical indices to synthetic accessor offsets; it does not claim a row-major or column-major physical order.
Algorithms that need writable storage, such as destructive LAPACK routines, materialize a work tensor through the normal copy and backend-dispatch path:
Tensor(view) and make_tensor(view) both eagerly materialize a tensor view. Without an explicit policy, CTAD and make_tensor preserve a canonical layout_left or layout_right physical source. A generated or otherwise noncanonical source uses the ordinary Tensor default, ColumnMajor. make_tensor<Layout>(view) is the explicit spelling when the output layout must differ from the inferred layout; layout changes the concrete return type. Materialization traverses canonical output storage in its native order, so a generated source does not impose strided writes on the destination.
GeneratedStorage is backend-neutral. When generated and concretely stored operands participate in one operation, the concrete storage policy selects the default backend list. Each candidate kernel still decides whether it supports the generated accessor. With no concrete operand, generated storage provides the host reference backend as its default.
For a rank-R result with extents (n0, ..., n{R-1}), eye is defined by
The extents need not be equal. Rank-zero eye<T>() is scalar one; rank-one eye<T>(n) is an all-ones vector; rank-two eye<T>(m, n) is the ordinary rectangular identity matrix. This is also the tensor-network copy tensor, so a separate copy_tensor factory is unnecessary.
Uni20 separates aliasing, object mutation, and owning value construction:
One requested extent may be -1; Uni20 infers it from the source element count. All forms reject mismatched element counts and ambiguous inference.
reshape_view never allocates or copies elements. It preserves the source data handle and accessor. Automatic order selection is available only when the source mdspan's static layout type is layout_left or layout_right; the result preserves that same layout type.
For a general strided mapping, select the interpretation explicitly:
Both explicit forms require a unique, exhaustive, canonical contiguous mapping in the selected order. Singleton-axis strides are ignored because they do not affect addressing. A contiguous rank-one source is compatible with either choice, and the selected result layout resolves the otherwise unavoidable ambiguity. Non-contiguous mappings are rejected by every no-copy form.
A tensor-level view accepts lvalues only because its descriptor does not extend the lifetime of addressable source storage. Mutability follows the source accessor: reshaping a mutable tensor gives a mutable alias, while a const source remains read-only. Generated tensors are layout-neutral and do not have a structural reshape_view; use owning reshape when a value-level reshape is required.
Repeated tensor-level reshapes preserve order through the result's static layout_left or layout_right type. No runtime provenance flag is required, including when an intermediate shape contains singleton extents.
For asynchronous tensors, async::reshape_view(parent, ...) returns an owner-retaining Async<View> whose View models TensorView, on the parent's exact epoch queue. The alias may be created while the parent value is still pending. Once the parent epoch becomes readable, it validates the mapping and rebuilds the parent's mdspan or deferred mdspec with the reshaped mapping. The result models ImmediateTensorView exactly when the parent does; reshaping a deferred descriptor does not acquire its data handle. A parent failure propagates through the alias. Mutable aliases support write-through assignment; aliases of a const parent are read-only. The async API also provides reshape_view_left and reshape_view_right for strided parents whose order must be selected explicitly.
The owning Async forms use independent result epochs. reshape(async_input,
...) preserves the stored value, while reshape(std::move(async_input), ...) consumes it and may transfer a compatible allocation. reshape_inplace enrolls one writer and publishes the mapping change on the original timeline.
reshape_inplace replaces a canonically laid-out owning tensor mapping without moving or reallocating its storage. The overload is capability-constrained rather than tied to a concrete tensor spelling. Because mdspan rank is part of the C++ type, the new shape must have the same rank. Existing standalone mdspan descriptors retain their old mappings, as normal for copied view descriptors.
Plain reshape always returns an owning tensor. Its canonical, storage- transfer-capable overload takes the source by value:
As with consuming eigensolver overloads, std::move grants permission to reuse storage but is not a separate operation name. There is therefore no reshape_move or reshape_copy API.
Immediately accessible canonical non-owning sources are reshaped before materialization so their contiguous sequence is preserved. Deferred non-owning views are first materialized through their selected backend, then reshaped by transferring the resulting owning allocation. Layout-neutral generated inputs similarly materialize in the default or explicitly requested layout:
Plain reshape does not guess an order for a layout_stride source. Use reshape_view_left or reshape_view_right, then materialize the selected view when an owning result is required. Python bindings can build NumPy-compatible C, F, A, and copy policies from these explicit primitives without adding runtime layout ambiguity to the C++ return types.