This document freezes the ABI contract used by generated functions:
tensorium_inittensorium_rhstensorium_entrytensorium_init_pointtensorium_init_grid_scftensorium_init_grid_affinetensorium_rhs_grid_scftensorium_rhs_grid_affinetensorium_rhs_grid_paralleltensorium_residual_grid_scftensorium_residual_grid_affinetensorium_residual_grid_paralleltensorium_spectral_residual_<target>tensorium_spectral_residual_jvp_<target>tensorium_spectral_residual_grid_<target>
Source of truth for ABI constants:
include/tensorium_mlir/Target/MLIRGen/GeneratedKernelABI.h.
Source of truth for the host-callable kernel descriptor:
include/tensorium_mlir/Target/MLIRGen/MLIRGenHostABI.h.
Source of truth for the runtime spectral residual callback ABI:
include/tensorium_mlir/Runtime/SpectralResidualKernel.h.
This remains the public umbrella include; the runtime implementation is split
under SpectralResidualTypes.h, SpectralResidualAssembly.h,
SpectralResidualJVP.h, and SpectralEllipticSolver.h.
Architectural context: docs/language_mlir_abi_architecture.md describes how
this ABI fits between Tensorium MLIR/LLVM kernels, generated host glue,
standalone execution, and optional AMReX integration.
- ABI version attribute:
tensorium.abi.version - Current value:
3 - Memory layout attribute:
tensorium.abi.memory_layout = "soa_component_major" - Memref ABI attribute:
tensorium.abi.memref_abi = "strided_memref_rank1_f64"
Every generated function above carries these attrs, plus a stable
tensorium.abi.kind.
Generated functions expose argument-order metadata:
tensorium.abi.param_names: runtime scalar parameter order (f64values)tensorium.abi.coord_names: coordinate buffer order (x,y,zorr,theta,phi)tensorium.abi.field_names: field buffer order (matching function args)tensorium.abi.output_names: written output field namestensorium.abi.read_arg_indices: absolute argument indices read by the kerneltensorium.abi.write_arg_indices: absolute argument indices (in function signature) written by the kerneltensorium.abi.stencil_radius: required interior ghost/radius width for RHS and residual grid kernels, derived from lowered stencil readstensorium.abi.residual_kernel: internal marker on source residual modules that asks grid lowering to expose residual aliases
After LLVM lowering, each memref<?xf64> or memref<Nxf64> argument is lowered
to the 5-value descriptor:
double *allocateddouble *alignedint64_t offsetint64_t sizeint64_t stride
The helper host-side shape is provided as:
tensorium_mlir::abi::StridedMemRef1DF64
in include/tensorium_mlir/Target/MLIRGen/GeneratedKernelABI.h.
The driver can emit a C header that mirrors the lowered ABI and adds thin buffer wrappers:
Tensorium_cc --emit-host-header tensorium_generated_host.h <file.tn>The header contains:
- raw
extern void tensorium_*prototypes with expanded memref descriptors, tensorium_memref1d_f64for callers that need descriptor-level access,- convenience wrappers such as
tensorium_call_init_grid_affine(...)andtensorium_call_rhs_grid_affine(...).
Convenience wrappers accept plain double * buffers and compute descriptor
sizes from ABI metadata and field tensor ranks. For example, a rank-2 spatial
field uses 9 * n_points in 3D, and a rank-3 spatial field uses
27 * n_points.
The internal host module descriptor also exposes simulation metadata
(dimension, coordinate system, resolution, spatial scheme/order), field
descriptors (name, variance, rank, component count), and per-kernel
read/write/stencil metadata. This descriptor is the intended source for future
C++ runtime and AMReX wrappers.
HostModuleABI now materializes the buffer-level contract that a runtime should
consume directly:
- each
HostFieldABIrecords field name, variance (up/down), rank, and component count per grid point; - each
HostKernelABIrecords raw scalar/memref arguments and abufferstable; - each
HostBufferABIrecords logical buffer name, C-safe name, absolute function argument index, role (Coordinate,Field,Output), access (Read,Write,ReadWrite,None), variance/rank, and component count; requiredBufferScalars(buffer, nPoints)returns the exact scalar allocation size required by SoA component-major layout;validateHostModuleABI(abi)checks the descriptor before a runtime trusts it.tensorium_mlir::runtime::HostFieldStoragebuilds a deduplicated storage plan from the ABI and a uniform grid shape. It allocates one contiguous scalar arena for all logical buffers, then exposes stable per-kernel binding plans and rank-1 memref descriptors into that arena.tensorium_mlir::runtime::GeneratedHostStorageprovides the same uniform-grid storage plan from generated C descriptor tables, so standalone runners can consumetensorium_host_kernels/tensorium_host_bufferswithout linking the compiler-side MLIR ABI builder.- Generated host headers also emit
tensorium_host_kernel_adapters, a uniform invocation table for grid kernels.GeneratedHostStorage::invoke(...)uses those adapters to bind runtime-owned buffers by descriptor order instead of requiring callers to spell every lowered field argument manually. constraintsDSL blocks lower through the same scalarization path as RHS kernels but additionally exposetensorium_residual_grid_affine/tensorium_residual_grid_scf/tensorium_residual_grid_parallelhost-callable symbols. These kernels compute residual buffersF(u); the solver runtime is responsible for choosing the update method that drives those residuals toward zero.tensorium_rhs_grid_parallelandtensorium_residual_grid_parallelhave the same low-level argument layout as the other field-grid kernels. Their MLIR body usesscf.parallel; LLVM emission lowers it through OpenMP runtime calls.GeneratedHostStoragealso exposes a descriptor-level Euler helper for standalone runtime experiments:eulerUpdatePairsFromDerivativePrefix()discovers writable derivative fields nameddXand maps them to state fieldX, thenapplyEulerUpdate(...)performsX += dt * dXover the runtime arena. This is intentionally minimal and should be replaced by the target runtime's integrator once AMReX owns the storage.- Spectral initial-data experiments use
tensorium_spectral_residual_pointandtensorium_spectral_residual_kernel_fnas the pointwise residual callback ABI. The runtime constructs the derivative bundle on the selected spectral grid, applies an optional coordinate map, then calls the generated callback to compute one scalarF(u)=0value per collocation point. This keeps NRPy/Kadath/TwoPunctures-style formulations above the generic spectral grid and solver machinery. - For
constraintsmodules withspatial { scheme = spectral order = 0 }, the compiler emitstensorium_spectral_residual_<target>point kernels for scalar residuals. Every referenced scalar field receives its complete value, gradient, and Hessian bundle, so one residual may contain derivatives of several unknowns. Generated host headers expose these throughtensorium_spectral_residual_kernels. SpectralResidualProblemis the runtime-side assembly surface for these point kernels. It binds the grid, generated callback, scalar params, optional auxiliary fields, optional coordinate map, optional derivative map, and optional generated grid kernel. A derivative map transforms the complete first/second derivative bundle before either kernel path is evaluated; the TwoPunctures implementation uses it to expose Cartesian derivatives from the compact(A,B,phi)collocation domain. An independentSpectralUnknownMapmay first transform the solver variable and its logical derivatives into the physical unknown consumed by the generated residual.makeLinearBoundaryFactorUnknownMap()implements the generic productu=scale*(q_axis-boundary)*v; the two-puncture runtime uses it forU=(A-1)vwithout embedding that representation in the DSL equation or nonlinear solver.assembleSpectralResidual(...)computes the global collocation vectorF(u)plus L2/max norms. WhenSpectralResidualProblem::gridKernelis set fromtensorium_spectral_residual_grid_kernels, assembly uses the generated MLIR/LLVM global kernel; otherwise it falls back to the pointwise callback loop. For ABI v3 kernels,evaluateSpectralJacobianVectorProduct(...)calls the generated forward-mode JVP kernel. Manually supplied kernels that leave that callback null retain the centered finite-difference path as a fallback.solveSpectralNewton(...)is the first scalar spectral elliptic solve path. It uses the common JVP interface to assemble a dense Jacobian, solves the dense Newton system with pivoting, and performs a damped residual-decreasing line search. InSpectralLinearSolveKind::Auto, small problems use the dense Jacobian path and grids abovedenseJacobianMaxUnknownsuse matrix-free GMRES over the same JVP interface. GMRES supports right preconditioning through Jacobi JVP diagonals, a dense collocation inverse-Laplacian oracle, and a modallaplacian + shiftinverse approximation intended as the scalable runtime path.SpectralResidualSystemProblemis the multi-residual assembly surface. It combines several scalarSpectralResidualProblemequations into one equation-major vector[F0(points), F1(points), ...]. Each equation has one primary unknown and may use the complete derivative bundles of other current unknowns through auxiliary mappings. The first differentiated field encountered in the residual expression is the primary unknown; formulations should therefore place their principal operator first. Generated host headers exposetensorium_spectral_residual_systems[]with the constraint-block name, field-major unknown names, per-equation residual names, unknown indices, point/grid kernel indices, parameter names, auxiliary field names, and auxiliary-to-unknown maps (-1means static auxiliary).evaluateSpectralResidualSystemJacobianVectorProduct(...)returns equation-majorJv, resolving both primal and tangent auxiliary-unknown maps so coupled residuals receive the current direction. It uses generated JVP kernels only when every equation exposes one, otherwise it falls back to a consistent finite-difference perturbation of the whole field bundle.solveSpectralNewton(...)also accepts a system problem plus a mutable field-major unknown bundle; it solves the square multi-residual system with either dense JVP assembly or matrix-free GMRES over the same system JVP.SpectralPreconditionerKind::DiagonalJVPadds a Jacobi smoke preconditioner by estimating the residual/Jacobian diagonal from JVP columns.SpectralPreconditionerKind::DenseLaplacianShiftapplies a right preconditioner by solving dense collocation systems forlaplacian + shift, giving the first runtime inverse-Laplacian path on the existing spectral derivative matrices. It is kept as a small-grid oracle.SpectralPreconditionerKind::ModalLaplacianShiftapplies the samelaplacian + shiftidea in spectral coefficient space using Chebyshev/Fourier modal transforms. For the current Chebyshev/Chebyshev/Fourier layout it solves dense Chebyshev 2D modal blocks per Fourier mode and per field; current nonlinear and system runtime solve tests use this path.SpectralPreconditionerKind::MappedFiniteDifferenceLaplacianShiftinstead builds at most seven mapped finite-difference coefficients per collocation row and approximately inverts them with symmetric relaxation. The physical TwoPunctures regression combines thisO(N)preconditioner with bounded- memory restarted GMRES.SpectralPreconditionerKind::MappedFiniteDifferenceMultigridextends the same mapped sparse operator with a recursive geometric hierarchy. Restriction and prolongation use tensor-product Chebyshev/Fourier interpolation; exact Galerkin coarsening is retained for small two-level problems, while deeper hierarchies use stable mapped rediscretization at the terminal level. Dense LU is limited to the configured small terminal grid and cached across Krylov applications. At each Newton state, the point JVP kernel also evaluates the local reaction derivativedF/duinO(N)work (or estimates it with the finite-difference fallback).tests/fixtures/elliptic/spectral_hamiltonian_toy_nonlinear_3d.tnis a manufactured nonlinear spectral constraint. Its runtime test solveslaplacian(psi) + mass * psi + alpha * psi^5 + source = 0from a non-exact local guess with matrix-free GMRES preconditioned bylaplacian + mass, validating nonlinear Newton/JVP behavior against an analytic solution.tests/fixtures/elliptic/spectral_lichnerowicz_manufactured_3d.tnis the first manufactured conformal Hamiltonian fixture:laplacian(u) + 1/8 * A2 * (background + u)^(-7) + source = 0. Its runtime test keepsA2andsourceas auxiliary fields, solves from a non-exact guess with modal GMRES preconditioning, and checks against the analytic manufactured solution.tools/dev/test_generated_two_puncture_solve_ll.shexercises the first mapped-domain vertical slice. It compiles a DSL Poisson residual to LLVM, transforms the runtime derivative bundle through the compact two-puncture geometry, and solves a manufacturedU=(A-1)vcorrection with Newton.tests/fixtures/elliptic/spectral_two_puncture_hamiltonian_3d.tncontains a physical, generated two-centre Bowen-York/Lichnerowicz residual. The DSL computes both puncture distances, arbitrary momentum and spin contributions, their full symmetric-tensor contraction, the singular conformal background, and the nonlinear Hamiltonian residual. Its runtime test checks the closed single-puncture momentum/spin contractions, the exact Brill-Lindquist limit, and a nonlinear equal-and-opposite-momentum solve on the compact map.SpectralAxis::interpolate(...)andSpectralGrid3D::interpolate(...)evaluate collocation fields at arbitrary logical probes with Chebyshev barycentric/Fourier tensor-product interpolation. The two-puncture regression uses the compactified endpoint valuev(A=1,B=0,phi=0)for the ADM energym1+m2-4*b*v_infinityand compares fixed probes across three resolutions.TwoPunctureHandoff.hanalytically inverts the compact map at arbitrary Cartesian target points, applies the configured spectral unknown transform, and writes conformally flat BSSN fields into caller-owned SoA buffers. The physical regression exercises finite puncture limits and rechecks the Hamiltonian and momentum constraints after interpolation.- The compiler also emits
tensorium_spectral_residual_grid_<target>MLIR/LLVM kernels. These consume the runtime-computed spectral derivative buffers, auxiliary field buffers, coordinate buffers, scalar params, and one residual output buffer, then call the point kernel inside an MLIRscf.forloop. This moves globalF(u)evaluation into generated code while keeping spectral differentiation in the runtime grid layer for now. Generated host headers expose uniform descriptors throughtensorium_spectral_residual_grid_kernels.
Generated host headers also expose the same runtime contract in C-compatible tables:
TENSORIUM_HOST_KERNEL_COUNT/tensorium_host_kernels;TENSORIUM_HOST_BUFFER_COUNT/tensorium_host_buffers;TENSORIUM_HOST_KERNEL_ADAPTER_COUNT/tensorium_host_kernel_adapters;tensorium_host_buffer_desc::component_count,role,access, andarg_indexare enough for a lightweight runtime to deduplicate buffers and bind generated wrappers without reconstructing tensor sizes by hand.
Runtime code should use this contract instead of reconstructing argument order from names. For AMReX this means:
- allocate one
MultiFabcomponent group per logical field or map component ranges according tocomponentCount; - allocate at least
stencilRadiusghost cells for RHS kernels; - bind read/write buffers according to
HostBufferABI::access; - reject descriptors with non-empty
validateHostModuleABIdiagnostics.
Generated grid loops should not perform per-point heap allocation. Scratch buffers used by fallback init-grid lowering are hoisted outside the generated loop and reused for every point. RHS old-state snapshots, when required by read/write overlap, are full-grid temporaries allocated once before the loop and released after it.
The development probe can print this contract for a fixture:
build/tools/runtime/Tensorium_abi_probe \
tests/fixtures/gr/schwarzschild_bssn_constraints_analytic_3d.tnMLIR-level:
(params..., coords..., alpha: memref<1xf64>, gamma: memref<9xf64>, gammaU: memref<9xf64>) -> ()
LLVM-level:
- scalar
f64params/coords first, - then 3 memref descriptors (
alpha,gamma,gammaU), each expanded to 5 C arguments.
MLIR-level:
(params..., coord_buffers..., alpha, gamma, gammaU) -> ()- coord/output buffers are
memref<?xf64>.
LLVM-level:
- scalar params first,
- then one descriptor per coord/output buffer.
MLIR-level:
(nx:index, ny:index, nz:index, dx:f64, dy:f64, dz:f64, params..., fields...) -> ()- each field is
memref<?xf64>.
LLVM-level:
- prefix:
i64,i64,i64,double,double,double, - then scalar params (
double), - then one 5-argument memref descriptor per field buffer.
Same low-level signature shape as tensorium_rhs_grid_{scf,affine}:
- prefix:
i64,i64,i64,double,double,double, - scalar params,
- one 5-argument memref descriptor per participating field buffer.
The semantic difference is the ABI kind:
tensorium.abi.kind = "residual_grid_scf"or"residual_grid_affine".
Residual grid kernels write the declared residual targets from a
constraints block. Host wrappers and descriptor tables expose those outputs in
the same tensorium.abi.output_names and access metadata used by RHS kernels.
Same low-level signature shape as tensorium_rhs_grid_affine:
- prefix:
i64,i64,i64,double,double,double, - scalar params,
- one 5-argument memref descriptor per participating field buffer.
The semantic difference is the ABI kind:
tensorium.abi.kind = "rhs_grid_parallel"or"residual_grid_parallel".
The Tensorium MLIR body is a three-dimensional scf.parallel over the interior
stencil domain. The LLVM lowering pipeline converts this path to OpenMP runtime
calls, so executables that link the generated object need an OpenMP runtime.
MLIR-level:
(field0_value, field0_d1, ..., field0_d33, field1_value, field1_d1, ..., fieldN_d33, x1, x2, x3, params...) -> f64
Host callback-level:
- generated headers define a
tensorium_spectral_residual_kernel_descentry; - the callback receives
tensorium_spectral_residual_point, scalar params, and optional user data; - the first
tensorium.abi.field_namesentry is the primary unknown; - subsequent scalar fields remain available by value through
point.aux_values[]for manual callback compatibility and expose their full bundles throughpoint.aux_derivatives[]in the same field-name order; point.physical[]is populated by the runtime coordinate map before the generated residual is called.
Residual-system descriptors can bind an auxiliary field to another current unknown. Assembly then propagates that unknown's unknown-map, projector, coordinate-derivative map, and complete derivative bundle into both point and grid kernels.
MLIR-level:
(field0_bundle, ..., fieldN_bundle, direction_field0_bundle, ..., direction_fieldN_bundle, x1, x2, x3, params...) -> f64, where every bundle is(value, d1, d2, d3, d11, d12, d13, d22, d23, d33).
Host callback-level:
- the ABI v3 residual descriptor pairs
evaluate_jvpandjvp_symbol_namewith the primal callback; - the callback receives a primal
tensorium_spectral_residual_pointand a direction point with the same derivative/auxiliary layout; - parameters and coordinates have zero tangent, while auxiliary tangents are populated from coupled unknown directions when the system descriptor maps them to another unknown.
The compiler emits the primal and tangent MLIR together from the DSL residual.
The forward-mode rules currently cover scalar +, -, *, /, sqrt,
sin, first/second derivatives of any referenced scalar field, contractions
that form a Laplacian, contracted scalar gradients, and laplacian().
Constants, parameters, and coordinates have zero tangent.
Opaque external scalar calls without a registered derivative remain
unsupported in this generated path. The runtime automatically falls back to
centered finite differences when evaluate_jvp is null.
MLIR-level:
(n_points:index, params..., field0_bundle..., fieldN_bundle..., x1, x2, x3, residual_out) -> ()- every bundle component, coordinate, and output buffer is
memref<?xf64>.
Generated host wrapper:
tensorium_call_spectral_residual_grid_<target>(n_points, params..., field0_value, ..., fieldN_d33, x1, x2, x3, residual_out)tensorium_spectral_residual_grid_kernels[]exposes the same generated kernel through a uniform callback consumed bySpectralResidualProblem; its auxiliary SoA bundles usetensorium_spectral_residual_derivative_fields.
This kernel does not compute spectral derivatives itself. The runtime supplies the derivative buffers from its selected spectral basis and coordinate mapping, then the generated MLIR loop evaluates the residual at every collocation point.
Tensor field components are flattened as:
flat = component * nPoints + pointLinearIndex
where:
componentis row-major over tensor indices,pointLinearIndexis row-major over(x,y,z)grid index.
Examples:
- covariant/convariant 2-tensor uses 9 components.
- rank-3 tensor uses 27 components.
tensorium_rhs_grid_* snapshots all field buffers before stencil reads, then
writes dt_assign targets to original field buffers. This guarantees read
consistency within one RHS sweep and avoids write-after-read hazards.