Skip to content

feat(ck-tile): preshuffle GEMM TE to dispatcher bridge#9307

Open
ozturkosu wants to merge 12 commits into
developfrom
users/muozturk/ck/bridge_gemm_preshuffle
Open

feat(ck-tile): preshuffle GEMM TE to dispatcher bridge#9307
ozturkosu wants to merge 12 commits into
developfrom
users/muozturk/ck/bridge_gemm_preshuffle

Conversation

@ozturkosu

@ozturkosu ozturkosu commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

ISSUE ID: #8997

Motivation

The TileEngine → Dispatcher bridge had no path for the weight-preshuffled GEMM op
gemm_preshuffle, which pre-permutes the B (weight) tensor into the pipeline's
packed layout for higher throughput on weight-heavy GEMMs. This is a real Old-TE
capability with no dispatcher equivalent, so this PR adds the bridge so the dispatcher
can generate and launch preshuffle GEMM at parity with the legacy Tile Engine version.

The capability set matches the Old-TE op exactly: fp16, bf16, fp8, bf8, rcr
layout only (A row-major, B col-major, C row-major), with the B tensor preshuffled
into the packed layout. It follows the same bridge scheme as #8997 (regular GEMM),
#9000 (grouped), and #9028 (stream-K).

Test Plan

  • Run the CPU-only unit tests (no GPU required):
    python3 -m pytest dispatcher/tests/test_preshuffle_bridge.py -v
  • Codegen + build the .so for all 4 dtypes and verify output vs an fp32 NumPy
    reference at M=N=K=512 on gfx942 / MI300X.
  • Confirm the preshuffle B-shuffle matches ck_tile::shuffle_b byte-for-byte.

Test Result

  • CPU-only unit tests pass (8 passed).
  • Codegen + .so build succeed for all 4 dtypes (0 codegen failures). Output verified
    vs fp32 reference at M=N=K=512:
