Skip to content

perf(rocsparse): avoid per-row L2 write-back/invalidate on gfx942/gfx950 - #13374

Merged
ntrost57 merged 12 commits into
developfrom
users/ntrost/rocsparse/opt/spinloop-gfx9
Oct 12, 2026
Merged

ntrost57 merged 12 commits into
developfrom
users/ntrost/rocsparse/opt/spinloop-gfx9

Conversation

@ntrost57

@ntrost57 ntrost57 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

On gfx942 and gfx950, each XCD has its own L2 cache, and the L2 caches are not coherent with each other. Agent-scope acquire and release fences therefore lower to a full L2 invalidate (buffer_inv sc1) and a full L2 write-back (buffer_wbl2). The level-scheduled triangular solves and incomplete factorizations issue these fences for every row: the producer before it sets the row's done flag, and the consumer after it has waited for the flag. As a result, these routines spend most of their time on cache maintenance on these architectures.

The analysis stage has a second issue. Every row updates the maximum row length with its own device-scope atomic on a single address, and on these architectures these atomics serialize across the XCDs.

Technical details

Solve and factorization kernels. On gfx942, gfx950 and gfx9-4-generic (has_noncoherent_xcd_l2), row results and done flags are exchanged through device-scope relaxed atomic loads and stores, which carry sc1 and are coherent across the XCD L2s. They replace plain accesses ordered by agent-scope fences.

  • Producer: waits for its result stores to complete (__builtin_amdgcn_s_waitcnt(0) behind a workgroup release fence) before it stores the done flag. No buffer_wbl2.
  • Consumer: after the spin loop on the flag, a workgroup acquire fence keeps the result loads after the flag loads. No buffer_inv.

The new helpers are in rocsparse_common.hpp:

  • acquire_done_rows
  • load_done_row_data
  • store_done_row_data
  • store_done_row_result (falls back to nontemporal_store on other architectures)
  • release_done_row_data
  • set_done_flag
  • wait_store_completion (gfx9 only; used by release_done_row_data and set_done_flag when has_noncoherent_xcd_l2 is true)

On all other architectures the helpers compile to the previous code: plain loads and stores, nontemporal stores, __threadfence(), and the agent-scope acquire fence and release store.

Small-block BSR factorizations. In these kernels one wavefront factorizes a block row, so only the blocks that other block rows read need device-scope accesses. The blocks that only the same wavefront reads back use plain loads and stores, because the memory model needs no cache maintenance for coherence between the lanes of one wavefront.

  • bsrilu0 (block sizes up to 8): dependent rows read only the diagonal and upper blocks, so only those are stored with device scope. The block row's own loads and its lower-block stores are plain.
  • bsric0 (block sizes up to 16): dependent rows read every block a row stores, so all stores stay device-scope. The re-reads of the row's own blocks are plain loads. The store wait after each block is replaced with a wavefront-scope fence, because set_done_flag waits for all stores before it sets the flag.

Affected kernels:

  • solves: csrsv (also used for CSC and COO), ellsv, csrsm, and bsrsv for block sizes up to 32
  • factorizations: csrilu0, csric0, and csrildlt0 (hash and binsearch variants), bsrilu0 for block sizes up to 8, and bsric0 for block sizes up to 16 with at most 128 blocks per block row

bsrsm, bsrsv for block sizes above 32, and the larger-block bsrilu0 and bsric0 kernels keep develop's fence-based synchronization. These kernels reread each dependency's result many times, and every device-scope read bypasses the local L2, so the fence pair per dependency is cheaper there.

Analysis (gfx942, gfx950 and gfx9-4-generic only). Each row updates a per-workgroup maximum row length with a shared-memory atomic. The last row of the workgroup to finish publishes it with a single device-scope atomic. This applies to the analysis of the CSR, CSC, COO, and BSR solves and factorizations, including the analysis stage of rocsparse_spilu0, rocsparse_spic0, and rocsparse_spildlt0. Other architectures keep one device-scope atomic per row.

JIRA ID : ROCM-32435

… factorizations on gfx942/gfx950

On gfx942/gfx950, device-scope acquire/release fences lower to whole-L2
invalidate (buffer_inv sc1) and write-back (buffer_wbl2). The level-scheduled
solves and incomplete factorizations issued them once per row.

- Solves and factorizations (CSR, CSC, COO, ELL, BSR; sv, sm, ilu0, ic0,
  ildlt0) on gfx942/gfx950 exchange row results and done flags with
  device-scope relaxed atomics instead of fences (done_flag_bypass_l2).
  The result stores complete before the done flag is set.
