Precision and factor storage¶
Precision is chosen separately for graph storage, the assembled factor, the
installed triangular-solve arrays, and the outer iteration. A build with
APXCHOL_POOL_FP32=OFF is not an all-FP64 solver.
Stage |
Representation |
Reason |
|---|---|---|
Mutable residual graph |
FP32 weights by default; optional FP64 weights |
Storage/bandwidth versus rounding during factor construction |
Assembled factor |
FP32 values |
Shared factor/export representation |
CPU triangular solves |
FP32 storage by default; optional scaled FP16 off-diagonals |
FP16 requires efficient conversion; CPU arithmetic remains FP64 |
CUDA triangular solves |
Scaled FP16 off-diagonals by default; FP32 alternative |
Reduced value traffic; GPU triangular-solve arithmetic is FP32 |
FP16 scales and diagonals |
FP32 |
Retain scale range and diagonal quality |
Outer CPU/CUDA PCG |
FP64 vectors and reductions |
Preserve the original-system iteration and residual accuracy |
Metal triangular solves |
FP32 storage by default; optional scaled FP16 |
Shared CUDA/Metal dataflow worker; FP32 arithmetic |
Metal PCG |
Mixed FP32 and double-float; host FP64 exit residual |
Metal has no native FP64 arithmetic; see below |
factor_options::factor_storage selects triangular-solve storage per solver (automatic, fp16, or fp32). Python exposes this as factor_storage_dtype=None, np.float16, or np.float32. The default is CPU/Metal FP32 and CUDA FP16; C++ owns parsing and default selection.
Construction precision is independent of triangular-solve storage; Python factor exports preserve the C++ factor’s FP32 dtype, while CPU solve inputs and results use FP64 arithmetic.
CPU FP16 is available on x86 CPUs with AVX/F16C and on Apple ARM64, including portable wheels. Unsupported explicit requests fail at setup. The CPU path selects available conversion and SIMD instructions at runtime.
The GPU operator may use FP32 storage when the original values are exactly representable; this is separate from narrowing a preconditioner. CUDA rescales extreme RHS and warm-start vectors by a common power of two and restores the solution scale on return. Intermediate triangular-solve values must still fit FP32.
Built-in solves verify the final relative residual in FP64: CUDA checks on the device, Metal on the host. Verification uses the original symmetric operator (canonical lower-triangle coefficients) and unmodified RHS, before any preconditioner lumping.
If a recursive tolerance stop fails this check, the solver can continue from
the current solution within the remaining iteration budget. Numerical breakdown
does not restart. Check the returned converged flag. CUDA retains one extra
FP64 RHS vector per allocated panel column for verification.
Scaled FP16 contract¶
drop_tolerance must be in [0, 1]; zero disables threshold dropping. For each
column, let s_j be the maximum absolute off-diagonal factor value, or one
for an empty/zero off-diagonal column. The threshold uses this scale before
dropping. Compensation redistributes dropped mass among retained
off-diagonals before narrowing, when any remain.
An off-diagonal is stored by native conversion of L_ij / s_j to FP16.
Under the normal floating-point environment this rounds to nearest, ties to even.
FP16 subnormals are flushed to signed zero. The scaled diagonal remains FP32
and absorbs the off-diagonal rounding residual. Degenerate scales fall back
to one before dropping; invalid final diagonals/scales are rejected.
Let S be the diagonal matrix of column scales s_j. CPU and CUDA solve with
the scaled factor L S^-1: the forward sweep returns S y, and the backward
sweep applies the reciprocal scale squared to its input. CPU
arithmetic widens to FP64, GPU triangular-solve arithmetic to FP32. A stored
FP16 factor is not an FP16 outer solve. CPU and GPU-host preparation share the
narrowing/flush rules in lowprec.h; CUDA and Metal finalize device-owned factors on the device.
Metal reconstructs FP32 coefficients before multiplication; each column stores an FP32 scale and compensated diagonal.
Apple Metal PCG¶
Metal factorizes supported inputs on the device and prepares FP32 or scaled FP16 solve storage with compensated dropping. The solver accepts vector or matrix right-hand sides, reusing its factor and workspace.
Double-float represents a value as the sum of two FP32 numbers, giving about 48 significant bits but not IEEE FP64 precision or range. Metal uses it for x, r, A p and fixed-tree reductions. The operator uses FP32 when all values are exactly representable there, and double-float otherwise. Directions p, preconditioned residuals z, alpha and beta are FP32. Setup accumulates degrees and diagonal excess in double-float; graph weights and exported factor values are FP32.
Each RHS (or initial residual b - A x0) is scaled by an exact power of two,
targeting a largest magnitude in [1, 2) within FP64’s exponent range. The host
computes (tol ||b|| s)^2 in FP64; the device stops when its recursive squared
residual is strictly below that threshold. Breakdown is not convergence.
The FP64 exit check described above also covers early exits and breakdown.
Laplacian iterations use grounded coordinates. Host FP64 centering applies to
the initial residual and returned solution, and to both sides of apply.
Repeatability covers the same factor and execution configuration, not different
host thread counts, devices, OS/compiler versions or independent factorizations.
Runtime compilation uses safe math and disables contraction where supported;
metal_solver::available() checks rounding, intermediate precision and FMA.
Metal rejects inputs outside these ranges:
Nonzero operator values and unscaled factor diagonals: magnitude in [2^-100, 2^100].
Factor off-diagonals: finite values.
Factor/operator offsets: signed 32-bit integers; the operator must have fewer than 2^30 stored entries.
Positive regularization: in [2^-100, 2^100], with normal positive FP32 products when multiplied by the operator diagonal.
Accuracy and configuration choice¶
FP16 applies to the installed triangular-solve arrays, not the mutable graph or assembled/exported factor. Graph weights use FP32 by default; FP64 pool storage is an optional reference. Scaled FP16 graph storage is not supported.
Low-precision preconditioning can retain final solution accuracy while changing iteration count. Select precision using original-system residuals, total solve cost and memory on the intended inputs; smaller storage alone is not a speed guarantee. Bitwise factor repeatability is a separate property from convergence.
A fixed factor seed determines the per-vertex random streams, but does not promise byte-identical independently rebuilt parallel factors. Thread arrival order can change floating-point sums, which can change sampled edges as well as factor values. FP32 rounding may hide some differences that FP64 retains; neither precision makes floating-point addition independent of order. FP16 factor storage is applied later and does not make factor construction deterministic.
Selection is deterministic for the same graph, candidates, selector state and actual thread team. Reusing one factor avoids the variability of rebuilding it; moving a host factor to a GPU does not change how it was constructed.
The benchmark protocol describes how to compare supported configurations without changing timing boundaries.