dtype max_rel status
fp16 5.2e-4 PASS
bf16 5.5e-3 PASS
fp8 3.4e-2 PASS
bf8 6.5e-2 PASS
  • The preshuffle B-shuffle (the Initial workflows #1 correctness risk) is verified byte-for-byte via
    ck_tile::shuffle_b; the permuteN shuffle gives ~1.25 rel error and is deliberately
    not used, since the bridge does not emit the permuteN pipeline.
  • All touched C/C++ formatted with clang-format-18 -style=file.
  • Serialized A/B perf-parity vs Old-TE (MI300X / gfx942, all 4 dtypes, rcr, 16
    stems × 5 shapes = 80 rows, interleaved, fair 50/100/flush/rotating both sides):
    at parity / slightly ahead — median gap +3.03%, mean +2.24%, 100% within
    ±15%, 97.5% within ±5% (range [-3.79%, +5.24%]; per-dtype medians fp16 +2.63%,
    bf16 +2.92%, fp8 +2.98%, bf8 +3.30%). See the parity comment for details.

Related PRs / references (TileEngine → Dispatcher GEMM bridge series): #8997 (regular GEMM fp16/bf16 all-layout), #9000 (grouped GEMM), #9028 (stream-K), #8887 (fp8/bf8/int8). This PR is a sibling in the same bridge effort tracked across those PRs.

Extend the bridge so the Dispatcher generates and launches the weight-
preshuffled GEMM (gemm_preshuffle) at parity with the legacy Tile Engine
op, for all four supported dtypes (fp16/bf16/fp8/bf8), rcr layout only.

- unified_gemm_codegen.py: complete the PRESHUFFLE variant branch so it
  works for the bridge single-config path (accept a pinned preshufflev2
  pipeline, sweep the epilogue, de-dup on the sweep anchor); emit a
  PermuteN kernel-struct member and thread permute_n through key_name /
  KernelNaming; export ALayout/BLayout/CLayout globally for single-kernel
  consumers; fix the wrapper double_buffer flag for preshufflev2.
- gemm_ctypes_lib.cpp: preshuffle host B before upload using the same
  ck_tile::shuffle_b transform Old-TE applies, built from the force-
  included kernel geometry (BridgePreshuffleConfig) and its BLayout, so
  the device reads byte-for-byte identical packed weights. B_host stays
  logical so the numpy reference (A @ B) remains valid.
- gemm_utils.py: add permute_n to GemmKernelConfig, thread variant into
  expand_sweep, and pin permute_n=False (the emitted
  WeightPreshufflePipelineAGmemBGmemCRegV2 matches the non-permuteN
  shuffle; GPU-verified).
- ctypes_utils.py: validate preshuffle configs against the preshuffle-
  specific warp-tile whitelist and key the warp-tile tables on the
  accumulator dtype (fixes fp8/bf8 admitting tiles the codegen rejects).
- gemm_full_benchmark.py: map gemm_preshuffle to the preshuffle codegen
  variant, allow fp8/bf8 + rcr-only, and pass the variant to expand_sweep.

GPU smoke on MI300X (gfx942): codegen + .so build succeed for all four
dtypes; output verified vs fp32 numpy reference at M=N=K=512 (max_rel
fp16 5e-4, bf16 5.5e-3, fp8 3.4e-2, bf8 6.5e-2).
@ozturkosu ozturkosu changed the title Add gemm_preshuffle to the TileEngine->Dispatcher bridge feat(ck-tile): preshuffle GEMM TE to dispatcher bridge Jul 11, 2026
@ozturkosu ozturkosu self-assigned this Jul 11, 2026
@therock-pr-bot

therock-pr-bot Bot commented Jul 11, 2026

Copy link
Copy Markdown

✅ All Checks Passed — Ready for Review

Check Status Details
🌿 Branch Name ✅ Pass
📝 PR Title/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.

@therock-pr-bot

therock-pr-bot Bot commented Jul 11, 2026

Copy link
Copy Markdown

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

… permute_n

- Cache the host B-preshuffle per (B pointer, K, N) in the ctypes lib so the
  reorder runs once per callable instead of on every dispatcher_run_gemm call.
  The kernel-timed region (g_dispatcher->run) never contained the shuffle, but
  this keeps the repeated per-iteration path launch-only so the upcoming A/B
  perf sweep is apples-to-apples with Old-TE (which shuffles B once).
- Make permute_n a single source of truth (BRIDGE_PERMUTE_N in gemm_utils);
  the pin now reads that constant instead of a bare hardcoded False, with a
  note that both default_config.json and default_ci_config.json ship
  permute_n=true and must not be honored (host-marker only, no distinct bridged
  device kernel). Mirror the note at the codegen read site.

Correctness re-verified on GPU (rcr, M=N=K=512): fp16 5.15e-04, bf16 5.53e-03,
fp8 3.37e-02, bf8 6.54e-02 -- byte-identical to pre-hoist results.
@ozturkosu
ozturkosu marked this pull request as ready for review July 12, 2026 22:34
@ozturkosu
ozturkosu requested a review from a team as a code owner July 12, 2026 22:34
@ozturkosu

Copy link
Copy Markdown
Contributor Author

A/B perf-parity sweep vs Old-TE (MI300X / gfx942)

Ran the full serialized bridge-vs-Old-TE performance parity sweep for preshuffle GEMM on a single MI300X (gfx942), apples-to-apples, across all four dtypes:

  • Both sides identical kernels: the bridge codegens the same preshufflev2 (non-permuteN) ck_tile kernel the Old-TE gemm_preshuffle benchmark builds; same CI kernel set (rcr, 4 kernels × 4 dtypes = 16 stems from the fixed CI config), same TE -mllvm codegen flags on both.
  • Fair timing: warmup=50 / repeat=100 / flush_cache=true / rotating_count=1000 on both sides (the dispatcher backend stream_config was set to match the Old-TE profiler for the measurement).
  • Interleaved A/B: per (stem, shape) the two sides are measured back-to-back, 3 reps alternating order, median taken.
  • dtypes: fp16, bf16, fp8, bf8. Shapes: 512³, 1024³, 2048³, 1024×512×256, 4096³.

Result (80 rows = 16 stems × 5 shapes)

metric value
median gap +3.03%
mean gap +2.24%
within ±5% 97.5%
within ±15% 100%
range [-3.79%, +5.24%]

Per-dtype median gap: fp16 +2.63%, bf16 +2.92%, fp8 +2.98%, bf8 +3.30% (20 rows each).

gap% = (bridge_tflops − oldte_tflops) / oldte_tflops × 100 (positive = bridge faster). The bridge is at parity or slightly ahead of Old-TE on every dtype — 100% within ±15%, 97.5% within ±5%, no regression. The preshuffle B-shuffle (the main correctness risk) uses the identical ck_tile::shuffle_b transform, so both sides run byte-identical kernels.

Full per-(dtype, stem, shape) CSV is attached to the PR working set (parity_preshuffle_MI300X_gfx942_all4dtypes_rcr_2026-07-12.csv).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds TileEngine → Dispatcher bridging support for weight-preshuffled GEMM (gemm_preshuffle) so the Dispatcher can codegen/name/run preshuffle kernels consistently with the existing GEMM bridge infrastructure, including host-side B reordering for preshuffle correctness.

Changes:

  • Adds CPU-only preshuffle bridge tests to lock down kernel naming and codegen JSON (permute_n) projection.
  • Extends GEMM bridge config/name plumbing to carry a preshuffle variant (including permute_n) through Python config objects and unified codegen naming.
  • Updates the ctypes GEMM runtime to apply the host-side B preshuffle transform when compiling/running a preshuffle kernel.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
projects/composablekernel/tile_engine/ops/gemm/gemm_full_benchmark.py Threads a variant token into expand_sweep and adds preshuffle dtype/layout guards.
projects/composablekernel/dispatcher/tests/test_preshuffle_bridge.py New CPU-only unit tests for preshuffle naming + codegen JSON + shipped config validation.
projects/composablekernel/dispatcher/python/gemm_utils.py Adds permute_n to GemmKernelConfig, pins bridge permute behavior, threads variant through sweep expansion.
projects/composablekernel/dispatcher/python/ctypes_utils.py Fixes warp-tile whitelist lookup to key by accumulator dtype and adds preshuffle-specific whitelist selection.
projects/composablekernel/dispatcher/codegen/unified_gemm_codegen.py Completes preshuffle variant naming/config generation (incl. permute_n) and fixes preshuffle double-buffer flag emission.
projects/composablekernel/dispatcher/bindings/ctypes/gemm_ctypes_lib.cpp Adds host-side B preshuffle (and caching) for preshuffle kernels before H2D upload.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread projects/composablekernel/tile_engine/ops/gemm/gemm_full_benchmark.py Outdated
Comment thread projects/composablekernel/dispatcher/python/ctypes_utils.py Outdated
Comment thread projects/composablekernel/dispatcher/codegen/unified_gemm_codegen.py Outdated
@ozturkosu

Copy link
Copy Markdown
Contributor Author

A/B perf-parity sweep vs Old-TE (MI350X / gfx950)

MI350X (CDNA4 / gfx950) re-run of the preshuffle (weight-preshuffle) GEMM
TE-to-dispatcher bridge perf parity, mirroring the MI300X methodology.
ROCm 7.1.0 (this node's toolchain; parity is still apples-to-apples because
BOTH bridge and Old-TE are built and timed on this same node/ROCm), single
dedicated GPU.

  • Identical kernels: both sides emit the byte-identical ck_tile kernel via
    the identical-kernel trick. The PR's shipped default_ci_config.json was
    copied into the Old-TE tree as parity_ci_config.json with permute_n set
    to false
    , and passed with -DGEMM_PRESHUFFLE_CONFIG_FILE=parity_ci_config.json -DSUPPORTED_GPU_TARGETS=gfx950. The Old-TE stem set then matched the bridge
    stem set 1:1 (verified before timing).
    • Corrected op-specific assumption (permute_n): the brief said to copy the
      shipped CI config verbatim, but that config ships permute_n=true. Verified
      against the Old-TE source that PermuteN is not a device-kernel template
      parameter (gemm_instance_builder.py bakes it only as a constexpr; the device
      kernel is built from Preshuffle=true alone), so permute_n true/false give
      the SAME device kernel and the SAME TE stem name. permute_n only selects the
      HOST B-shuffle: shuffle_b_permuteN (true) vs shuffle_b (false). The
      device pipeline expects the shuffle_b order, and the bridge pins
      BRIDGE_PERMUTE_N=False (shuffle_b, GPU-verified max_rel ~5e-4). Using the
      shipped permute_n=true on Old-TE would host-shuffle B with the wrong
      permutation. So byte-identity required permute_n=false on BOTH sides.
  • Fair timing (both sides): warmup=50, repeat=100, flush_cache=true, rotating_count=1000. Preshuffle uses the REGISTRY path, so the bridge
    generated_kernel_backend.hpp stream_config was patched to
    {cold_niters=50, nrepeat=100, flush_cache=bench, rotating_count=1000} and the
    16 .so rebuilt; Old-TE bins run -warmup=50 -repeat=100 -flush_cache=true -rotating_count=1000 -metric=1 -verify=0 -json_output=true.
  • Same build flags: Old-TE built with the PR's own CMake (TE -mllvm codegen
    flags on); the bridge codegen applies the identical _TILE_ENGINE_CODEGEN_FLAGS.
    No noalias=0, no hand-rolled hipcc.
  • Interleaved 3-rep median: for each (stem, shape) bridge and Old-TE are
    measured back-to-back, alternating order across reps, median per side (no
    thermal drift). Every bridge stem is fp32-verified once before timing.
  • Shapes: 512^3, 1024^3, 2048^3, 1024x512x256, 4096^3.
  • Scope: layout rcr (preshuffle is rcr-only). Requested dtypes fp16/bf16/fp8/
    bf8. See the correctness/coverage exclusions below.

Result (fp16 + bf16, default epilogue -- the buildable matched set)

metric value
rows 20
median gap -0.12%
mean gap -0.23%
within +/-5% 100.0%
within +/-15% 100.0%
range [-3.1%, +0.6%]

gap_pct = (bridge_tflops - oldte_tflops) / oldte_tflops * 100 (+ = bridge faster).

Verdict: at parity. On the byte-identical matched set the bridge is centered
on zero (median -0.12%, mean -0.23%, 100% within +/-5%, 100% within +/-15%).
There are zero |gap|>15% outliers, so no standalone re-measure was needed.
Parity holds across all five shapes and both persistent/non-persistent variants.

Correctness / coverage exclusions (NOT perf gaps)

Unlike the MI300X run (80 rows, all 4 dtypes, both epilogues), on gfx950 with
ROCm 7.1.0 the following do not yield a fair, correct comparison and are excluded
from the timing set. They are recorded in the CSV with a flag note:

  • fp8 and bf8 (all epilogues): SHARED ck_tile correctness/build bug. Old-TE's
    benchmark_single fails to compile on gfx950 for fp8/bf8 preshuffle:
    cshuffle_epilogue.hpp:506/507/530 -- constexpr RakedXDLN_PerWarp must be a constant expression + division by zero (fatal under Old-TE's -Werror -Weverything). The bridge .so compiles (no -Werror) but its fp8/bf8 output
    is numerically WRONG (fp32-verify max_rel ~2.96 fp8 / ~3.02 bf8, far beyond
    fp8 quantization). This is the known gfx950 fp8/bf8 preshuffle issue and it is
    a shared upstream ck_tile bug (Old-TE cannot even build it), not a bridge
    regression. Reported as a correctness flag, no perf number.
  • cshuffle epilogue (fp16/bf16): coverage asymmetry. The same
    CShuffleEpilogue constexpr div-by-zero makes Old-TE's cshuffle bins
    fail to compile on gfx950 for this CI-config geometry (64x64x64,
    warp 2x2x1, warp_tile 16x16x32), so there is no fair Old-TE counterpart to time.
    The bridge cshuffle fp16/bf16 kernels DO build and fp32-verify correctly
    (max_rel ~5e-4), so this is bridge coverage, not a perf gap.

Net timable, byte-identical, correctness-gated set on this node: fp16/bf16 x
default epilogue x {persistent, non-persistent} = 4 stems x 5 shapes = 20 rows.

CSV: parity_preshuffle_MI350X_gfx950_all4dtypes_rcr_2026-07-13.csv
(20 timed rows + 12 flag rows for the excluded fp8/bf8 and cshuffle stems).

@ozturkosu ozturkosu added the gfx950 run CI on gfx950 label Jul 13, 2026
ozturkosu and others added 7 commits July 15, 2026 15:49
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…bridge_gemm_preshuffle

# Conflicts:
#	projects/composablekernel/dispatcher/python/gemm_utils.py
…out guard, dtype key)