- The analysis of the CSR/CSC/COO/BSR solves and factorizations reduces
  per workgroup in shared memory and issues a single device-scope atomic,
  instead of one atomic per row on the same address.

Other architectures keep the previous code paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
@therock-pr-bot

therock-pr-bot Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
📝 PR Description ✅ Pass —
⛔ Forbidden Files ✅ Pass —
🧪 Unit Test ⚠️ Warning Error: Source/code files changed without an accompanying unit test.
Expected: add at least one test file named like test_<name>.py / test_<name>.cpp (or <name>_test.*).
Current: code file(s) changed: projects/rocsparse/library/src/include/rocsparse_common.hpp, projects/rocsparse/library/src/level2/bsrsv_device.h, projects/rocsparse/library/src/level2/csrsv_device.h, projects/rocsparse/library/src/level2/ellsv_device.h, projects/rocsparse/library/src/level3/csrsm_device.h (+10 more); no test file found
🔎 pre-commit ✅ Pass —
🚫 Draft PR 🔜 To Be Enabled —
🚩 Feature Flag 🔜 To Be Enabled —
📊 Code Coverage 🔜 To Be Enabled —
🤖 therock-pr-bot ✅ Pass —

🎉 All checks passed! This PR is ready for review.

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

🙋 Wish to Override Policy?

@therock-pr-bot

therock-pr-bot Bot commented Oct 9, 2026

Copy link
Copy Markdown

🎉 All checks passed! This PR is ready for review.

ntrost57 and others added 4 commits October 9, 2026 09:07
- Rename done_flag_bypass_l2 to has_noncoherent_xcd_l2, include
  __gfx9_4_generic__, and document that the waits rely on the gfx9 wait
  counters.
- Move the store completion wait into wait_store_completion, and emit a
  workgroup acquire fence in acquire_done_rows so the compiler keeps the
  result loads after the done flag loads.
- Add store_done_row_result for the csrsv and ellsv row results.
- Use the __scoped_atomic builtins in the csrsv analysis reduction.
- Update stale comments, complete the CHANGELOG entry, and format with
  clang-format 13.

Co-authored-by: Cursor <cursoragent@cursor.com>
- wait_store_completion: the s_waitcnt builtin keeps memory operations on
  both sides of it; the helper is only valid with the gfx9 wait counters.
- Describe the device scope atomics as coherent across the XCD L2s
  instead of resolved beyond the L2.
- Explain the relaxed done-flag store in csrsm and bsrsm, the in-wavefront
  diagonal exchange in bsric0, and fix the remaining csrilu0 and bsrsm
  comments.

Co-authored-by: Cursor <cursoragent@cursor.com>
The bsrsv kernels for block sizes above 32, the bsrsm kernels, the bsrilu0
kernels for block sizes above 8 and the bsric0 kernels for block sizes above
16 read each dependency result once per block entry. Exchanging the results
with device-scope atomics turns every one of these reads into an access that
bypasses the L1, which costs more than the L2 write-back and invalidation it
replaces. Restore the develop code for these kernels.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codecov

codecov Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #13374      +/-   ##
===========================================
+ Coverage    71.84%   72.13%   +0.29%     
===========================================
  Files         2876     2869       -7     
  Lines       475010   473864    -1146     
  Branches     70171    69892     -279     
===========================================
+ Hits        341230   341775     +545     
+ Misses      110016   108330    -1686     
+ Partials     23764    23759       -5     
Flag Coverage Δ *Carryforward flag
TensileLite-CPP 49.33% <ø> (-0.35%) ⬇️ Carriedforward from 999bc56
TensileLite-Unit 83.29% <ø> (-0.06%) ⬇️ Carriedforward from 999bc56
hipBLAS 90.62% <ø> (ø) Carriedforward from 999bc56
hipBLASLt 38.91% <ø> (-3.03%) ⬇️ Carriedforward from 999bc56
hipCUB 82.68% <ø> (ø) Carriedforward from 999bc56
hipDNN 86.66% <ø> (ø) Carriedforward from 999bc56
hipFFT 43.43% <ø> (+0.91%) ⬆️ Carriedforward from 999bc56
hipRAND 76.12% <ø> (ø) Carriedforward from 999bc56
hipSOLVER 68.96% <ø> (ø) Carriedforward from 999bc56
hipSPARSE 86.99% <ø> (ø) Carriedforward from 999bc56
rocBLAS 48.24% <ø> (+0.07%) ⬆️ Carriedforward from 999bc56
rocFFT 51.45% <ø> (+4.94%) ⬆️ Carriedforward from 999bc56
rocRAND 57.45% <ø> (ø) Carriedforward from 999bc56
rocSOLVER 76.83% <ø> (ø) Carriedforward from 999bc56
rocSPARSE 74.11% <ø> (+0.03%) ⬆️
rocThrust 93.15% <ø> (ø) Carriedforward from 999bc56

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
...rocsparse/library/src/include/rocsparse_common.hpp 48.57% <ø> (ø)
...src/precond/bsric0/rocsparse_bsric0_kernel_2_8.cpp 0.00% <ø> (ø)
...nd/bsric0/rocsparse_bsric0_kernel_2_8_unrolled.cpp 54.29% <ø> (ø)
...rc/precond/bsric0/rocsparse_bsric0_kernel_9_16.cpp 48.31% <ø> (ø)
...c/precond/bsrilu0/rocsparse_bsrilu0_kernel_2_8.cpp 68.25% <ø> (ø)
...econd/csric0/rocsparse_csric0_kernel_binsearch.cpp 46.71% <ø> (ø)
...rc/precond/csric0/rocsparse_csric0_kernel_hash.cpp 60.61% <ø> (ø)
...csrildlt0/rocsparse_csrildlt0_kernel_binsearch.cpp 0.00% <ø> (ø)
...cond/csrildlt0/rocsparse_csrildlt0_kernel_hash.cpp 67.10% <ø> (ø)
...ond/csrilu0/rocsparse_csrilu0_kernel_binsearch.cpp 0.00% <ø> (ø)
... and 1 more

