Skip to content

Read backward reductions freshest-last (closes most of the remaining gap) - #45

Merged
ChrisRackauckas merged 1 commit into
JuliaSIMD:mainfrom
ChrisRackauckas-Claude:ldiv-upper-perf2
Aug 8, 2026
Merged

Read backward reductions freshest-last (closes most of the remaining gap)#45
ChrisRackauckas merged 1 commit into
JuliaSIMD:mainfrom
ChrisRackauckas-Claude:ldiv-upper-perf2

Conversation

@ChrisRackauckas-Claude

Copy link
Copy Markdown

⚠️ Draft — please ignore until reviewed by @ChrisRackauckas.

Second (and, on this machine, final) round of closing the backward-vs-forward gap, following #44. Performance-only; forward kernels untouched; bumps version to 0.2.3 (0.2.2 was registered from a0118b0, before #44 merged).

What changed and why

Same theme as #44's fma-chain fix, but in memory rather than registers: the backward kernels' reduction over the already-solved trailing columns ran ascending nk, so its first loads hit the columns the previous kernel call had just stored — stalling on stores still draining from the store buffer. The forward kernels' ascending reduction naturally ends on their freshest block instead. Walking the backward reduction descending restores the freshest-last property.

A welcome side effect: the down-counting loop compiles to the same shared-scaled-index addressing as the forward loop (2 address-update instructions per trip instead of 6 — LLVM had been splitting the broadcast streams into independent pointer inductions in the ascending form; #44's zero-based re-basing alone had not fixed that).

Measured (EPYC 7502 / AVX2, Float64, nrhs = 8, min times, 1 BLAS thread)

Left-upper vs left-lower ldiv! at equal UNIT flag (μs):

   n  | uLower  uUpper(#44) uUpper(this) | ratio #44 -> this
   48 |   0.68     0.78        0.75      |  1.15x -> 1.10x
   64 |   1.25     1.38        1.32      |  1.11x -> 1.06x
  128 |   4.45     4.70        4.61      |  1.06x -> 1.04x
  256 |  18.74     ~20.3       19.18     |  1.05x -> 1.02x
  500 |  73.80     75.43       74.97     |  1.02x -> 1.02x
 1000 | 284.37    286.08      284.97     |  1.02x -> 1.00x

The remaining ≤10% at n ≤ 64 is a ~5 ns/kernel-call fixed cost that shrinks under profiling instrumentation (i.e. at the measurement noise floor); I could not attribute it to any further structural asymmetry.

Left-upper ldiv! (non-unit) vs OpenBLAS trsm:

   n  |   BLAS   TS upper   speedup     (#41 baseline)
   32 |   1.63     0.57      2.86x       (2.2x)
   64 |   4.12     1.51      2.73x       (2.3x)
  128 |  12.37     4.99      2.48x       (2.2x)
  256 |  48.36    19.51      2.48x       (2.3x)
  500 | 153.31    77.05      1.99x       (1.9x)
 1000 | 587.23   290.08      2.02x       (2.0x)

Verification

  • Full suite: TriangularSolve.jl | 148781 148781 pass (Julia 1.12.4), Aqua + ambiguities clean.
  • Extended sweep (n = 1..400 incl. all remainder cases, nrhs = 1..32, Float32/64, unit/non-unit, 2/3-arg, Val(true/false), packed lu! parents to 512, BigFloat fallback): 0 failures; zero steady-state allocations.
  • Reduction order change reassociates the fma accumulation, so results can differ from 0.2.2 by rounding within the same fast-math regime the kernels already use.
  • Not verified: AVX-512 / aarch64 (this machine is AVX2), MKL.

After merge this needs a @JuliaRegistrator register from an org member (as in #43) to ship 0.2.3.

🤖 Generated with Claude Code

The backward kernels' nmuladd reduction read the already-solved trailing
columns with ascending nk, so its first loads hit the block the previous
kernel call had just stored, stalling on stores still draining. Walking
nk descending reads the oldest columns first and the freshest block last
— the mirror of the forward kernels' ascending reduction, which also
ends on its most recently written block. The down-counting loop also
compiles to the same shared-scaled-index address form as the forward
loop (two address updates per trip instead of six).

Measured (EPYC 7502/AVX2, Float64, nrhs=8, min times), left-upper ldiv!
vs left-lower at equal UNIT flag, after the chain fix in JuliaSIMD#44:
n=48 1.15x -> 1.10x, n=64 1.11x -> 1.06x, n=128 1.06x -> 1.04x,
n=256 1.05x -> 1.02x, n=1000 1.02x -> 1.00x. Vs OpenBLAS trsm the
upper leg is now 2.0-2.9x. Forward kernels untouched.

0.2.2 was registered from a0118b0 (pre-JuliaSIMD#44), so the JuliaSIMD#44 and this-PR
improvements ship as 0.2.3.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
@JuliaRegistrator

Copy link
Copy Markdown

Error while trying to register: Register Failed
@ChrisRackauckas-Claude, it looks like you are not a publicly listed member/owner in the parent organization (JuliaSIMD).
If you are a member/owner, you will need to change your membership to public. See GitHub Help

@ChrisRackauckas
ChrisRackauckas marked this pull request as ready for review August 8, 2026 08:52
@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.62%. Comparing base (b28192b) to head (e4d4714).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #45      +/-   ##
==========================================
+ Coverage   94.55%   94.62%   +0.06%     
==========================================
  Files           1        1              
  Lines         827      837      +10     
==========================================
+ Hits          782      792      +10     
  Misses         45       45              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ChrisRackauckas
ChrisRackauckas merged commit 8803b92 into JuliaSIMD:main Aug 8, 2026
7 checks passed
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Author

Round 3, pushed to this PR: the remaining fixed ~5 ns/kernel-call was hunted with a direct-call A/B harness (dispatch excluded, zero-trip reduction so only the straight-line body differs; reproducible 5.3±0.7 ns across sessions — not code-placement luck). Facts established: per-trip loop cost is now identical (2.60 vs 2.65 ns/trip), every FP opcode count in the two bodies matches exactly, the delta is +15 integer bookkeeping instructions + 2 extra callee-saved registers, llvm-mca -mcpu=znver2 models only ~10 of the ~20 cycles, and five source-level variants (guard-sunk pointer setup, absolute-index loop, store-order changes, combinations) all failed to close it — it is call-boundary overhead, not kernel structure.

Resolution: $(Expr(:meta, :inline)) on uldiv_solve_W_u! and uldiv_solve_W!, whose driver call sites are unique, eliminating the boundary and letting LLVM hoist stride-invariant setup out of the _ldiv_U! loop. The forward kernels keep their (cheaper) non-inlined calls, mirroring their commented-out inline metas.

Final gap (min times, unit-vs-unit, nrhs=8): n=36 1.09x, n=48 1.06x, n=64 1.04x, n=128 1.02x, n≥256 1.00-1.01x — from 1.39x/1.30x/1.17x at n=48/64/128 when this started. Suite 148,781/148,781 green, sweep + packed-LU + alloc checks clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants