Skip to content

test(rocke): add a launch-path kernarg-packing A/B profiler - #12168

Draft
AviralGoelAMD wants to merge 1 commit into
developfrom
users/avirgoel/rocke/launch-overhead-profiler
Draft

AviralGoelAMD wants to merge 1 commit into
developfrom
users/avirgoel/rocke/launch-overhead-profiler

Conversation

@AviralGoelAMD

Copy link
Copy Markdown
Contributor

ISSUE ID : AICK-1513

Draft. Split out of #12070 at review request so the quad_perm intrinsic and the kernarg-packer precompile can land without waiting on a measurement-validity issue in this tool.

What it is

benchmark/perf/examples/profile_launch_overhead.py answers a question a packing-only microbenchmark cannot: the precompiled kernarg packer is faster, but what fraction of the complete Python launch path was packing?

A share needs a named denominator. Here it is one call to KernelLauncher.__call__ on its async (fence=False) branch — the production hot path — and nothing else.

Two arms in one process, alternated A/B/A'/B' so drift hits both equally:

arm packer
A pack_args(signature, values) — the pre-precompile behaviour
B compile_packer(signature)

The arm is installed on launcher._packer, so the real library launcher stays in the loop for both arms — nothing is copied into the script that could drift from the library. verify_arms() then confirms from the code path (not from assumption) that each arm holds the object intended and that both produce byte-identical kernargs.

python -m rocke.benchmark.perf.examples.profile_launch_overhead --arch gfx950

Follows the existing profile_gemm_sweep.py + test_profile_gemm_sweep.py convention.

Guards

