Contraction-parallel ops (the ``*_reduced`` family) =================================================== Every default L2/L3 product in GLASS maps **one thread to one output element** and walks the contraction dimension *serially* inside that thread: ``gemm`` / ``gemv`` / ``syrk`` all loop ``for k: acc += A[..]*B[..]``. When the output count is large that saturates the block. When the output count is *small* — a 7×7 control Hessian, a length-14 mat-vec — most of the block sits idle while a handful of threads grind through the sum. The ``*_reduced`` family flips the mapping: **one warp owns one output**, and its 32 lanes split the contraction, combining with a single warp-shuffle reduce (:cpp:func:`glass::warp::reduce`). The engine appears as: - :cpp:func:`glass::gemm_reduced` — the core (and the mechanism behind FR-4); - :cpp:func:`glass::gemv_reduced`, :cpp:func:`glass::syrk_reduced` — the L2 / SYRK siblings; - the tensor and congruence families (``tensor_vec_contract``, ``vec_tensor_vec``, ``congruence_sym``, ``bilinear``) — products the serial BLAS surface cannot express in one call, built on the same engine. All ship in the three SIMT surfaces (``glass::`` block, ``glass::warp::``, ``glass::cgrps::``). .. warning:: **Expressiveness / fusion only — not a throughput path.** Measured on a quiet RTX 5090 (sm_120), the contraction-parallel decomposition is **slower than the plain serial in-thread contraction in 47 of 48 swept shapes**, often by 10–100× (full table: ``bench/RESULTS.md`` (reduced section)), and the :cpp:func:`glass::suggested_use_reduced` picker now declines it everywhere (retired to constant ``false`` after the 2026-07-08 quiet resweep measured 0/48 wins under the ±5% tie margin). **Prefer the plain ops** (``gemm`` / ``gemv`` / ``syrk``) for throughput; reach for this family only for the fused forms (``tensor_vec_contract``, ``vec_tensor_vec``, ``congruence_sym``, ``bilinear``) that the serial surface cannot express in one call. The honest win-condition ------------------------ The total multiply-add work is **identical** to the serial op — the contraction is the same length either way. The *only* thing ``*_reduced`` buys is thread **utilization**, and only when both of these hold: #. **The output count is smaller than the block** (``n_out < blockDim``) — so the serial path would leave threads idle, and there is spare parallelism for the warp-per-output mapping to soak up. #. **The contraction K amortizes the shuffle tail** — the warp reduce costs a ~5-step ``__shfl_down`` tail per output, so K must be large enough (roughly the 14–21 range of a trajectory-optimization knot) for the split to pay for it. When ``n_out >= blockDim`` with a small K, ``*_reduced`` is **neutral or slower** — the serial op already keeps every thread busy and avoids the shuffle. So this is **opt-in**: the default ops are unchanged, and a caller (or GRiD-style codegen) chooses ``*_reduced`` only where it wins. What the measurement actually says ---------------------------------- The crossover sweep (``bench/bench_reduced.cu``, full table in ``bench/RESULTS.md`` (reduced section)) was run on a quiet **RTX 5090 / sm_120**. The result is blunt: **the contraction-parallel path is slower than serial in almost every configuration** — 47 of 48 swept shapes lose, often by 10–100×. The serial ``gemm`` over shared-resident data is a tight per-thread loop that is very hard to beat at these sizes, while ``*_reduced`` pays a ~5-step shuffle latency per output and, at the typical short contraction (K = 14–21), leaves most of a warp's lanes idle. The earlier shared-GPU sweep showed one marginal win (``n_out = 4, K = 64`` at ``blockDim ≥ 128``, ~1.1×); the quiet-GPU resweep of 2026-07-08 collapsed even that cell into the ±5% noise band — **0 of 48 configurations pick reduced**. So there is no "~14×", and no corner either; the realistic story is "use serial." :cpp:func:`glass::suggested_use_reduced` encodes that measurement — it returns ``false`` unconditionally on sm_120, and keeps its ```` signature as the seam where a retune on different hardware (e.g. Jetson Orin) can reinstate a data-derived corner without touching call sites: .. code-block:: cuda if constexpr (glass::suggested_use_reduced()) glass::gemm_reduced(1.f, A, B, 0.f, C); else glass::gemm(1.f, A, B, 0.f, C); .. note:: The tensor / congruence families (``tensor_vec_contract``, ``vec_tensor_vec``, ``congruence_sym``, ``bilinear``) share this engine and so inherit the same overhead. Their value is **expressiveness and fusion** — operations the serial surface cannot express in one call — not beating a hand-tuned serial loop. If you are optimizing for latency, benchmark against your own serial code first. Thread-count invariance ----------------------- Like every GLASS primitive, the ``*_reduced`` ops are **thread-count invariant**: identical output at 1 thread, a partial warp, or many warps. Each output is reduced by the *same* fixed 32-way tree regardless of how many warps the block has — a trailing partial warp (``blockDim % 32``) idles, and below 32 threads a register path (:cpp:func:`reduced_tree32`) reproduces the warp-shuffle summation order **bit-for-bit**, so the result does not change across the 32-thread boundary. Within a surface this is bit-identical. *Across* surfaces (block vs warp vs cgrps) the single-step ops agree bit-for-bit, while the composed two-step ops (``congruence_sym``, ``bilinear``, ``riccati_gain``) agree only to floating-point tolerance, because their intermediate gemm may fuse its FMA differently per instantiation. Both are correct; only the rounding of the last bit differs.