[AMDGPU] Feat: per-kernel LLVM function attributes via @qd.kernel(fn_attrs=...)#774
[AMDGPU] Feat: per-kernel LLVM function attributes via @qd.kernel(fn_attrs=...)#774paveltc wants to merge 4 commits into
Conversation
…I#11) Lets users override AMDGPU codegen attributes per kernel without editing the JIT pipeline. Attributes must be pre-registered in quadrants/program/fn_attrs_registry.h; unknown backend or attribute names raise QuadrantsSyntaxError at decoration time. Currently registered: amdgpu-max-num-workgroups, amdgpu-agpr-alloc, amdgpu-waves-per-eu, amdgpu-flat-work-group-size. Examples: ```python @qd.kernel(fn_attrs={"amdgpu": {"amdgpu-max-num-workgroups": "128,1,1"}}) def k(...): ... @qd.kernel(fn_attrs={"amdgpu": {"amdgpu-waves-per-eu": "1,2"}}) def k(...): ... ``` Plumbed Python decorator -> Kernel.fn_attrs -> set_fn_attrs pybind -> codegen_llvm.cpp addFnAttr -> jit_amdgpu.cpp (defaults gated by hasFnAttribute so user values win). Included in both fastcache and frontend offline cache keys so changing fn_attrs forces a rebuild.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f44102799
ℹ️ 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".
| F.addFnAttr("unsafe-fp-math", "true"); | ||
| F.addFnAttr("no-signed-zeros-fp-math", "true"); |
There was a problem hiding this comment.
Honor fast_math=False before forcing unsafe AMDGPU math
In AMDGPU runs configured with fast_math=False, these unconditional attributes still enable unsafe FP transformations and no-signed-zero handling on every function; CompileConfig::fast_math is used just below to set strict target options, and docs/source/user_guide/init_options.md documents fast_math as controlling IEEE-relaxed rewrites. This means tests/users that disable fast math for numerical stability on AMDGPU can still get contracted or reordered arithmetic.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 9cb65ab. unsafe-fp-math and no-signed-zeros-fp-math are now gated inside if (this->config_.fast_math) in jit_amdgpu.cpp, matching the TargetOptions block below and preserving IEEE semantics when qd.init(fast_math=False) is set.
| int clamped = std::max(block_dim, kAmdgpuWavefrontSize); | ||
| std::string size_str = std::to_string(clamped) + "," + std::to_string(clamped); | ||
| func->addFnAttr("amdgpu-flat-work-group-size", size_str); |
There was a problem hiding this comment.
Keep flat-work-group-size within the launched block size
For AMDGPU tasks whose actual block size is below 64, e.g. emit_amdgpu_gc() sets the reinit_lists task to block_dim = 1 and the launcher dispatches task.block_dim, this advertises amdgpu-flat-work-group-size as 64,64. The existing LLVM-context comment says this attribute is the min/max workgroup sizes specified at dispatch, so the IR metadata is false for those kernels and can drive invalid occupancy or optimization assumptions.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 9cb65ab. The wavefront-size clamping is removed; mark_function_as_amdgpu_kernel now uses block_dim verbatim (block_dim,block_dim) so tasks with block_dim=1 correctly produce amdgpu-flat-work-group-size="1,1" instead of "64,64".
| graph: bool = False, | ||
| checkpoints: bool = False, | ||
| cuda_graph: bool = False, | ||
| fn_attrs: dict[str, dict[str, str]] | None = None, |
There was a problem hiding this comment.
Add docs for the new fn_attrs kernel API
This exposes fn_attrs as a new public @qd.kernel usage and allows backend-specific values, but the diff contains no docs/ changes. The repository AGENTS.md requires docs updates for public API or usage changes; without them, end users have no guidance for supported backends/attribute names or how these attributes interact with caching.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit 9cb65ab. Added a 'Per-kernel LLVM function attributes' section to docs/source/user_guide/optimization_passes.md covering usage, the registered AMDGPU attribute table, and cache interaction.
…s PR Four fixes from Codex review of PR Genesis-Embodied-AI#774: 1. Gate unsafe-fp-math on config_.fast_math Previously unconditional in jit_amdgpu.cpp; now matches the TargetOptions block below it. qd.init(fast_math=False) now correctly preserves IEEE semantics on AMDGPU. 2. Fix block_dim clamping in mark_function_as_amdgpu_kernel Prior impl clamped both min and max to max(block_dim, 64), so block_dim=1 produced "64,64" instead of "1,1". Now uses block_dim as-is so the IR accurately reflects the actual dispatch size. 3. Make cuda_graph a deprecated alias for graph cuda_graph=True was silently ignored because the launch path only checks use_graph. Now _kernel_impl treats cuda_graph=True as graph=True with a DeprecationWarning, and removes the now-unused use_cuda_graph field from Kernel. 4. Add fn_attrs docs to optimization_passes.md Per AGENTS.md, public API changes need docs/ updates. Adds a "Per-kernel LLVM function attributes" section documenting fn_attrs, the supported amdgpu attributes, and cache interaction. Co-authored-by: Cursor <cursoragent@cursor.com>
|
|
||
| ## Per-kernel LLVM function attributes (`fn_attrs`) | ||
|
|
||
| For fine-grained control over AMDGPU codegen on a per-kernel basis, `@qd.kernel` accepts an optional `fn_attrs` argument. This lets you pass LLVM function attributes directly to the backend JIT without editing the JIT pipeline. |
There was a problem hiding this comment.
Platform-specific configuration conflicts with the ideal that Quadrants code will run unmodified on all platforms.
The standard approach is to set appropriate defaults - or use appropriate heuristics - within each platform-specific pipeline.
There was a problem hiding this comment.
Thanks — I agree with the principle, and I think this PR is consistent with it. The sensible AMDGPU defaults already live in the backend pipeline (jit_amdgpu.cpp, each gated by hasFnAttribute); fn_attrs is just an optional override on top for when a user knows better. Portable code never needs it.
Portability is also preserved: attributes are only applied under arch == Arch::amdgpu (codegen_llvm.cpp:3244), so @qd.kernel(fn_attrs={"amdgpu": {...}}) still runs unmodified on CPU/CUDA/Metal — the block is simply ignored there. It's namespaced by backend for exactly that reason, much like CUDA __launch_bounds__ or Triton num_warps: an optional hint that only tunes the backend that understands it.
To land the right scope, which is your concern:
- the concept of any user-facing backend override, or
- specifically exposing it on
@qd.kernel(...)?
If (1), I'll narrow this PR to just the backend-default infrastructure and drop the public fn_attrs= for now. If (2), I'll move it behind a clearly-marked advanced API. Just say which unblocks the merge.
There was a problem hiding this comment.
My concern is anything platform-specific in the public API.
What I want to avoid is code that runs on one platform, and not on other; or on all platforms except one.
This includes optimizations, ideally. Code should ideally run optimally on each platform without the user needing platform-specific knowledge.
There was a problem hiding this comment.
Agreed as the default, and it holds for the common case. For peak perf I don't think it's attainable, though — no production GPU stack manages it. CUDA and HIP ship launch_bounds precisely so the user can state an occupancy/register tradeoff the compiler can't infer; Triton has num_warps/num_stages/@autotune; Halide/TVM make the schedule explicit. The vendors ship these knobs because the "sufficiently smart compiler" doesn't exist for this.
On the "AMD-only" objection: I'd argue that's expected, not a defect. Tuning is vendor-owned — AMD owns the AMD backend and its lowering and can't be expected to implement the NVIDIA/Intel/Apple equivalents; each vendor adds their own. That's how the ecosystem is already structured (torch.backends.cuda/.cudnn/.mps, LLVM's namespaced amdgpu-/nvvm. attributes).
And crucially, the current design already answers the "runs on one platform, breaks on another" worry:
-
It's namespaced by backend — fn_attrs={"amdgpu": {...}}, applied only under arch == Arch::amdgpu (codegen_llvm.cpp:3244). @qd.kernel(fn_attrs={"amdgpu": {...}}) runs unmodified on CPU/CUDA/Metal; the block is just ignored. Portable code is unaffected; only the backend that understands the hint tunes on it — same model as launch_bounds.
-
It's registry-validated, not an open escape hatch — _validate_fn_attrs rejects unknown backends and unknown attributes against get_fn_attrs_registry(), so the surface is curated and enumerable rather than "any LLVM attribute goes."
So it's an optional, explicitly-scoped, validated per-backend hint — opt-in, ignored elsewhere, and owned by the vendor whose backend it tunes. That seems to satisfy the underlying concern (no silent cross-platform breakage, no unbounded surface) while accepting that vendor-specific tuning is a legitimate and necessary thing to expose.
There was a problem hiding this comment.
Interesting. Currently, in genesis-world, we are not - I think - doing such tuning, for either CPU or CUDA. Perhaps we should. Or perhaps we are doing so implicitly.
I would much prefer to ground design and needs on real-world needs, ideally.
Do you see anywhere in genesis-world where we would be able to improve our performance, concretely, on our rigid benchmarks https://github.com/Genesis-Embodied-AI/genesis-world/blob/main/tests/benchmarks/test_rigid.py , for CUDA and AMD, by doing such tuning? And crucially, do you see anywhere where such tuning would use different numbers for each of CUDA and AMD?
This would give a concrete example to inspect and discuss, rather than discussing in the abstract.
(You could also provide a non genesis-world example, but, if it's about as much work for you to use genesis-world vs some other example, genesis-world will be a lot easier for me personally to understand and gain intuition and insight from).
There was a problem hiding this comment.
Benchmark: amdgpu-waves-per-eu via this PR's per-kernel fn_attrs
I benchmarked the knob this PR exposes on a Genesis workload — the anymal_random
benchmark in tests/benchmarks/test_rigid.py, 30,000 parallel envs, MI308X GPU.
I applied amdgpu-waves-per-eu="2,4" to the rigid-body solver kernels via
@qd.kernel(fn_attrs={"amdgpu": {"amdgpu-waves-per-eu": "2,4"}}) and compared against the
same kernels with no attribute (compiler default occupancy):
| Workload | speedup vs default | consistency |
|---|---|---|
anymal_random (30k envs, GPU) |
+48% (geomean) | faster in 5/5 rounds |
This node runs the CG constraint solver (the parametrization passes solver=None). Each
round runs default and tuned back-to-back and takes the ratio within the round, averaged
with a geometric mean (30 s warmup + 30 s record, 5 rounds).
What was modified: the feature under test is this PR's fn_attrs= API in Quadrants. To
exercise it, the only change on the Genesis side is passing fn_attrs=... on the
rigid-solver kernel decorators — the anymal_random workload and the runtime_fps metric
are the stock upstream test_rigid.py. The 5-round interleaved averaging is an external
wrapper around repeated stock runs (to cancel GPU clock drift); the longer warmup/record
windows are an optional tweak and not required to see the effect.
amdgpu-waves-per-eu directly tells the register allocator the target occupancy
(resident wavefronts per EU), trading latency-hiding against registers-per-wave. This is
a more direct handle than __launch_bounds__ (which HIP and CUDA already expose):
__launch_bounds__ constrains occupancy only indirectly, as a side effect of a block-size
cap and an optional min-blocks hint, whereas amdgpu-waves-per-eu sets the occupancy
target itself. That LLVM attribute isn't otherwise reachable from the JIT kernel path —
this PR is what makes it settable per kernel. The optimal point is kernel- and
workload-specific, so per-kernel granularity is the right level rather than a single
global flag, and the results show it has first-order performance impact on a real, public
workload — so exposing it is clearly worthwhile.
Reproduction
Requirements: Quadrants built from this PR, an AMD gfx942 GPU (MI3xx), and a Genesis build. test_rigid.py is run unmodified — the only source edit is adding the attribute to the rigid-solver kernel decorators (that's the feature under test).
Step 1. Add fn_attrs={"amdgpu": {"amdgpu-waves-per-eu": "2,4"}} to these @qd.kernel(...) decorators — kernel_step_1 and kernel_step_2 in genesis/engine/solvers/rigid/rigid_solver.py, and func_solve_init and _kernel_solve_monolith in genesis/engine/solvers/rigid/constraint/solver.py. For example, @qd.kernel(fastcache=True) becomes @qd.kernel(fastcache=True, fn_attrs={"amdgpu": {"amdgpu-waves-per-eu": "2,4"}}).
Step 2. Run the stock benchmark node once with the edit (tuned) and once without it (baseline):
python -m pytest -m benchmarks --backend gpu "tests/benchmarks/test_rigid.py::test_speed[anymal_random-None-None-30000-gpu]"
Step 3. Compare runtime_fps. A single run of each already shows the gain; averaging a few back-to-back rounds just tightens it against GPU clock noise.
hughperkins
left a comment
There was a problem hiding this comment.
Platform-specific configuration conflicts with the ideal that Quadrants code will run unmodified on all platforms.
The standard approach is to set appropriate defaults - or use appropriate heuristics - within each platform-specific pipeline.
cuda_graph was never a public @qd.kernel parameter (the feature is `graph`), so introducing it only to deprecate it in favor of `graph` made no sense. It slipped in during the merge that combined the graph/checkpoints work with this PR's fn_attrs. Removes the parameter and its deprecation shim so this PR only touches fn_attrs. Co-authored-by: Cursor <cursoragent@cursor.com>
get_fn_attrs_registry() returns unordered_map<string, unordered_set<string>>, but export.h only pulls in the std::map caster. Without the unordered_map/ unordered_set casters the binding raised TypeError at runtime, so every @qd.kernel(fn_attrs=...) call failed and the fn_attrs validation tests could never pass in a real build. Adds the two caster includes to export_lang.cpp. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Backport of AMD-Ecosystem#11 onto the upstream
mainbranch.Lets users override AMDGPU codegen attributes per kernel without editing
the JIT pipeline. Attributes must be pre-registered in
quadrants/program/fn_attrs_registry.h; unknown backend or attribute namesraise
QuadrantsSyntaxErrorat decoration time.Currently registered attributes (
amdgpubackend):amdgpu-max-num-workgroupsamdgpu-agpr-allocamdgpu-waves-per-euamdgpu-flat-work-group-sizeamdgpu-sched-strategyExample usage:
```python
@qd.kernel(fn_attrs={"amdgpu": {"amdgpu-max-num-workgroups": "128,1,1"}})
def k(...): ...
@qd.kernel(fn_attrs={"amdgpu": {"amdgpu-waves-per-eu": "1,2"}})
def k(...): ...
```
Changes
quadrants/program/fn_attrs_registry.h(new): central registry of allowed backend/attr pairsquadrants/program/kernel.h: addsfn_attrsfield toKernelquadrants/python/export_lang.cpp: exposesset_fn_attrs+get_fn_attrs_registrypybindingsquadrants/codegen/llvm/codegen_llvm.cpp: applies per-kernel fn_attrs viaaddFnAttrat codegenquadrants/analysis/offline_cache_util.cpp: includes fn_attrs in the offline cache key (deterministic serialisation)quadrants/runtime/amdgpu/jit_amdgpu.cpp: default AMDGPU attributes (unsafe-fp-math,amdgpu-waves-per-eu,amdgpu-ieee,amdgpu-dx10-clamp, flat-work-group-size inheritance) gated byhasFnAttributeso user-supplied values always winquadrants/runtime/llvm/llvm_context.{h,cpp}:mark_function_as_amdgpu_kernelgains optionalblock_dimparameter to setamdgpu-flat-work-group-sizeat codegen timekernel.py,kernel_impl.py,src_hasher.py):fn_attrsplumbed through@qd.kernel()decorator, validated at decoration time, included in fastcache keytests/python/test_fn_attrs.py(new): validation, cache-key differentiation, and AMDGPU end-to-end IR plumbing testsMerge notes
Backported on top of commit
2e2b43b40(same base as PR #1 and PR #2). Conflicts resolved by combining HEAD'sgraph/checkpoints/use_graph/use_checkpointsadditions with this commit'sfn_attrsadditions — all parameters coexist.Test results
Full suite run in Docker image
quadrants:amd-upstream-pr3withQD_AMDGPU_FORCE_PERMLANE64_FALLBACK=1:All 21 failures are pre-existing baseline flaky tests in
test_simt.py::test_block_reduceandtest_algorithms.py::test_reduce_composition— identical failure set as the unpatchedamd-upstreambaseline. Zero new regressions.tests/python/test_fn_attrs.py(all 5 new tests) passed.Co-authored-by: kevinjosephamd kevin.joseph@amd.com
Made with Cursor