Address reviewer correctness items on the preshuffle GEMM TE->Dispatcher bridge:

1. get_shuffled_b stale-read foot-gun: the shuffle cache is keyed on
   (B pointer, K, N) and cannot detect in-place mutation of the bytes behind an
   unchanged pointer. Document a loud immutability contract at the public
   dispatcher_run_gemm boundary AND add an opt-in recompute path via the
   CK_DISPATCHER_PRESHUFFLE_NO_CACHE env var so a correctness caller that mutates
   B is never served a stale shuffle.

2. BLayout byte-identity assumption: the host B-shuffle only reproduces Old-TE's
   b_k_n byte-for-byte when BLayout is column-major. Add a static_assert(!kBRowMajor)
   in preshuffle_host_b so a future row-major-B layout expansion fails loudly at
   compile time instead of silently mis-shuffling B.

3. dtype_key mixed-A/B latent bug: validate_kernel_config lives on the shared
   standard path and keyed warp-tile lookup on dtype_a repeated. Thread dtype_b
   through so a future mixed-A/B-dtype caller (e.g. fp8_bf8) forms the correct
   {dtype_a}_{dtype_b}_{dtype_acc} key instead of silently hitting the wrong one.
   No behaviour change for preshuffle scope (dtype_a == dtype_b).
