|
docc 0.8.3 (0d44999b)
C++ API Reference
|
Directories | |
| contractions | |
| library_nodes | |
| passes | |
| transformations | |
Files | |
| plugin.h | |
Floating-point arithmetic is not real arithmetic: every operation rounds its result to the nearest representable value. Transformations that are trivially correct over the reals — reassociating a sum, contracting a multiply and an add — change where that rounding happens and therefore, in general, the bits of the result. Compilers expose these rewrites behind explicit opt-ins (-ffp-contract, -fassociative-math, -ffast-math, ...). This module collects the SDFG transformations that rely on such relaxed floating-point semantics.
A fused multiply-add computes a * b + c with a single rounding at the end, instead of rounding the product and then the sum:
Most hardware executes an FMA in one instruction, and the result is at least as accurate as the unfused sequence — but it is a different result. Contraction is therefore legal only if the user opts into it; -ffp-contract=fast allows it across statements, which is what an SDFG block, holding the dataflow of many statements, needs.
| Transformation | Rewrite | Notes |
|---|---|---|
| FuseFMA(add) | t = a * b; d = t + c → d = fma(a, b, c) | t must be a transient scalar written by the fp_mul and read only by the fp_add, both in the same block; all operands of the same precision. |
| UnfuseFMA(fma) | d = fma(a, b, c) → t = a * b; d = t + c | Inverse of FuseFMA; introduces a new transient scalar t for the product. |
FPContractionPass applies FuseFMA in every block until a fixed point is reached. It runs at the end of the Python frontend's simplify if TargetOptions::fp_contract (DoccOptions(fp_contract=...)) is set, which is the default, so the SDFG is consistent with frontends that already emit fp_fma (e.g. clang's default contraction); later passes (einsum detection, reduction recognition, targets) handle fp_fma like a multiply/add pair. With fp_contract=False no contraction is performed, while FMAs created explicitly by a frontend are kept. UnfuseFMA is used where a target prefers the split form, e.g. to hand the product on to a separate computation.
Matrix units are far faster in Int8 than in Float or Double, e.g. 2x FP16 on AMD CDNA3 and ~30x FP64 on NVIDIA H100. The transformations below compute a Float or Double MatMul (Y = A @ B) or row-major GEMM (C = alpha A @ B + beta C, transposes allowed) from exact Int8 matmuls — MatMul library nodes with ContractionTypes{Int8, Int32, Int32} that the target maps, e.g. onto integer matrix units — plus elementwise maps that split the operands and combine the products. Every row of A and column of B is scaled by its own factor, so rows and columns of different magnitudes keep their precision.
| Transformation | Result | Int8 matmuls | Accuracy |
|---|---|---|---|
| QuantizeContraction(matmul, bits = 8) | Y ≈ (A_q @ B_q) · s_A[i] · s_B[j] / q² with A_q = rint(q · A / s_A), q = 2^(bits-1) - 1, s the absmax of the row or column | 1 | lossy: ~1/q of the largest magnitudes (3.6e-3 measured for 8 bits) |
| OzakiContraction(matmul, Slicing, s) | Ozaki-I: A / 2^e = Σ_t 2^-7t A_t in Int8 slices; Y = 2^(e_i+f_j) Σ_{t+u ≤ s+1} 2^-7(t+u) A_t @ B_u | s(s+1)/2 | ~7s bits per row and column; default s = 4 (Float), 8 (Double) |
| OzakiContraction(matmul, Modular, L) | Ozaki-II: integers A' = trunc(2^q A / 2^e), their product modulo L coprime moduli (one matmul each), reconstructed with Garner's algorithm | L | q bits with K 2^2q < Π m_l / 4; default L reaches 26 (Float) or 55 (Double) bits |
The Ozaki variants are as accurate as the native matmul — 1.7e-16 (Double) and 2.3e-8 (Float) measured against 1.7e-16 and 5.6e-8 natively, relative to Σ_k |A[i,k] B[k,j]| — but not bitwise equal to it. Like every Ozaki scheme they lose bits on elements much smaller than the largest of their row or column.
Where they pay off:
On AMD GPUs the generated matrix-core path (GpuMmaExpander, GpuMmaEinsumTransform) maps the Int8 matmuls onto Int8 MFMA/WMMA with Int32 accumulators, using the full-rate 16x16x32 blocks on CDNA3 (gfx942) and 16x16x16 on CDNA2 and RDNA.
Requirements: a 2-D MatMul whose inputs, accumulator and output share the Float or Double type, or a row-major Float or Double GEMM with constant alpha ≠ 0 and beta, and a constant K for which the Int32 accumulation is exact (K ≤ 133143 for Int8 values up to ±127). Only constant coefficients keep BLAS semantics: C is not read if beta is 0, and A @ B is not computed if alpha is 0, even if they hold NaNs or products that overflow. The Ozaki splitting and reconstruction run in Double. Results are rescaled by the sum of their row's and column's power-of-two exponents in one step, so no intermediate overflows where the result does not. QuantizeContraction(matmul, bits, Rescaling::Direct) multiplies by the scales instead, for units without floor or exact powers of two; it overflows only for magnitudes within a factor K q of the type's largest value. Quantization computes its scales and results in the contraction's own type, whose rounding is far below the quantization error, so targets without Double run it.
The scales are Max reductions (Reduce loops), one per row of A and column of B. Quantization keeps each operand's storage order: an operand stored transposed, such as a GEMM's B with trans_b or a MatMul's column-major layout, is read along its rows and quantized into a buffer of the same order, which the Int8 MatMul then reads through a column-major layout. No operand is read across its rows, and a target that needs another order transposes the Int8 values, a quarter of the bytes of Float.
ContractionPrecisionPass applies one of these transformations to every MatMul and GEMM it can; the others stay native. The Python frontends select it with DoccOptions(contraction_precision=...) (TargetOptions::contraction_precision), applied at the start of expand, before the targets map matmuls (e.g. to BLAS), so they map the Int8 matmuls instead:
| contraction_precision | Computation |
|---|---|
| native (default) | unchanged |
| int8, int2 ... int7 | QuantizeContraction with that many bits (lossy) |
| ozaki | OzakiContraction, modular, which needs the fewest Int8 matmuls |
| ozaki1[:s] / ozaki2[:L] | OzakiContraction, slicing with s slices / modular with L moduli (default: by precision) |
With the frontend's default symbolic shapes K is not a constant, so pass symbolic_shapes=False for the transformations to apply.