Skip to content

Commit bc358d8

Browse files
feat(rocke): quad_perm crosslane intrinsic + kernarg-packer precompile (#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`.
1 parent 9342d1b commit bc358d8

14 files changed

Lines changed: 575 additions & 2 deletions

File tree

dnn-providers/hip-kernel-provider/rocke/platform/cpp/core/ir/core_types.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ static const char* const rocke_opcode_names[ROCKE_OP__COUNT] = {
581581
"tile.ds_swizzle_xor",
582582
"tile.ds_swizzle",
583583
"tile.mov_dpp8",
584+
"tile.quad_perm",
584585
"tile.wave_reduce",
585586
"tile.readlane",
586587
"tile.writelane",
@@ -826,6 +827,7 @@ static const bool rocke_opcode_pure[ROCKE_OP__COUNT] = {
826827
/* ds_swizzle_xor */ true,
827828
/* ds_swizzle */ true,
828829
/* mov_dpp8 */ true,
830+
/* quad_perm */ true,
829831
/* wave_reduce */ true,
830832
/* readlane */ true,
831833
/* writelane */ true,

dnn-providers/hip-kernel-provider/rocke/platform/cpp/core/ir/ir_flow.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,32 @@ rocke_value_t* rocke_b_mov_dpp8(rocke_ir_builder_t* b, rocke_value_t* data, int
182182
return rocke_i_op1(b, ROCKE_OP_TILE_MOV_DPP8, &data, 1, data->type, &attrs, "dpp8");
183183
}
184184

185+
rocke_value_t*
186+
rocke_b_quad_perm(rocke_ir_builder_t* b, rocke_value_t* data, int p0, int p1, int p2, int p3)
187+
{
188+
rocke_attr_map_t attrs;
189+
int ctrl;
190+
if(!rocke_i_live(b))
191+
return NULL;
192+
if(!data)
193+
return (rocke_value_t*)rocke_i_set_err(b, ROCKE_ERR_VALUE, "quad_perm: NULL data");
194+
if(!rocke_flow_is_i32(data->type))
195+
return (rocke_value_t*)rocke_i_set_err(b, ROCKE_ERR_VALUE, "quad_perm requires i32 data");
196+
if(p0 < 0 || p0 > 3 || p1 < 0 || p1 > 3 || p2 < 0 || p2 > 3 || p3 < 0 || p3 > 3)
197+
return (rocke_value_t*)rocke_i_set_err(
198+
b,
199+
ROCKE_ERR_VALUE,
200+
"quad_perm lane selectors must be in 0..3, got [%d,%d,%d,%d]",
201+
p0,
202+
p1,
203+
p2,
204+
p3);
205+
ctrl = p0 | (p1 << 2) | (p2 << 4) | (p3 << 6);
206+
attrs = rocke_i_attrs(b);
207+
rocke_attr_set_int(b, &attrs, "ctrl", (int64_t)ctrl);
208+
return rocke_i_op1(b, ROCKE_OP_TILE_QUAD_PERM, &data, 1, rocke_i32(), &attrs, "qperm");
209+
}
210+
185211
static bool flow_wave_reduce_allowed(const char* reduce_op, const char* ty)
186212
{
187213
if(!reduce_op || !ty)

dnn-providers/hip-kernel-provider/rocke/platform/cpp/core/lower_hip/lower_hip_mma.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,29 @@ static rocke_status_t rocke_h_op_tile_ds_swizzle_xor(rocke_h_lowerer_t* lw, cons
327327
return lw->status;
328328
}
329329

330+
static rocke_status_t rocke_h_op_tile_quad_perm(rocke_h_lowerer_t* lw, const rocke_op_t* op)
331+
{
332+
const rocke_value_t* data = op->operands[0];
333+
const rocke_value_t* r = h_res(op);
334+
int64_t ctrl = 0;
335+
if(!rocke_attr_get_int(&op->attrs, "ctrl", &ctrl))
336+
return rocke_h_fail(lw, ROCKE_ERR_KEY, "tile.quad_perm: missing 'ctrl'");
337+
/* 0..255 is the whole legal range: ctrl packs four two-bit lane
338+
* selectors. Reject instead of masking (see lower_llvm/crosslane.cpp). */
339+
if(ctrl < 0 || ctrl > 255)
340+
return rocke_h_fail(lw,
341+
ROCKE_ERR_VALUE,
342+
"tile.quad_perm: ctrl must be in 0..255, got %lld",
343+
(long long)ctrl);
344+
rocke_h_emitf(lw,
345+
"int %s = __builtin_amdgcn_update_dpp(%s, %s, %lld, 15, 15, 1);",
346+
rocke_h_name(lw, r),
347+
rocke_h_name(lw, data),
348+
rocke_h_name(lw, data),
349+
(long long)ctrl);
350+
return lw->status;
351+
}
352+
330353
/* def _op_tile_mov_dpp(self, op): row_shr/row_shl -> dpp_ctrl, update_dpp. */
331354
static rocke_status_t rocke_h_op_tile_mov_dpp(rocke_h_lowerer_t* lw, const rocke_op_t* op)
332355
{
@@ -1346,6 +1369,7 @@ const rocke_h_handler_entry_t* rocke_h_handlers_mma(void)
13461369
{ROCKE_OP_TILE_DS_BPERMUTE, rocke_h_op_tile_ds_bpermute},
13471370
{ROCKE_OP_TILE_DS_BPERMUTE_B64, rocke_h_op_tile_ds_bpermute_b64},
13481371
{ROCKE_OP_TILE_DS_SWIZZLE_XOR, rocke_h_op_tile_ds_swizzle_xor},
1372+
{ROCKE_OP_TILE_QUAD_PERM, rocke_h_op_tile_quad_perm},
13491373
{ROCKE_OP_TILE_MOV_DPP, rocke_h_op_tile_mov_dpp},
13501374
{ROCKE_OP_TILE_PERMLANE32_SWAP, rocke_h_op_tile_permlane32_swap},
13511375
{ROCKE_OP_TILE_PERM_B32, rocke_h_op_tile_perm_b32},

dnn-providers/hip-kernel-provider/rocke/platform/cpp/core/lower_llvm/crosslane.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,30 @@ static void _op_tile_mov_dpp8(rocke_lower_t* L, const rocke_op_t* op)
325325
(long long)(sel & 0xFFFFFF));
326326
}
327327

328+
static void _op_tile_quad_perm(rocke_lower_t* L, const rocke_op_t* op)
329+
{
330+
const rocke_value_t* data = op->operands[0];
331+
int64_t ctrl = 0;
332+
if(!rocke_attr_get_int(&op->attrs, "ctrl", &ctrl))
333+
rocke_ll_fail(L, ROCKE_ERR_KEY, "tile.quad_perm: missing 'ctrl'");
334+
/* ctrl packs four two-bit lane selectors (p0 | p1<<2 | p2<<4 | p3<<6),
335+
* so 0..255 is the whole legal range. Reject instead of masking: a
336+
* truncated control is a different, silently valid permutation. */
337+
if(ctrl < 0 || ctrl > 255)
338+
rocke_ll_fail(L,
339+
ROCKE_ERR_VALUE,
340+
"tile.quad_perm: ctrl must be in 0..255, got %lld",
341+
(long long)ctrl);
342+
rocke_ll_need(L, "update.dpp.i32");
343+
rocke_ll_emitf(L,
344+
" %s = call i32 @llvm.amdgcn.update.dpp.i32("
345+
"i32 %s, i32 %s, i32 %lld, i32 15, i32 15, i1 true)",
346+
ll_result_name(op),
347+
rocke_ll_operand(L, data),
348+
rocke_ll_operand(L, data),
349+
(long long)ctrl);
350+
}
351+
328352
static void _op_tile_wave_reduce(rocke_lower_t* L, const rocke_op_t* op)
329353
{
330354
const rocke_value_t* v = op->operands[0];
@@ -1050,6 +1074,7 @@ void rocke_ll_register_crosslane(void)
10501074
rocke_ll_set_handler(ROCKE_OP_TILE_DS_SWIZZLE_XOR, _op_tile_ds_swizzle_xor);
10511075
rocke_ll_set_handler(ROCKE_OP_TILE_DS_SWIZZLE, _op_tile_ds_swizzle);
10521076
rocke_ll_set_handler(ROCKE_OP_TILE_MOV_DPP8, _op_tile_mov_dpp8);
1077+
rocke_ll_set_handler(ROCKE_OP_TILE_QUAD_PERM, _op_tile_quad_perm);
10531078
rocke_ll_set_handler(ROCKE_OP_TILE_WAVE_REDUCE, _op_tile_wave_reduce);
10541079
rocke_ll_set_handler(ROCKE_OP_TILE_READLANE, _op_tile_readlane);
10551080
rocke_ll_set_handler(ROCKE_OP_TILE_WRITELANE, _op_tile_writelane);

dnn-providers/hip-kernel-provider/rocke/platform/cpp/include/rocke/ir.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ typedef enum rocke_opcode
360360
ROCKE_OP_TILE_DS_SWIZZLE_XOR,
361361
ROCKE_OP_TILE_DS_SWIZZLE,
362362
ROCKE_OP_TILE_MOV_DPP8,
363+
ROCKE_OP_TILE_QUAD_PERM,
363364
ROCKE_OP_TILE_WAVE_REDUCE,
364365
ROCKE_OP_TILE_READLANE,
365366
ROCKE_OP_TILE_WRITELANE,
@@ -1137,6 +1138,15 @@ rocke_value_t* rocke_b_permlane16(rocke_ir_builder_t* b,
11371138
rocke_value_t* src2,
11381139
bool fi,
11391140
bool bound_ctrl);
1141+
/* quad_perm: each pN selects source lane 0..3 for destination lane N.
1142+
* Wave-size-independent: the control word applies within every four-lane
1143+
* group and four divides both 32 and 64, so a lane never addresses outside
1144+
* its own quad; wave size changes only the number of quads. Requires
1145+
* DPP-capable hardware (base DPP, so CDNA as well as RDNA). No lane
1146+
* targeting -- the control is broadcast to every quad, row/bank masks
1147+
* fixed at 15, 15 by the lowerers. */
1148+
rocke_value_t*
1149+
rocke_b_quad_perm(rocke_ir_builder_t* b, rocke_value_t* data, int p0, int p1, int p2, int p3);
11401150
rocke_value_t* rocke_b_permlane64(rocke_ir_builder_t* b, rocke_value_t* src);
11411151
rocke_value_t* rocke_b_alignbyte(rocke_ir_builder_t* b,
11421152
rocke_value_t* a,

dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/core/ir.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2845,6 +2845,64 @@ def dpp_xor(self, data: Value, xor_mask: int) -> Value:
28452845
result_name_hint="dppx",
28462846
).result
28472847

2848+
def quad_perm(self, data: Value, perm) -> Value:
2849+
"""Intra-quad ``v_mov_b32_dpp`` permutation on the VALU.
2850+
2851+
Lane ``4q + i`` reads ``data`` from lane ``4q + perm[i]``.
2852+
``perm`` is encoded in the low eight bits of the DPP control word.
2853+
2854+
**Wave size.** The mapping is wave-size-independent: the same
2855+
control word applies within every four-lane group, and four
2856+
divides both 32 and 64, so a lane never addresses outside its own
2857+
quad. Wave size changes only the *number* of quads (8 in wave32,
2858+
16 in wave64), never the permutation a quad performs. Contrast
2859+
:meth:`warp_shuffle_xor`, whose ``lane_xor = 32`` partner is a
2860+
real lane in wave64 and does not exist in wave32.
2861+
2862+
That is a property of the quad, not a claim about every target:
2863+
the op still requires DPP-capable hardware. Base-DPP
2864+
``quad_perm`` is available on CDNA, where the RDNA-only
2865+
``row_xmask`` of :meth:`dpp_xor` is not.
2866+
2867+
The op carries no lane targeting -- the control word is broadcast
2868+
to every quad in the wave, with row and bank masks fixed at
2869+
``15, 15`` (all enabled) by the lowerers. Selecting a subset of
2870+
quads is the caller's job.
2871+
"""
2872+
perm = list(perm)
2873+
if len(perm) != 4 or any(not (0 <= p <= 3) for p in perm):
2874+
raise ValueError(f"quad_perm perm must be 4 values in 0..3, got {perm}")
2875+
if data.type.name != "i32":
2876+
raise ValueError("quad_perm requires i32 data")
2877+
ctrl = perm[0] | (perm[1] << 2) | (perm[2] << 4) | (perm[3] << 6)
2878+
return self._op(
2879+
"tile.quad_perm",
2880+
[data],
2881+
[I32],
2882+
attrs={"ctrl": int(ctrl)},
2883+
result_name_hint="qperm",
2884+
).result
2885+
2886+
def warp_shuffle_xor_quad(self, v: Value, xor_mask: int) -> Value:
2887+
"""XOR shuffle within a four-lane quad.
2888+
2889+
Masks 1 and 2 stay inside the quad and use :meth:`quad_perm`. Larger
2890+
masks require :meth:`warp_shuffle_xor`, which uses ``ds_swizzle``.
2891+
"""
2892+
if xor_mask == 1:
2893+
perm = [1, 0, 3, 2]
2894+
elif xor_mask == 2:
2895+
perm = [2, 3, 0, 1]
2896+
else:
2897+
raise ValueError(
2898+
f"warp_shuffle_xor_quad supports xor_mask 1 or 2, got {xor_mask}"
2899+
)
2900+
if v.type.name == "f32":
2901+
return self.bitcast(self.quad_perm(self.bitcast(v, I32), perm), F32)
2902+
if v.type.name == "i32":
2903+
return self.quad_perm(v, perm)
2904+
raise ValueError(f"warp_shuffle_xor_quad: unsupported type {v.type.name}")
2905+
28482906
def ds_bpermute_b64(self, addr: Value, data: Value) -> Value:
28492907
"""Packed 64-bit ``ds_bpermute`` — single LDS op for paired
28502908
``(val, idx)`` cross-lane shuffles (gfx9+).
@@ -4477,6 +4535,7 @@ def scf_if_else(self, cond: Value):
44774535
"tile.ds_swizzle_xor",
44784536
"tile.ds_swizzle",
44794537
"tile.mov_dpp8",
4538+
"tile.quad_perm",
44804539
"tile.wave_reduce",
44814540
"tile.readlane",
44824541
"tile.writelane",

dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/core/lower_hip.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,22 @@ def _op_tile_dpp_xor(self, op: Op) -> None:
19711971
f"{_name(data)}, {_name(data)}, {dpp_ctrl}, 15, 15, 1);"
19721972
)
19731973

1974+
def _op_tile_quad_perm(self, op: Op) -> None:
1975+
"""Lower an eight-bit DPP quad-permute control word.
1976+
1977+
See :meth:`_op_tile_quad_perm` in ``lower_llvm.py``: ``ctrl``
1978+
packs four two-bit lane selectors, so ``0..255`` is the whole
1979+
legal range and out-of-range values are malformed IR.
1980+
"""
1981+
(data,) = op.operands
1982+
ctrl = int(op.attrs["ctrl"])
1983+
if not 0 <= ctrl <= 255:
1984+
raise ValueError(f"tile.quad_perm: ctrl must be in 0..255, got {ctrl}")
1985+
self._emit(
1986+
f"int {_name(op.result)} = __builtin_amdgcn_update_dpp("
1987+
f"{_name(data)}, {_name(data)}, {ctrl}, 15, 15, 1);"
1988+
)
1989+
19741990
def _op_tile_ds_swizzle_xor(self, op: Op) -> None:
19751991
"""``ds_swizzle_b32`` XOR butterfly via SWAP-mode encoding.
19761992

dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/core/lower_llvm.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3800,6 +3800,26 @@ def _op_tile_dpp_xor(self, op: Op) -> None:
38003800
f"i32 {dpp_ctrl}, i32 15, i32 15, i1 true)"
38013801
)
38023802

3803+
def _op_tile_quad_perm(self, op: Op) -> None:
3804+
"""Lower an eight-bit DPP quad-permute control word.
3805+
3806+
``ctrl`` packs four two-bit lane selectors
3807+
(``p0 | p1 << 2 | p2 << 4 | p3 << 6``), so every value in
3808+
``0..255`` is legal and anything outside it is malformed IR.
3809+
Reject rather than mask: truncation would turn an out-of-range
3810+
control into a different, silently valid permutation.
3811+
"""
3812+
(data,) = op.operands
3813+
self._need("update.dpp.i32")
3814+
ctrl = int(op.attrs["ctrl"])
3815+
if not 0 <= ctrl <= 255:
3816+
raise ValueError(f"tile.quad_perm: ctrl must be in 0..255, got {ctrl}")
3817+
self._current().emit(
3818+
f" {op.result.name} = call i32 @llvm.amdgcn.update.dpp.i32("
3819+
f"i32 {self._operand(data)}, i32 {self._operand(data)}, "
3820+
f"i32 {ctrl}, i32 15, i32 15, i1 true)"
3821+
)
3822+
38033823
def _op_tile_ds_swizzle_xor(self, op: Op) -> None:
38043824
"""``ds_swizzle_b32`` with XOR butterfly via SWAP-mode encoding.
38053825

dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/runtime/launcher.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
from typing import Any, Callable, Dict, Iterator, Mapping, Optional, Sequence, Tuple
9696

9797
from .hip_module import Runtime
98-
from .packing import pack_args
98+
from .packing import compile_packer
9999
from .torch_interop import resolve_stream
100100

101101
__all__ = [
@@ -420,6 +420,14 @@ def __init__(
420420
rt = _runtime()
421421
self._module = rt.load_module(hsaco)
422422
self._fn = self._module.get_function(kernel_name)
423+
# Precompiled hot-path kernarg packer (signature is immutable for
424+
# the launcher's lifetime). Byte-identical to ``pack_args``: it
425+
# precomputes the fixed argument layout once here so a launch does
426+
# not rebuild the offset table, re-dispatch on argument types, or
427+
# re-assemble the format string. Note ``struct`` already caches
428+
# recently used formats, so the saving is that surrounding work,
429+
# not the format compile itself.
430+
self._packer = compile_packer(self._signature)
423431

424432
@property
425433
def kernel_name(self) -> str:
@@ -436,7 +444,7 @@ def __call__(
436444
config: LaunchConfig,
437445
) -> LaunchSummary:
438446
rt = _runtime()
439-
args = pack_args(self._signature, values)
447+
args = self._packer(values)
440448
stream = resolve_stream(config.stream)
441449
fence = _resolved_fence(config.fence)
442450

dnn-providers/hip-kernel-provider/rocke/platform/python/rocke/runtime/packing.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,67 @@ def pack_args(
8181
return struct.pack("".join(fmt_parts), *packed)
8282

8383

84+
def compile_packer(signature: Sequence[Mapping[str, Any]]):
85+
"""Precompile a hot-path kernarg packer for a fixed ``signature``.
86+
87+
Returns ``packer(values) -> bytes`` that is **byte-identical** to
88+
:func:`pack_args` for the same signature, but hoists the invariant
89+
work (type dispatch, alignment padding, format-string build, and
90+
``struct`` format compile) out of the per-call path, leaving only
91+
the per-call work that genuinely varies: reading each value and
92+
coercing it by kind. ``struct`` already caches recently used
93+
formats, so the saving is the layout reconstruction around that
94+
compile rather than the compile itself. The signature is immutable
95+
per kernel, so a launcher can build this once at construction and
96+
reuse it; the effect is proportionally largest where per-launch
97+
host work is smallest.
98+
"""
99+
_TY_FMT: Mapping[str, Tuple[str, int, int]] = {
100+
"i32": ("i", 4, 4),
101+
"i64": ("q", 8, 8),
102+
"f32": ("f", 4, 4),
103+
}
104+
fmt_parts: List[str] = ["<"]
105+
# plan: (name, kind) with kind in {"ptr", "f32", "int"} -- the only
106+
# per-call work is reading values[name] and coercing by kind.
107+
plan: List[Tuple[str, str]] = []
108+
offset = 0
109+
for arg in signature:
110+
name = str(arg["name"])
111+
ty = str(arg["type"])
112+
if ty.startswith("ptr<"):
113+
fmt_char, size, align, kind = "Q", 8, 8, "ptr"
114+
elif ty in _TY_FMT:
115+
fmt_char, size, align = _TY_FMT[ty]
116+
kind = "f32" if ty == "f32" else "int"
117+
else:
118+
raise ValueError(f"unsupported kernel arg type {ty!r} for {name}")
119+
pad = (-offset) % align
120+
if pad:
121+
fmt_parts.append(f"{pad}x")
122+
offset += pad
123+
fmt_parts.append(fmt_char)
124+
plan.append((name, kind))
125+
offset += size
126+
packer_struct = struct.Struct("".join(fmt_parts))
127+
128+
def packer(values: Mapping[str, Any]) -> bytes:
129+
out: List[Any] = []
130+
for name, kind in plan:
131+
if name not in values:
132+
raise KeyError(f"missing kernel arg {name!r}")
133+
v = values[name]
134+
if kind == "ptr":
135+
out.append(_as_ptr(v))
136+
elif kind == "f32":
137+
out.append(float(v))
138+
else:
139+
out.append(int(v))
140+
return packer_struct.pack(*out)
141+
142+
return packer
143+
144+
84145
def pack_args_kernelparams(
85146
signature: Sequence[Mapping[str, Any]], values: Mapping[str, Any]
86147
) -> List[Any]:

0 commit comments

Comments
 (0)