Old-TE ships persistent=[true,false] for the preshuffle op, so both are
legitimate configs the bridge must support. On gfx942 both verify against
an fp32 reference at rel ~4e-4. Add CPU regression tests that pin the
host-launch invariant guarding correctness of the non-persistent path:
a non-persistent kernel must use the full one-block-per-tile GridSize (not
the occupancy-sized MaxOccupancyGridSize used by persistent kernels), and
UsePersistentKernel must track the trait so the host grid and the device
entry point agree. Also assert the shipped CI/default configs keep
persistent=False in the sweep so the non-persistent path stays exercised.
…bridge_gemm_preshuffle

# Conflicts:
#	projects/composablekernel/dispatcher/python/gemm_utils.py
@ozturkosu
ozturkosu requested review from Copilot and yraparti and removed request for yraparti July 21, 2026 02:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread projects/composablekernel/dispatcher/bindings/ctypes/gemm_ctypes_lib.cpp Outdated
Comment thread projects/composablekernel/dispatcher/python/gemm_utils.py
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Whole-config perf parity — preshuffle GEMM bridge vs Old Tile-Engine (MI300X / gfx942)

Extends the earlier fixed-CI sweep to the full gemm_preshuffle/configs/default_config.json config space (step=64 dense grid, full tile/warp set), fp16, layout rcr, on MI300X gfx942 (GPU 2 only, permute_n=False apples-to-apples subset).

Verdict: AT PARITY.

metric value
matched stems timed 92 (permute_n=False, both-engine-buildable)
valid timed rows 460
geomean bridge/old ratio 1.0015 (+0.15%)
median gap +0.19%
within ±5% / ±10% / ±15% 94.3% / 98.9% / 99.8%
surviving gap

gap = (bridge − old)/old × 100 (+ = bridge faster). Shapes: 512³, 1024³, 2048³, 1024×512×256, 4096³.

permute_n scope: default_config.json ships permute_n: true (global), but the bridge codegen emits the non-permuteN device pipeline (matches ck_tile::shuffle_b, GPU-verified max_rel ~5e-4). The fair apples-to-apples subset is therefore permute_n=False — Old-TE was built from a permute_n:false copy of the same tile/trait grid so both sides run the identical device kernel + identical host B-shuffle. The 104 permute_n=true un-bridged instances feed a different (un-bridged) host shuffle and carry no parity ratio (coverage-only).

Coverage findings (not perf gaps, flagged separately):

  • cshuffle epilogue: all 104 Old-TE cshuffle targets fail to compile (Old-TE codegen bug: cshuffle_epilogue.hpp:507 division by zero). The bridge compiles/runs all 194 cshuffle stems — a bridge coverage WIN; not comparable apples-to-apples in this grid, so the verdict is for epilogue=default.
  • 64x192x64 tiles (12 stems): bridge dispatch returns status −2 (N=192 non-power-of-2 tile_n unsupported on gfx942) → fp32-verify fails → excluded from timing. Correctness/coverage item for the author, not a perf number.