... and 126 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ilu0 2_8

Dependent block rows read only the diagonal and upper blocks of a block row.
Use plain accesses for the remaining blocks, which only the row's own lanes
read back, so that they are served from the caches again.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ntrost57
ntrost57 marked this pull request as draft October 9, 2026 10:11
In the 2_8, 2_8_unrolled and 9_16 kernels, a block row's own blocks are only
read back by the wavefront that stored them, so they need no device-scope
loads, and the per-block store wait is not needed either: set_done_flag
waits for all stores before the done flag is set. Only the blocks of the
dependency rows are read with device-scope loads.

Co-authored-by: Cursor <cursoragent@cursor.com>
ntrost57 and others added 3 commits October 9, 2026 14:38
…vice scope

The update of block m in the dot product loop is a plain store, but block m can be a diagonal or upper block that dependent rows read. It is published only because the diagonal and upper blocks are stored again with device scope before the done flag is set.

Co-authored-by: Cursor <cursoragent@cursor.com>
In csrsm the explanation of the relaxed done-flag store sat above the old label, and in the small-block bsric0 kernels the note on device-scope loads sat under the old loop label. Merge each into one comment.

Co-authored-by: Cursor <cursoragent@cursor.com>
The comment named only spin_loop, but several callers wait with their own relaxed device-scope atomic load loop. State the actual requirement instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ntrost57
ntrost57 marked this pull request as ready for review October 9, 2026 12:43
On gfx942 and gfx950 the small-block bsric0 kernels store each factored
block of the current block row with a device scope store, which bypasses
the vector L1. Later iterations read these blocks back as v2 in the dot
product, from other lanes of the same wavefront. A plain load there can
hit the L1 line that the plain load of the unfactored block allocated
earlier and return the old value. The wavefront fences that replaced the
per-block wait emit no instructions and do not prevent this.

Read these blocks back with load_done_row_data and restore the
release_done_row_data wait after storing each block. The initial loads of
the own blocks and of the own diagonal block stay plain, since no lane
has stored to those blocks yet when they are read.

Applies to the 2_8, 2_8_unrolled and 9_16 kernels.

Co-authored-by: Cursor <cursoragent@cursor.com>
csrilu0 is right-looking: processing column j updates later entries of
the current row, including the diagonal, with relaxed device scope
stores. On gfx942 and gfx950 the next column loaded its entry before the
dependency spin loop, and the diagonal was loaded after the loop, both
without waiting for those stores to complete.

Load the entry of the current column after the spin loop and wait for
the stores of the wavefront first, and wait before the diagonal reload.
The spin loop's flag load has usually drained the stores already, so the
wait is mostly free. Other architectures keep the previous code.

csric0 and csrildlt0 are left-looking and only store the current entry.
They reload earlier entries of their row only after the spin loop and
keep the diagonal contributions in registers, so they need no change.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ntrost57
ntrost57 merged commit e2aef14 into develop Oct 12, 2026
116 of 117 checks passed
@ntrost57
ntrost57 deleted the users/ntrost/rocsparse/opt/spinloop-gfx9 branch October 12, 2026 09:04
@assistant-librarian

Copy link
Copy Markdown
Contributor

TheRock Submodule Bump Activity

Newest first

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants