|
Uni20
Asynchronous tensor and runtime library.
|
This document defines the reproducible environment, coding rules, and documentation standards for Uni20. It serves both human developers and automated agents (e.g., Codex, Copilot Workspace, ChatGPT Code Interpreter). Agents must execute the “Mandatory Environment Setup” section before running CMake.
Notes for hosted environments:
CMakeLists.txt to bypass dependency detection.libtbb-dev 2021.11 package is below the supported minimum.libfmt-dev 9.1 package is below the supported minimum.Optional packages
These packages are automatically fetched from GitHub if they are not detected.
Install libtbb-dev only when the distribution provides oneTBB 2022.3 or newer, and install libfmt-dev only when it provides fmt 11.1 or newer. In network-restricted environments, provide compatible package directories or pre-populate the CMake FetchContent sources instead.
docs/ or README.md whenever APIs change.docs/development/agent_assisted_development.md and docs/development/code_review.md for the current workflow and review guidance.int const& x style — const follows the type.this->foo() to clarify scope._) on private member variables, except for simple aggregates.= ... or = {}). Treat fields without explicit defaults as required. In C++ aggregate list-initialization, omitted non-reference members are already initialized from {} when no default member initializer exists, so -Wmissing-field-initializers is a style/API signal rather than an uninitialized-memory warning: a warning should mean either a required field was omitted or the aggregate should spell out its default.constexpr, consteval, and concepts from C++23 wherever they simplify code or improve correctness.mdspan implementation, in namespace stdex::.[] for multi-dimensional indexing of tensors. Do not define MDSPAN_USE_PAREN_OPERATOR, use [] instead. This may mean adding brackets when code like [a,b] is used in a macro invocation, especially in TRACE and gtest macros.uni20::complex<T> in Uni20 code, tests, examples, and docs. Native real types retain std::complex<T> identity; complex<mpreal> selects the MPC-backed owning scalar when enabled. Deduce generic complex value types directly through Complex C, since the selecting alias is not a deduction surface. Direct std::complex<T> spellings should be limited to the native alias definition, explicit alias tests/docs, or narrow external interop boundaries. Internal standard-family adapters may use detail::standard_complex<T> where real-type deduction is required.uni20::numeric_limits<T> in scalar-generic Uni20 algorithms. It delegates to std::numeric_limits<T> for ordinary arithmetic types and is the project customization point for extension or library scalar types with missing/incomplete standard-library limits. Precision-dependent queries are functions: use digits(value)/epsilon(value) or pass precision_of(value). Fixed types also provide constexpr no-argument queries such as digits(). Explicit operation precision must match fixed result types; see docs/tensor/scalar_policy.md#generic-precision.uni20::math from core/math.hpp for scalar-generic host math where the operation is provided, and uni20::isfinite for finite-value classification. Keep scalar-specific ADL overloads in their scalar layer; do not duplicate subsystem-private adl_* wrappers. See docs/tensor/scalar_math_design.md.Rule: Lambdas that define async coroutines must not have capture lists and must be defined static.
Why: Async coroutine lambdas are lambdas that return anything derived from BasicAsyncTask (so AsyncTask, CudaTask, etc). This is a wrapper for a coroutine_handle, and that coroutine handle has a lifetime that is distinct from the lambda itself. Captured values are stored inside the lambda closure object, and hence go out of scope as soon as the lambda variable is destroyed. They are not copied onto the coroutine stack frame. If the lifetime if the coroutine is longer than the lifetime of the lambda, then any use of the captured variable inside the coroutine will be referring to something that no longer exists (a dangling reference). Passing values explicitly puts them into the coroutine frame ensures safety and reproducible async semantics. This is ensured by making the lambda operator()functionstatic, using the C++23static` lambda modifier.
Agents must enforce: no coroutine lambda that is passed into the scheduler may have a capture list. To ensure this, always use the
staticmodifier on the lambda function. Use the codebase formatting conventionstatic -> AsyncTaskfor coroutine lambdas.
The same considerations apply to function objects that return coroutine handles, where it is clear that the function object cannot contain state that might have a shorter lifetime than the coroutine itself.
Async<T> is the canonical async value wrapper.DebugScheduler, TbbScheduler, etc.) manage task lifetimes.std::thread or manual synchronization primitives.Why: Schedulers coordinate task causality and epoch ordering. Direct threading bypasses dependency tracking, leading to data races or missed wakeups.
EpochQueue, ReadBuffer, or WriteBuffer.Why: These primitives enforce causal consistency: all reads and writes occur in dependency order, ensuring determinism across async tasks.
LocalSpace, BlockSpace, QNum, or leg-orientation metadata when moving between MPS, MPO, environment, SVD, TensorContraction, CUDA, or MPI layers.to_dense_reference, materialize_dense_debug, or similar.ThreeLegBlockMatrix uses q_column = q_row + q_local, local operator coefficients use q_bra = q_ket + q_operator, and sparse MPO entries use q_left_virtual + q_ket = q_right_virtual + q_bra.Why: Symmetry sectors define which tensor blocks exist and which contractions are legal. Losing that metadata changes the mathematical problem, hides invalid states, and prevents the CUDA/MPI block distribution strategy needed for large DMRG calculations.
data_handle_type alone. A pointer-shaped data handle only identifies a storage handle; the mdspan accessor defines the value semantics of access(...).element_type, even when access(...) returns a calculated value rather than a reference. Do not encode read-only behavior only in the handle type or reference alias. MutableMdspanLike uses const element_type together with indexed assignment validity to reject ordinary mutation. An opaque device-memory accessor without assignable element semantics does not model MutableMdspanLike or MutableMdspecLike; resolve it to an accessor with the required reference semantics before mutation.element_type. Mutable access belongs on the non-const mdspan() overload; shallow-const descriptors must not make TensorView const& writable.stdex::default_accessor<T> or when the wrapper explicitly understands and lowers the accessor semantics, for example Uni20's conjugating accessor into a BLAS transpose/conjugation flag.UNI20_HOST_DEVICE. Stored function objects invoked through CUDA-accessible accessors must annotate the invoked call operator the same way. Do not add a separate function- or mapping-domain trait: an actual CUDA compilation probe is the check for device-callability.enable_accessor_in_domain states where an acquired handle may be evaluated; UNI20_HOST_DEVICE only makes the execution code available to both compilers. A device-callable accessor still needs an explicit precompiled backend lowering or a sufficiently general execution plan.std::linalg::conjugated_accessor model from WG21 P3050R3, but the user-facing helper is uni20::conj(span). Do not use std::conj to decide real-mdspan behavior: uni20::conj is the project customization point that preserves real scalar type semantics.Why: Mdspan layouts and accessors are not just tags. A view can preserve the original pointer while changing every value observed through access(...); bypassing that accessor silently changes the mathematical operation.
DenseMatrix<T>(rows, cols) therefore permits diagonal-only or other partial population; untouched elements remain zero. This includes native real and complex scalars. Runtime-precision constructors such as DenseMatrix<complex<mpreal>>(rows, cols, p) create actual zeros at p.DenseMatrix<T>(uninitialized, rows, cols) when values will be supplied later. Every element must receive a value before it is read. The tag preserves required object lifetimes; runtime-precision scalars use unset placeholders on this path.HostBuffer(size) and CudaBuffer(size) APIs have allocation-only contracts; they do not determine the initialization policy of an owning Tensor.tests/tensor/test_tensor_initialization.cpp and tests/tensor/test_tensor_precision.cpp.tests/<module>/.add_test_module(...) in the relevant CMakeLists.txt.REQUIRE_SEED.tests/numerics/ and follow docs/development/numerical_testing.md. Preserve tested precision in inputs, tolerances, and error measurements. Record unsupported, unavailable, and inapplicable operation/scalar/backend combinations in the coverage report; register executable tests only for configured, supported combinations. Missing expected support must fail compilation or testing, not remove a test. Reserve runtime skips for runtime conditions such as unavailable hardware topology. Do not treat a fallback as provider validation.When profiling TensorContraction CUDA benchmarks with Nsight Systems, always disable CUDA event tracing explicitly:
Why: Nsight Systems defaults --cuda-event-trace to auto. On CUDA 12.8+ drivers this may behave like enabled for TensorContraction workloads. These workloads create many cudaEventRecord and cudaStreamWaitEvent calls, so CUDA event tracing can add massive profiler-induced overhead and false dependencies. In local profiling on a single-GPU L=20, m=512 DMRG run, event tracing caused a short two-sweep profile to time out after only 58 benchmark rows, while the same run with --cuda-event-trace=false completed all 77 rows.
Use --cuda-event-trace=true only for a targeted CUDA event investigation, and then run a much smaller benchmark with an explicit timeout. Do not use Nsight's implicit auto setting for TensorContraction profiling.
bindings/python/.docs/ when adding or modifying bindings.docs/.README.md, link to directory/, not directory/README.md, so GitHub shows the directory listing before the rendered index. Link directly to README.md only when a fragment identifies a specific section.docs/tensor/scalar_policy.md.docs/krylov/algorithms.md. If scalar support changes, also update docs/krylov/precision_validation.md.docs/linalg/dense_blas_lapack_coverage.md. Keep provider-wide coverage inventories out of the Krylov algorithm contract.Purpose: keep generated API documentation useful without forcing noisy, mechanical comments into ordinary implementation code.
/// is the preferred Doxygen form for ordinary function, class, alias, concept, and member documentation. Treat contiguous /// lines immediately preceding a declaration as that declaration's documentation block./** ... */ may appear for:\file, \ingroup, \defgroup)./* ... */ and // are non-Doxygen implementation comments. They are free for agents to clean, rewrite, or insert to clarify logic, lifetime, or invariants. These comments do not appear in generated documentation.\brief.\brief unless they improve readability for a multi-paragraph block.\param, \tparam, and \return when they clarify behavior, requirements, ownership, lifetime, or non-obvious semantics. Do not add tautological tags solely to satisfy a checklist.\return for callable entities (functions, lambdas, or overloaded operators) whose declaration includes parentheses and is not a constructor or destructor.\return for:\brief, \details, \pre, \post, \throws, \note, \warning, \tparam, \param, \return, \ingroup.When cleaning or generating documentation:
[TODO] Document this function./// for new ordinary declaration docs./** ... */ may remain block-style.\brief, \details, \note, \warning, etc.) wraps, indent the following lines so that the first character of text aligns under the first character of the text on the previous line.Example:
// or /* ... */ comments to improve clarity or correctness.\ingroup only where it materially improves generated navigation.\ingroup on members, nested types, or declarations already covered by the surrounding file or type documentation.When generating or repairing top-of-file documentation, use a block comment:
Uni20 does not currently define a stable "public API" boundary.
Treat headers under src/uni20/ as potentially developer-facing, but do not document every internal helper mechanically.
\internal ... \endinternal only when generated documentation must hide or distinguish a non-public declaration.uni20::internal, detail, or directories named internal or detail already signal internal scope; do not add redundant internal grouping tags unless they help the generated docs.