[AMDGPU] Add cross-platform min_blocks_per_cu occupancy hint - #892
[AMDGPU] Add cross-platform min_blocks_per_cu occupancy hint#892paveltc wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 487002d22f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| constexpr int kSimdsPerCu = 4; | ||
| int waves_per_block = (block_dim + kWavefrontSize - 1) / kWavefrontSize; | ||
| int min_waves_per_eu = | ||
| (min_blocks_per_cu * waves_per_block + kSimdsPerCu - 1) / kSimdsPerCu; |
There was a problem hiding this comment.
Derive the SIMD count for supported RDNA targets
On AMD RDNA devices, which this repository explicitly supports in quadrants/runtime/amdgpu/jit_amdgpu.cpp:68-73 and llvm_context.cpp:583-588, a CU has two SIMD execution units rather than the four assumed here. For example, a 256-thread wave64 block with min_blocks_per_cu=4 is lowered to amdgpu-waves-per-eu=4 instead of the required 8, so register allocation may permit only half the requested blocks per CU. Select the execution-unit count from the actual mcpu/AMDGPU subtarget rather than fixing it to the CDNA3 value.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — this is a real CDNA-vs-RDNA asymmetry. The 4 SIMDs/CU is CDNA3-specific; RDNA has 2/CU, so as written the hint under-provisions occupancy on RDNA (~half the requested blocks/CU).
Two reasons I'm deferring the mcpu-derived fix to a follow-up rather than taking it here:
- It's opt-in and never affects correctness —
amdgpu-waves-per-euonly bounds the register allocator, so a low value just means less aggressive occupancy, not wrong results. Default (min_blocks_per_cu=None) is a no-op on every target. - RDNA is genuinely untested for this.
jit_amdgpu.cppforces wave64 on RDNA hosts (+wavefrontsize64), so the correct waves-per-EU there depends on LLVM's occupancy model in forced-wave64 mode, which I can't confirm without RDNA hardware. This PR is tuned/validated only on CDNA3 (MI308X) and CUDA, so I'd rather not swap a CDNA-correct constant for an unvalidated RDNA formula in the same PR.
I've pushed a comment clarifying that 4 SIMDs/CU is CDNA-specific and that mcpu-based SIMD selection is deferred to a follow-up with RDNA hardware to validate the wave64-on-RDNA occupancy model. Thanks!
Adds an optional @qd.kernel(min_blocks_per_cu=N) parameter: a portable,
per-kernel occupancy hint requesting at least N thread-blocks resident per
compute unit. It lowers to the native occupancy control on each backend and
is ignored where the concept does not apply, so the same kernel stays
portable:
- AMDGPU: converted to amdgpu-waves-per-eu via
ceil(min_blocks_per_cu * ceil(block_dim/64) / 4) (wave64, 4 SIMDs/CU).
- CUDA: mapped to the minctasm launch bound; this also parameterizes the
previously hardcoded default of 2 (unset preserves that default).
- CPU/Metal: ignored (no occupancy concept).
The value is validated at decoration time (positive int or None), applied to
both primal and adjoint kernels, and participates in both the C++ offline
cache key and the Python fastcache key so differing values never collide or
reuse stale code.
Co-authored-by: Cursor <cursoragent@cursor.com>
Covers decoration-time validation (accepts None / positive ints; rejects 0, negatives, bool, and non-ints with QuadrantsSyntaxError), fast-cache key participation (distinct min_blocks_per_cu values yield distinct keys, equal values are stable), and a GPU-gated smoke test that the hint compiles and runs without altering results on CUDA / AMDGPU. Co-authored-by: Cursor <cursoragent@cursor.com>
The blocks/CU -> waves/EU conversion hardcodes 4 SIMDs/CU, which is CDNA-specific: RDNA has 2 SIMDs/CU, so the hint under-provisions occupancy there. Since jit_amdgpu.cpp forces wave64 on RDNA hosts too, document that this path is unvalidated (PR is tuned/tested only on CDNA3 and CUDA) and defer mcpu-derived SIMD selection to a follow-up with RDNA hardware. Comment-only; the hint is opt-in and never affects correctness. Co-authored-by: Cursor <cursoragent@cursor.com>
Genesis-Embodied-AI#705 split fastcache into L1 (source+config) / L2 (narrow args). Occupancy changes codegen without changing arguments, so it belongs in L1. Update the cache-key tests to call make_source_config_key instead of the removed create_cache_key. Co-authored-by: Cursor <cursoragent@cursor.com>
63a7213 to
e6abc72
Compare
Pin amdgpu-flat-work-group-size to the launched block dim, preserve waves-per-eu across kernel reconstruction, and always_inline the range-for dispatcher so the portable occupancy hint reaches register allocation. API impact: none Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Adds an optional, portable per-kernel occupancy hint:
@qd.kernel(min_blocks_per_cu=N). It requests that the scheduler keep atleast N thread-blocks resident per compute unit, and lowers to each backend's
native occupancy control.
API impact
Adds one new standard
@qd.kernelparameter (min_blocks_per_cu: positiveint or
None; defaultNone). It is cross-platform by construction:minctasmlaunch bound, and parameterizesthe previously hardcoded default of
2(unset preserves that default).min_blocks_per_cu(blocks per computeunit) — never
amd_*.Noneis a true no-op, so existing code is unaffected.Lowering
minctasmlaunch boundamdgpu-waves-per-euceil(min_blocks_per_cu * ceil(block_dim/64) / 4)(wave64, 4 SIMDs/CU)Behavior & caching
None).key, so kernels differing only in
min_blocks_per_cunever collide orreuse stale code.
Due diligence
Experiments have been done to implement this without a user-facing knob.
The only real way to do it would be to implement a tuning loop where different min_blocks_per_cu are set and performance tests are run on each value set.
This is borne out by measuring the same hint on two platforms (details in
the two sections below): the optimal setting differs by platform and
hardware. On AMD MI308X the CG workloads want
min_blocks_per_cu=4(+1–3%), whereas on an NVIDIA B300 the same scenes — running 10–70× faster
and no longer latency-bound — are best at the default and slightly regress at
higher occupancy. There is therefore no single static default (or backend
heuristic) that is correct everywhere; the only automatic alternative is a
per-workload autotuning loop. That is precisely why the feature is an
opt-in per-kernel hint (default
None= a no-op that preserves currentbehavior on every backend) rather than a forced default.
Testing —
tests/benchmarks/test_rigid.py(AMD MI308X)Validated with the Genesis rigid-body benchmark suite (
test_rigid.py),runtime_fpsmetric, on the memory-latency-bound CG constraint-solverpath. Three arms of the same benchmark differ only by the hint applied to the
rigid-solver kernels (
kernel_step_1,kernel_step_2inrigid_solver.py;func_solve_init,_kernel_solve_monolithinconstraint/solver.py), each gated on an env var so an arm is selected purelyby the environment of a fresh process:
min_blocks_per_cu=2min_blocks_per_cu=4Arms are interleaved per round across 5 rounds to cancel thermal/clock drift;
deltas are the geomean of per-round paired ratios vs the
offarm.rounds (r3–5) are the true signal. No regression once cold start is excluded.
indistinguishable from noise. A low-DOF arm at ~1.79M fps is not
latency-bound, so extra occupancy hides nothing → flat, as expected.
Bottom line: a small, consistent, workload-dependent speedup on the
latency-bound CG path, neutral where the workload isn't latency-bound, and
no regression observed anywhere. Because the benefit is workload-dependent
with no static rule to decide it automatically, the hint is an opt-in knob
rather than a default.
Environment (AMD)
feat/cross-platform-occupancy(base96c594499), LLVM 22.1.0genesis-worldmain @eeb91f82.10.0+rocm7.0, Python 3.10.12 · run 2026-08-25Cross-platform validation — CUDA (NVIDIA B300)
Same PR, exercised on CUDA to confirm the hint is portable (not AMD-only).
Functional — pass. PTX launch bounds (
print_kernel_asm):.minnctapersm 2.minnctapersm 2.minnctapersm 4.minnctapersm 8change on CUDA unless the hint is set.
0,-1,True,1.5(QuadrantsSyntaxError).=4vs=8produce distinct fastcache keys.Performance. Same CG protocol (5 interleaved rounds, 20 s warmup / 8 s
record, geomean vs
off=minctasm 2):On B300 these CG scenes run ~10–70× the MI308X fps and are not
latency-bound, so extra occupancy doesn't help:
=4is within noise,=8is a small, consistent loss (register pressure), franka is flat. Because the
default is a no-op (
unset ≡ minctasm 2), there is no CUDA regression fornon-opted-in code.
Takeaway. The lowering is correct on both platforms; the occupancy
sweet-spot is workload- and hardware-dependent (helps AMD MI308X CG at
4;neutral/slightly-worse on B300), which is exactly why this is an opt-in
per-kernel hint rather than a forced default.
Environment (CUDA)
487002dgenesis-world@eeb91f8(four-kernelGS_MIN_BLOCKS_PER_CUpatch)2.13.0+cu130