Each exists because its absence produces a confidently wrong number.

  • fence=False is asserted, via _resolved_fence, not assumed. A fence puts a device sync inside the timed region; GPU execution then swamps the host path and packing's share collapses toward zero.
  • The noise floor is the same-arm repeat spread (A vs A'). A delta is called real only if it exceeds it — otherwise an A/B on a shared node manufactures an effect out of drift.
  • A back-pressure gate marks a run valid: false when enqueue is blocking, since async enqueue is only a host clock while the host outruns the device. in_chunk_stepup times consecutive segments of one undrained chunk; chunk_size_sensitivity compares per-launch cost at two chunk sizes.

Known issue — why this is a draft

chunk_size_sensitivity is a ratio, so it only detects saturation that the small window escapes. Call it with a small large enough to saturate as well and both windows pay the same per-launch penalty, the ratio returns toward 1.0, and the gate accepts a run that is entirely back-pressured.

Deriving small from large (currently large // 8) does not guarantee it sits below the queue depth. A ratio cannot express "both windows are slow" — closing this needs an absolute reference, a measured host-only floor, rather than a second window. Documented in the function's docstring; not fixed here.

A second, smaller one: min-of-reps cut the gate's false-positive rate on a shared node from 4/110 to 2/110 (ratio IQR 0.140 → 0.020), but did not eliminate it. The survivors are stalls that outlast all five repetitions. The failure is conservative — the gate invalidates a good run, it never blesses a back-pressured one.

Status

Runs end to end on gfx950 (MI355X), correctness-gated against a torch fp32 reference. The gate fires decisively on genuine saturation: at the measured queue knee (~16,000 undrained launches, device 25.2 µs/launch vs host floor 8.4 µs) both checks report 1.31 and 1.79 against a 1.15 tolerance.

Unit tests cover the pure helpers with no GPU and no rocke import. The load-bearing cases are the negative ones: an effect smaller than the same-arm drift must be refused, and a single stall must not trip the gate.

ISSUE ID : AICK-1513

Splits the profiler out of the quad_perm PR so the intrinsic and the
kernarg-packer change can land without waiting on a measurement-validity
issue in this tool.

The script answers what a packing-only microbenchmark cannot: packing got
faster, but what FRACTION of the complete Python launch path was it? The
denominator is one call to KernelLauncher.__call__ on its async
fence=False branch. Two arms in one process -- pack_args versus
compile_packer -- alternated A/B/A'/B' so drift hits both equally, with
the real library launcher in the loop for both arms; the arm is installed
on launcher._packer and then verified from the code path, not assumed.

Guards that make the output trustworthy: fence=False is ASSERTED via
_resolved_fence rather than assumed, since a fence puts a device sync
inside the timed region and collapses the share toward zero; the noise
floor is the same-arm repeat spread, and a delta is called real only if
it exceeds it; a back-pressure gate marks a run invalid when enqueue is
blocking, because async enqueue is only a host clock while the host
outruns the device.

Known issue, documented in the code and not fixed here: the
chunk-size-sensitivity check is a RATIO, so a chunk size large enough to
saturate BOTH windows returns a ratio near 1.0 and the gate accepts a
fully back-pressured run. Closing it needs an absolute host-floor
reference rather than a second window.

Tests cover the pure helpers with no GPU and no rocke import, including
the negative cases: an effect smaller than the same-arm drift must be
REFUSED, and one stall must not trip the gate.
@therock-pr-bot

therock-pr-bot Bot commented Sep 16, 2026

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
📝 PR Description ✅ Pass
Forbidden Files ✅ Pass
🧪 Unit Test ✅ Pass
🔎 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

Copy link
Copy Markdown

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

AviralGoelAMD added a commit that referenced this pull request Sep 16, 2026
#12070)

ISSUE ID : AICK-1513

## What this is

PR 1 of a 4-PR stack that splits the GDN gfx950 work (originally one
51-file PR) into reviewable layers. This is the foundation layer: it
adds a new crosslane hardware primitive and a launch-path speedup, with
**no GDN code** and no dependency on the other three PRs.

Stack (bottom → top): **quad_perm (this)** → dispatch.core hoist → GDN
decode → GDN prefill.

## What changed

**1. `quad_perm` — an intra-quad DPP permute intrinsic, added to both
engines.**
`quad_perm(data, [p0,p1,p2,p3])` lets lane `4q+i` read `data` from lane
`4q+perm[i]` on the VALU (via `v_mov_b32_dpp` /
`llvm.amdgcn.update.dpp.i32`), with no LDS crossbar and no `lgkmcnt`
wait. Also adds `warp_shuffle_xor_quad`, a fast path for the two
xor-masks that stay inside a quad: masks 1 and 2 lower to `quad_perm`,
and **any other mask is rejected** — callers needing a wider mask use
`warp_shuffle_xor`, which goes through `ds_swizzle`.

- Python engine: `core/ir.py` (builder + validation),
`core/lower_hip.py`, `core/lower_llvm.py`.
- C++ engine (live): `core/ir/ir_flow.cpp`, `core/ir/core_types.cpp`
(opcode + purity), `core/lower_hip/lower_hip_mma.cpp`,
`core/lower_llvm/crosslane.cpp`, `include/rocke/ir.h`.
- Coverage: Python `tests/test_rocke.py`, C++
`tests/core/future_intrinsic_lowering.cpp`, and cross-engine parity
`tests/instances/parity/target_intrinsics_emit.{c,py}` (identical IR
from both lowerers).

**2. Kernarg-packer precompile — a launch-path speedup for every
kernel.**
`runtime/packing.py` gains `compile_packer(signature)`;
`runtime/launcher.py` compiles the packer once at module-load and calls
it per launch instead of re-deriving the layout each time.
Byte-identical to `pack_args`.

What it removes is **kernarg layout reconstruction** — the
offset/alignment walk, the per-argument type dispatch, and the
format-string assembly. It is *not* a saving on format compilation:
CPython's `struct` module already caches recently used format strings,
so re-packing the same format is a cache lookup, not a recompile.

## Review round 1 — what changed since the first push

Four items were raised by @yraparti and @tenpercent. All four are
addressed.

**1. Lowerers accepted out-of-range `ctrl` (`bffb5fd911f`).** All four
`quad_perm` lowering sites masked the control word with `0xFF` instead
of validating it. `ctrl` packs four two-bit lane selectors (`p0 | p1<<2
| p2<<4 | p3<<6`), so `0..255` is the whole legal range — and masking
silently rewrote malformed IR into a *different, valid* permute: `256`
became `0` (`[0,0,0,0]`, a lane-0 broadcast) and `-1` became `255`
(`[3,3,3,3]`). A wrong reduction then computed wrong numbers instead of
failing.

The builders already validate selectors, but IR reaching a lowerer by
another route (deserialized, rewritten by a pass, hand-built) skips
them. Now rejected in the Python lowerers (`ValueError`) and the C++
ones (`ROCKE_ERR_VALUE`), with the mask dropped so the check cannot be
bypassed. Tests on both sides are **mutation-verified**: with the mask
restored, the Python subtests and the four C++ assertions fail.

**2. Wave-size semantics undocumented (`cfeec0a1315`, `8cea213942f`).**
`quad_perm` makes no wave-size assumption, and both engines now say so:
the control word applies within every four-lane group, and four divides
both 32 and 64, so a lane never addresses outside its own quad. Wave
size changes only the *number* of quads (8 in wave32, 16 in wave64).

Deliberately scoped — wave-size-independent is **not**
architecture-independent. The op still needs DPP-capable hardware; the
useful point is that base-DPP `quad_perm` is available on CDNA where
`dpp_xor`'s RDNA-only `row_xmask` is not. Also recorded: the op has no
lane targeting (the control is broadcast to every quad, row/bank masks
fixed at `15, 15`), so selecting a subset of quads is the caller's job.

A first draft of this docstring cited `dpp_xor` as an op whose partner
lane can leave the wave; `8cea213942f` corrects that. `dpp_xor` caps
`xor_mask` at `1..15` and its partner stays inside a 16-lane row, which
also divides both wave sizes. The accurate counterexample is
`warp_shuffle_xor` at `lane_xor = 32`.

**3. "The dominant Python cost" was unbacked (`786f6c827b0`).** Correct:
a packing-only microbenchmark cannot establish a share of launcher
overhead. Both sites now describe the mechanism instead of a magnitude
-- the packer precomputes the fixed argument layout once per launcher,
so a launch does not rebuild the offset table, re-dispatch on argument
types, or re-assemble the format string. `struct` already caches
recently used format strings, so the saving is that surrounding work
rather than the format compile itself.

The profiler written to measure the share now lives in its own PR
(#12168, draft), and performance claims are deferred until its gate is
fixed: its chunk-size-sensitivity check is a ratio, so a chunk size
large enough to saturate both comparison windows accepts a
back-pressured run.

**4. PR description misdescribed `warp_shuffle_xor_quad`.** It said the
helper "leaves wider masks on `ds_swizzle`", implying a dispatcher. It
is a specialist: masks other than 1 and 2 raise. Corrected in the
What-changed section above.

## Scope note (disclosed)

Item 2 is functionally independent of item 1. It is kept here rather
than split for one concrete reason: its byte-identity test
(`test_compile_packer_matches_pack_args_byte_for_byte`) lives in
`tests/test_rocke.py`, the same file that holds the `quad_perm` tests —
splitting would put two PRs on one test file. Reviewers should weigh the
packer's **global blast radius** (it changes the launch path for every
kernel), not only its decode benefit.

## Why it's safe

- No kernel IR changes — this only adds an opcode and a runtime fast
path.
- `quad_perm` is validated at the builder (selectors ∈ 0..3, i32-only)
**and now at every lowerer** (`ctrl` ∈ 0..255), and is marked pure in
both engines.
- `llvm.amdgcn.update.dpp.i32` is an already-shipped overload (used by
`mov_dpp8`/`ds_swizzle`), not a new intrinsic surface.
- The packer is asserted byte-identical to the existing `pack_args`.

## Why it's first in the stack

GDN decode's in-quad xor-butterfly reduction lowers to `quad_perm` (3
call sites), so this must land before the decode PR. It touches only
`platform/`, so it reviews without any GDN context.

## Verification (run, not planned)

Both engines built and exercised locally on this diff:

- Python, with the C++ extension importable: `pytest tests/test_rocke.py
-k "quad_perm or warp_shuffle_xor_quad or compile_packer"` → **8 passed,
0 skipped** (the cross-engine assertions no longer skip). Full file:
**291 passed, 14 skipped, 43 subtests**.
- C++ engine: `rocke_future_intrinsic_lowering` → **31 case(s) OK**.
- Cross-engine byte-identity: `tools/check_byte_identity.py --only
target_intrinsics` → **GREEN, configs=8, bad=0** — Python and C++ emit
identical `.ll`.
- `test_rocke_ci_static.py` **5 passed**.
- Formatters: `black` clean; `clang-format` applied (local binary is
v20; repo pre-commit pins v18.1.4, so CI may adjust whitespace).

GPU numerical execution of `quad_perm` itself and the full all-family
byte-identity gate were **not** run here.

## Stacking / base

#11807 has merged, and this PR is now based directly on `develop`.
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.

1 participant