Skip to content

MultisplitPerm ranking "stable": ballot-ranked stable multisplit, reproducible without sorting (prototype) - #79

Closed
jowens wants to merge 1 commit into
subgroup-multisplit-corefrom
multisplit-stable-ranking
Closed

jowens wants to merge 1 commit into
subgroup-multisplit-corefrom
multisplit-stable-ranking

Conversation

@jowens

@jowens jowens commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

Step 3 of the GPU Multisplit plan: a prototype of a stable multisplit in MultisplitPerm, behind an option, to measure before restructuring anything. Stacked on #78 (it uses subgroupDigitMatch); review that first.

What it does

new MultisplitPerm({ ..., ranking: "stable" }). The default stays "atomic", and its output and code paths are unchanged.

  • wgslFunctions.wgStableRank is a new shared helper that ranks elements stably by digit across a workgroup (Ashkiani et al., TOPC 2017):
    • Ballot ranking within a subgroup (subgroupDigitMatch), with one counter row per subgroup. Only the highest matching lane adds, so no atomic arrival order reaches the result.
    • Positions are subgroup-blocked (each subgroup holds K * sgsz consecutive positions), so rank order is position order whatever the subgroup width.
    • Needs subgroups at least 32 wide: one row of 2^bits counters per subgroup.
  • Scatter: ranks each tile with wgStableRank instead of workgroup atomics.
  • Tiles: each workgroup takes a contiguous run of tiles instead of tiles slot, slot + M, …, in both hist and scatter. Otherwise a stable tile ranking still would not give a stable multisplit.
  • Finish: places sub-buckets with wgStableRank too (it used atomics).

Consequence: every group reaches the finish in index order, and each sub-bucket keeps that order. So the output is reproducible on every GPU without the sub-bucket sort or the overflow sort that reproducible: true needs today. reference() models it: the same Fisher–Yates streams, with index order where today's model sorts. Stable mode defaults to groups of 2,048, since it has no sort cost that grows with group size.

Testing

  • Simulated RDNA4 (gfx1201, wave64, the wide-mask path): every stable case equals reference() bit for bit. The cases are keys; indices with several tiles per workgroup; three passes; the overflow path; Philox-7 pairs; and 2^20 (8-bit pass).
  • Lavapipe: 8-wide, so the stable tests are skipped. Every other test passes except the existing 8-lane sort failures.
  • Not yet run: the 32-wide (u32 mask) path on real hardware. The M1 Max run below covers it.

Measure

examples/multisplitperm_bench_node.mjs compares, after a 200 ms warm-up on each variant:

  • atomic
  • reproducible (atomic + sort)
  • stable
  • Philox pairs, atomic vs stable

Decision this informs

  • If stable is about as fast as atomic, reproducible: true could use stable ranking. L3 would then cost about what L2 does. Reproducible outputs would change, and the paper's references would need regenerating.
  • If stable is slower than atomic but faster than today's reproducible, use it only for reproducible.
  • Otherwise, drop it.

Generated by Claude Code

…roducible without sorting

- wgslFunctions.wgStableRank: stable ranking by digit across a workgroup
  (GPU Multisplit, Ashkiani et al. TOPC 2017): subgroupDigitMatch within a
  subgroup, one counter row per subgroup, subgroup-blocked positions so the
  order does not depend on the subgroup size. Needs subgroups >= 32 wide.
- MultisplitPerm option ranking: "atomic" (default, unchanged) | "stable".
  Stable ranks the scatter with wgStableRank, gives each workgroup a
  contiguous run of tiles (hist and scatter), and places the finish's
  sub-buckets with wgStableRank. Every group then arrives in index order,
  so the output is reproducible with no sub-bucket or overflow sort;
  reference() models it. Default groups of 2,048 (no sort cost).
- Regression tests for stable (when subgroups >= 32): keys, indices with
  several tiles per workgroup, three passes, the overflow path, Philox
  pairs, and 2^20 in the large tests.
- examples/multisplitperm_bench_node.mjs: atomic vs reproducible vs stable,
  and Philox pairs atomic vs stable, with GPU timestamps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kfNC6id65zEHzuAAH4jtJ

jowens commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as a measured negative result. Stable (ballot-ranked) multisplit is correct everywhere tested: simulated RDNA4, M1 Max and V100, 133/133 each. It is slower than reproducible-by-sorting at every size, so it buys nothing.

G elements/s, keys, default group sizes (examples/multisplitperm_bench_node.mjs):

atomic reproducible (sort) stable
M1 Max, 2^20 4.00 3.20 1.40
M1 Max, 2^24 4.45 3.82 1.22
M1 Max, 2^26 3.85 3.22 1.19
V100, 2^20 10.00 6.67 5.93
V100, 2^24 9.18 7.44 5.74
V100, 2^26 6.73 5.10 4.67

Stable runs at 0.27–0.35× of atomic on Apple and 0.57–0.69× on the V100; reproducible-by-sorting runs at 0.67–0.86×. An 8-bit pass costs 8 ballots per element, plus a counter update and shuffle per round. Workgroup atomics are cheap on both GPUs, especially Apple. This agrees with Ashkiani et al.: ballot ranking pays off only with few buckets. The branch stays as a record.


Generated by Claude Code

@jowens jowens closed this Oct 24, 2026
@jowens
jowens deleted the multisplit-stable-ranking branch November 10, 2026 19:58
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.

2 participants