Fairness: same warmup=50/repeat=100/flush/rotating both sides; Old-TE via its own gemm_preshuffle CMake (real TE -mllvm flags), bridge .so stream_config patched to match and rebuilt (stale-.so guarded); bridge fresh from PR tip (HEAD 6d88c38); A/B interleaved per (stem,shape), median-of-3; outliers re-measured standalone median-3/5; Old-TE tflops from the full-precision field. CSV + full summary attached.

…_n guard, cache cleanup)

Address Copilot + internal review on the preshuffle GEMM bridge:

- Copilot / F6: preshuffle_host_b() copied from the caller's raw byte buffer
  (b_host comes from Python/ctypes, e.g. fp8/bf8 as numpy uint8) with std::copy,
  which does typed reads through the pointer -- strict-aliasing / object-lifetime
  UB. Replaced with a byte-wise std::memcpy of the exact K*N*sizeof(T) span into
  the tensor's flat storage (byte-identical, no typed reads).

- Copilot / F5: a preshuffle config with permute_n=True would build a "_permuteN"
  kernel whose device pipeline is not bridged yet (mis-shuffles B -> wrong
  results). setup_multiple_gemm_dispatchers now hard-fails on such a config
  before codegen/compile. expand_sweep still pins permute_n to BRIDGE_PERMUTE_N,
  so the normal flow (and the shipped configs that carry permute_n=true as a TE
  host-marker) is unaffected; name/JSON projection is untouched so the existing
  permute_n name/codegen tests still pass. Added a CPU test.

- F3: dispatcher_cleanup() now releases the process-lifetime shuffled-B cache so
  a library consumer gets its memory back.

Verified: 13/13 CPU tests pass; clang-format-18 + py_compile clean; GPU build+run
of a preshuffle fp16/rcr kernel (gfx942, ROCm 7.2) matches the numpy reference
(max_rel=0.000).
@ozturkosu

ozturkosu commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Review pass: Copilot threads + internal review (commit 74418a52e0)

Review

Went through both Copilot threads and my earlier internal review (F1–F7). Reliability assessed against the current tip; fixed the worthwhile items, verified on GPU.

Fixed

# Item Fix
F6 (Copilot) preshuffle_host_b() used std::copy, doing typed reads through the caller's raw byte buffer (fp8/bf8 numpy uint8) → strict-aliasing/lifetime UB Byte-wise std::memcpy of the exact K*N*sizeof(T) span into the tensor's flat storage — byte-identical, no typed reads
F5 (Copilot) a permute_n=True config would build a _permuteN kernel whose device pipeline isn't bridged (mis-shuffles B → wrong results) Hard-fail in setup_multiple_gemm_dispatchers (build path) before codegen/compile; expand_sweep still pins permute_n=BRIDGE_PERMUTE_N so shipped configs / normal flow are unaffected; name/JSON untouched (tests still pass). Added test_permute_n_build_hard_fails
F3 process-lifetime shuffled-B cache never released dispatcher_cleanup() now resets g_shuffled_b_cache

Reviewed, intentionally not changed (with rationale)

# Item Disposition
F1 #define GFX_ARCH "gfx942" default in gemm_ctypes_lib.cpp Shared file — changing to #error affects every sibling bridge; tracked for the central arch-cleanup PR, not this one (same as F7 in the codegen)
F2 shuffled-B cache keyed on (ptr,K,N) can serve stale bytes if B is mutated in place Kept: documented at 3 sites + CK_DISPATCHER_PRESHUFFLE_NO_CACHE=1 opt-out; benchmark-scoped. Making no-cache the default would break the A/B-parity purpose
F4 preshuffle_cache_disabled() env parse (v[0] != '0') is looser than the doc Kept: documented as "set to 1"; cosmetic

Verification

  • 13/13 CPU unit tests pass (incl. the new build-guard test); clang-format-18 + py_compile clean.
  • GPU build+run (gfx942, ROCm 7.2): a preshuffle fp16/rcr kernel built with the memcpy change matches the numpy reference (max_rel=0.000) — the shuffle path is unchanged in behavior, only the UB is removed.

@ozturkosu

ozturkosu commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Arch-agnostic fix — no more silent gfx942 default (commit efe73ce641)

(no silent GPU-arch default; resolve dynamically or throw), fixed the shared plain-GEMM files this preshuffle bridge reuses:

  • gemm_ctypes_lib.cpp: replaced #ifndef GFX_ARCH / #define GFX_ARCH "gfx942" with #error — the kernel can no longer be built for the wrong target by omission.
  • gemm_utils.py: added _get_arch() (rocminfo detect → validate against gfx90a/gfx942/gfx950 → raise on undetectable/unsupported, never defaults) and _resolve_arch(); changed GemmKernelConfig.gfx_arch default from "gfx942" to Optional[str] = None; expand_sweep() and the build path now resolve the arch so -DGFX_ARCH/--offload-arch always get a concrete, host-detected arch.
  • gemm_full_benchmark.py: --arch default "gfx942"None (resolved downstream).

Verification: py_compile + clang-format-18 clean; test_preshuffle_bridge.py 13/13 pass; broader gemm suite 82 passed / 63 skipped (the only failures are pre-existing example-integration tests that need a GPU, unrelated to this change). GPU build+run on gfx942 and gfx950 to follow.

ozturkosu added a commit that referenced this pull request Jul 22, 2026
## Summary
Adds a microscaling-GEMM (`mx_gemm`) bridge from the Old Tile-Engine
into the dispatcher's ctypes path for gfx950/MI350. Supports **fp4**
(`pk_fp4_t`, e2m1) and **fp8** (e4m3), **rcr** layout, `comp_async` +
`cshuffle` + `intrawave`, fixed `16x16x128` warp tile, `k_batch == 1`.
Per-32-K `e8m0` block scales are pre-shuffled on-host exactly as the
Old-TE profiler does.

## Motivation
Extend the TE→Dispatcher bridge family to the microscaling GEMM op so
the dispatcher can launch byte-identical `mx_gemm` kernels with block
scaling. Sibling to the other bridge PRs (see below).

## Design note
- **Byte-exact kernels:** the codegen reuses Old-TE
`MxGemmKernelBuilder._generate_kernel_instance` directly rather than
re-implementing header assembly, guaranteeing the emitted kernel matches
Old-TE. It only strips the stale `ck_tile/ops/gemm_mx.hpp` umbrella
include (absent on develop; the mx pipeline is pulled in via
`ck_tile/ops/gemm.hpp`).
- **Registry bypass:** `mx_gemm`'s `launch` takes
`ck_tile::MxGemmHostArgs` with per-32-K `e8m0` scales that the generic
dispatcher backend cannot express, so the ctypes lib builds
`MxGemmHostArgs` from plain C arrays and calls
`SelectedKernel::launch()` directly — the same direct-launch pattern as
the batched/multi-D bridges.
- **Scale pre-shuffle** mirrors `mx_gemm_profiler.hpp`; pack params are
derived from `SelectedKernel` tile dims at compile time (not hardcoded).
- **Packing-correct byte accounting:** device buffers are sized via
`HostTensor::get_element_space_size_in_bytes()`, which divides by
`numeric_traits<T>::PackedSize`, so fp4 (`PackedSize==2`, two e2m1 per
byte) and fp8 (`PackedSize==1`) are both correct.



## Verification
- fp8 derisk: PASS (`max_rel = 0.0`, kernel-name match, output fully
non-zero) on gfx950
- fp4: GPU-verified on gfx950
- `clang-format-18 -style=file` clean; Python `py_compile` clean



## Sibling PRs
- #8997 — TE→dispatcher GEMM bridge (fp16/bf16, all layouts) —
foundational (merged)
- #8998 — fp8/bf8/int8 bridge (merged)
- #9000 — grouped GEMM
- #9028 — stream-K GEMM
- #9305 — multi-ABD GEMM
- #9306 — batched GEMM
- #9307 — preshuffle GEMM
- #9308 — multi-D GEMM
- #9328 — batched-contraction GEMM

---------

Co-authored-by: Muhammed Emin Ozturk <3836908+ozturkosu@users.noreply.github.com>
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.

2 participants