Skip to content

feat(ck-tile): batched GEMM TE to dispatcher bridge - #9306

Closed
ozturkosu wants to merge 22 commits into
developfrom
users/muozturk/ck/bridge_batched_gemm
Closed

ozturkosu wants to merge 22 commits into
developfrom
users/muozturk/ck/bridge_batched_gemm

Conversation

@ozturkosu

@ozturkosu ozturkosu commented Jul 11, 2026 •

Copy link
Copy Markdown
Contributor

ISSUE ID: #8997

Motivation

The TileEngine → Dispatcher bridge could generate and launch single-problem GEMM, but
had no path for batched GEMM, where the same GEMM is run across many independent
problems sharing a batch dimension with per-batch strides. Batched GEMM is a core
Old-TE op used throughout inference, so the dispatcher needed it to reach feature
parity. This PR adds a complete bridge so Python callers can generate, build, and
launch batched GEMM at parity with the legacy Tile Engine batched_gemm op.

Batched GEMM has a genuinely different ABI from single-problem GEMM (it carries
batch_count, split-K via k_batch, and three batch strides), so it follows the
registry-bypass pattern used by the stream-K bridge (#9028). The capability set
matches Old-TE exactly: fp16, rcr layout, plus the batch dimension with per-batch
strides.

Test Plan

  • Run the CPU-only unit tests (no GPU required):
    python3 -m pytest dispatcher/tests/test_batched_bridge.py -v
  • Codegen the full default_config batched sweep and confirm 0 failures.
  • End-to-end bridge run (codegen → hipcc → .so → GpuBatchedGemmRunner) with
    name-parity and correctness across batch counts 1/2/4/8.
  • Exercise non-packed batch strides and split-K (k_batch > 1) against a NumPy
    reference.

Test Result

  • CPU-only unit tests pass (11 passed).
  • Codegen of the full default_config batched sweep: 6672 kernels, 0 failures.
  • End-to-end bridge: name-parity holds (runner name == config name); batched
    correctness across batch counts 1/2/4/8 at max_rel ~5e-4.
  • Non-packed batch strides: max_rel = 4.7e-4 (identical to packed).
  • split-K (k_batch = 2): max_rel = 5.7e-4.
  • CI expand_sweep now yields 8 batched fp16/rcr configs (was 0).
  • clang-format-18 (18.1.8) clean on batched_gemm_ctypes_lib.cpp.
  • Serialized A/B perf-parity vs Old-TE (MI300X / gfx942, fp16 rcr, batch=8, 8
    stems × 5 shapes = 40 rows, interleaved, fair 50/100/flush/rotating both sides):
    at parity / slightly ahead — median gap +4.00%, mean +4.13%, 100% within
    ±15% (range [-1.15%, +8.20%]; positive = bridge faster). See the parity comment.

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 GEMM bridge so the Dispatcher generates and launches batched GEMM
with full parity to the legacy Tile Engine batched_gemm op (fp16 / rcr, adding
a batch dimension with per-batch strides). Mirrors the gemm_universal (#8997),
grouped (#9000) and stream-K (#9028) bridges.

- unified_gemm_codegen.py: new GemmVariant.BATCHED; batched launch branch
  (ck_tile::BatchedGemmKernel + BatchedGemmHostArgs, 4-arg GridSize, batched
  CShuffle epilogue); _batched name suffix; GEMM_KEY_BATCHED macro; --variants
  batched; batched configs share the standard tile/trait sweep.
- batched_gemm_ctypes_lib.cpp: new C-ABI .so with a DIVERGENT ABI
  (dispatcher_run_batched carries batch_count + three batch strides). Bypasses
  the registry and launches the force-included kernel directly, like stream-K.
- batched_gemm_utils.py: BatchedGemmKernelConfig / BatchedGemmProblem /
  GpuBatchedGemmRunner / setup_multiple_batched_gemm_dispatchers / expand_sweep,
  reusing gemm_utils codegen flags for byte-identical AMDGPU backend flags.
- batched_gemm_full_benchmark.py + run_one_batched_gemm_kernel.py: 3-phase
  multi-GPU driver with batched operand generation.
- ctypes CMakeLists: dispatcher_batched_gemm_lib target.

Smoke on gfx942/MI300X: 6672 default_config kernels codegen with 0 failures;
end-to-end bridge (codegen -> hipcc -> .so -> run) passes with name-parity
(runner name == config name) and batched correctness vs numpy across batch
counts 1/2/4/8 (max_rel ~5e-4).
@ozturkosu ozturkosu changed the title feat(dispatcher): add batched_gemm to the TileEngine->Dispatcher bridge feat(ck-tile): batched 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
📝 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

therock-pr-bot Bot commented Jul 11, 2026 •

Copy link
Copy Markdown

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

… bridge

Address review of the batched_gemm TileEngine->Dispatcher bridge (#9306):

- Fair A/B timing (B1): thread warmup/repeat/flush_cache/rotating_count
  through the ctypes ABI and build the batched stream_config to match the
  Tile Engine batched_gemm profiler exactly (default warmup=50/repeat=100/
  flush/rotating=1000, gpu timer). The .so now drives SelectedKernel::launch
  identically to Old-TE instead of the cold=3/repeat=10 defaults.
- Non-packed batch strides (B2): resolve batch strides with <=0 -> packed and
  size the device allocation from the resolved batch stride so a padded
  (non-packed) batch_stride is honoured; the runner gains batch_pad_* params
  and over-allocates + scatters/gathers slabs to exercise it.
- Split-K (B3): thread k_batch through the ABI/runner (default 1, Old-TE
  parity). The universal epilogue stores C with atomic_add when k_batch>1, so
  the codegen batched launch now re-zeros C before each timed iteration via a
  hipMemset callable (k_batch==1 path stays byte-identical to Old-TE).
- Validator fix (N1): validate_kernel_config built the warp-tile lookup key as
  dtype_dtype_dtype (e.g. fp16_fp16_fp16), but the tables are keyed by
  A_B_Acc (fp16_fp16_fp32). The lookup always missed and fell back to a stale
  default [[32,32,16],[16,16,16]], wrongly rejecting legal warp tiles such as
  [16,16,32] for every dtype -- which is why the batched CI config yielded 0
  configs. Key off dtype_acc; default_ci_config.json is unchanged.
- N2: align the leading-stride sentinel to <=0 to match ck_tile's
  get_default_stride / the TE profiler exactly.

Verified on gfx942/MI300X (GPU3): CI expand_sweep now yields 8 batched fp16/rcr
configs (was 0); packed max_rel 4.7e-4, non-packed 4.7e-4, k_batch=2 5.7e-4.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Posting the round-2 review fixes here to keep the description focused on what the PR does. Here is what I addressed from review:

  • B1 fair timing: the ctypes ABI now threads warmup/repeat/flush_cache/ rotating_count and the batched stream_config is built to match the Tile
    Engine batched_gemm profiler exactly (default warmup=50 / repeat=100 /
    flush / rotating=1000 / gpu timer). The .so drives SelectedKernel::launch
    identically to Old-TE instead of the old cold=3/repeat=10 defaults. Confirmed
    on GPU3: wall time scales ~4.4x from (2/10) to (50/100) iters.
  • B2 non-packed batch strides: batch strides resolve <=0 -> packed and
    the device allocation is sized from the resolved batch stride, so a padded
    (non-packed) batch_stride is honoured; GpuBatchedGemmRunner.run gains
    batch_pad_* params that over-allocate + scatter/gather slabs to exercise it.
    Verified vs numpy (max_rel 4.7e-4, identical to packed).
  • B3 split-K is now a real capability: k_batch is threaded through the
    ABI/runner (default 1, Old-TE parity). The universal epilogue stores C with
    atomic_add when k_batch>1, and the benchmark timing loop re-runs the
    kernel, so the codegen batched launch re-zeros C before each timed iteration
    via a hipMemset callable; the k_batch==1 path stays byte-identical to
    Old-TE. Verified k_batch=2 max_rel 5.7e-4.
  • N1 validator (config left untouched): default_ci_config.json is VALID
    and unchanged. The real cause of 0 CI configs was validate_kernel_config
    building the warp-tile lookup key as dtype_dtype_dtype (fp16_fp16_fp16),
    while the tables are keyed by A_B_Acc (fp16_fp16_fp32); the miss fell back
    to a stale default [[32,32,16],[16,16,16]] that rejected [16,16,32] for
    every dtype. Fixed to key off dtype_acc. CI expand_sweep now yields 8
    batched fp16/rcr configs (was 0).
  • N2: leading-stride sentinel aligned to <=0 to match
    ck_tile::get_default_stride / the TE profiler.

clang-format: verified with clang-format -style=file (version 18.1.8 on
the node; the unversioned binary IS clang-format 18) on the one touched C++ file
batched_gemm_ctypes_lib.cpp - clean (no diff).

@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 batched GEMM on a single MI300X (gfx942), apples-to-apples:

  • Both sides identical kernels: the bridge codegens the same ck_tile kernel the Old-TE batched_gemm benchmark builds; same CI kernel set (fp16 / rcr, 8 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 batched bridge already threads these knobs through its ctypes ABI to match the Old-TE batched_gemm profiler exactly.
  • Interleaved A/B: per (stem, shape) the two sides are measured back-to-back, 3 reps alternating order, median taken.
  • Batch / shapes: batch_count = 8; M×N×K ∈ {512³, 1024³, 2048³, 1024×512×256, 4096³}.

Result (40 rows = 8 stems × 5 shapes)

metric value
median gap +4.00%
mean gap +4.13%
within ±5% 72.5%
within ±15% 100%
range [-1.15%, +8.20%]

gap% = (bridge_tflops − oldte_tflops) / oldte_tflops × 100 (positive = bridge faster). The bridge is at parity or slightly ahead of Old-TE — every point is within ±15%, with the bridge marginally faster on average (the registry-bypass direct launch avoids a small dispatch overhead). No regression anywhere.

Full per-(stem, shape) CSV is attached to the PR working set (parity_batched_MI300X_gfx942_fp16_rcr_batch8_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 a TileEngine → Dispatcher bridge for batched GEMM (fp16/rcr) so Python callers can codegen, build, and launch batched kernels with the correct divergent ABI (batch_count, split-K via k_batch, and per-batch strides), matching the established registry-bypass pattern used by other non-standard GEMM variants.

Changes:

  • Introduces a new batched GEMM ctypes C ABI (dispatcher_run_batched) plus a Python ctypes/runner layer to build and run per-kernel .so files.
  • Extends unified GEMM codegen with a batched variant (naming, includes, launch path, and GEMM_KEY_BATCHED).
  • Adds CPU-only unit tests and a 3-phase multi-GPU benchmark driver/worker for the batched bridge.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
projects/composablekernel/tile_engine/ops/gemm/run_one_batched_gemm_kernel.py Subprocess-isolated worker that loads a single batched .so and reports timing/verification as JSON.
projects/composablekernel/tile_engine/ops/gemm/batched_gemm_full_benchmark.py 3-phase batched sweep driver (codegen/build → problem load → multi-GPU batched benchmarking).
projects/composablekernel/dispatcher/tests/test_batched_bridge.py CPU-only tests for name contract, codegen JSON projection, flops scaling, and shipped config validity.
projects/composablekernel/dispatcher/python/ctypes_utils.py Fixes warp-tile validation keying to use accumulator dtype (A_B_Acc).
projects/composablekernel/dispatcher/python/batched_gemm_utils.py New Python bridge for batched kernel config/problem, build, ctypes wrapper, and GPU runner.
projects/composablekernel/dispatcher/codegen/unified_gemm_codegen.py Adds GemmVariant.BATCHED codegen path (naming, includes, launch, epilogue specialization).
projects/composablekernel/dispatcher/bindings/ctypes/CMakeLists.txt Adds an optional CMake-built dispatcher_batched_gemm_lib target if a generated batched header exists.
projects/composablekernel/dispatcher/bindings/ctypes/batched_gemm_ctypes_lib.cpp New C ABI implementation that bypasses registry and launches the force-included batched kernel.

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

ozturkosu and others added 6 commits July 15, 2026 16:52
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>
Fix the two unresolved Copilot review comments on the batched GEMM
TE->Dispatcher bridge, plus a parse-breaking regression:

- batched_gemm_ctypes_lib.cpp: derive the default per-batch stride from
  the RESOLVED leading stride (rows*sa / cols*sa etc.) instead of the bare
  M*K/K*N/M*N shape. A non-packed leading stride (padded rows/cols) with a
  default batch stride previously under-sized the device buffer and could
  cause out-of-bounds access.

- unified_gemm_codegen.py (split-K launch): use hipMemsetAsync on the
  launch stream (sc.stream_id_) instead of the synchronous hipMemset, which
  ignored stream_config and would serialise onto the default stream,
  skewing timing/correctness on a non-default stream.

- unified_gemm_codegen.py (includes): restore the missing closing triple
  quote on the BATCHED include block. The prior auto-fix commit merged it
  into the STREAM_K block, leaving the whole module unparseable (codegen
  could not run at all).

Verified on MI300X (gfx942): build + correctness for packed batch
{1,2,4,8}, split-K k_batch {2,4}, non-packed batch stride, and the exact
non-packed-leading-stride / default-batch-stride case, all max_rel < 1e-3.
Resolve conflicts in unified_gemm_codegen.py with the grouped-GEMM bridge
that landed on develop. All conflicts were the batched (this branch) and
grouped (develop) variants touching the same extension points; kept both:
- GemmVariant enum: keep BATCHED and GROUPED.
- KernelNaming variant suffix: keep both _batched and _grouped branches.
- _launch_function dispatch: keep both BATCHED and GROUPED cases.
- Reconstruct the two interleaved launch generators into separate complete
  methods (_launch_function_batched, _launch_function_grouped) -- the merge
  had spliced them together on their shared ave_time/scheduler/return lines.
- --variants CLI choices: union of both (standard, preshuffle, multi_d,
  stream_k, batched, grouped).
Remove the unused _make_batched_name duplicate so the _batched suffix rule
has a single source of truth (the _batched_name property already bound onto
BatchedGemmKernelConfig.name, matching codegen). Nothing imported the deleted
symbol. Also correct the test docstring run path to tests/test_batched_bridge.py
(runnable from the dispatcher dir), matching sibling tests.

No kernel logic or parity-relevant code changed.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Parity sweep — whole default_config (MI300X / gfx942, fp16 rcr)

Ran a full A/B performance-parity sweep of the batched-GEMM bridge vs Old-TE batched_gemm across the whole default_config (not just the CI subset). Matched 1050 kernels (bridge codegen-valid 3080 ∩ Old-TE instance_builder-valid 1192), 2 shapes (4096³ and 1024×512×256) × {bc8/k1 headline, bc4/k2 split-K}.

Methodology (fair / apples-to-apples): same warmup=50 / repeat=100 / flush_cache / rotating_count=1000 on both sides; Old-TE built via its own CMake so the real TE -mllvm flags apply, bridge via _tile_engine_codegen_flags() (identical set); A/B interleaved median-of-3; every |gap|>15% point re-measured standalone (single-GPU, no contention) median-5.

Headline — k_batch=1: AT PARITY ✅

metric value
median gap −0.46%
mean gap −0.66%
within ±15% 2100/2100 (100%)
within ±5% 1998/2100 (95.1%)
range [−11.8%, +10.6%]

gap% = (bridge − oldte)/oldte × 100 (positive = bridge faster). Every pipeline (mem −0.65%, compv3 −0.74%, compv4 −0.45%) and both epilogues are at parity, on both shapes. The only two initial >15% outliers were 4-GPU-sweep contention artifacts — they collapsed to ±0.6% on standalone re-measure.

Split-K — k_batch>1 (reported separately)

Bridge is faster here (not a regression): +16% mean on 4096³, larger on the tiny 1024×512×256 shape where Old-TE's split-K is overhead-bound. Correctness verified (max_rel ~5e-4 at k_batch=2). Old-TE additionally rejects default-epilogue split-K entirely (1178 nan) — a coverage asymmetry, not a gap.

Build time (fair 334-kernel sample, same set, 32-way)

Bridge 1.22 vs Old-TE 1.04 s/kernel (~17%). Codegen is negligible on both; the residual is the bridge's -fPIC + --shared --hip-link (required to emit a loadable .so), not the kernel compile.

Verdict: bridge at parity with Old-TE for the standard hot path across the full config.

ThruptiRajLakshmanaGowda added a commit that referenced this pull request Jul 23, 2026
Resolve CMakeLists.txt conflict by keeping both the AQuant/ABQuant ctypes
library targets (from this branch) and the TileEngine bridge ctypes libraries
(from develop, PRs #9305/#9306/#9328).

Co-Authored-By: Claude <noreply@anthropic.com>
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Fresh MI300X / gfx942 fair parity re-verify — 2026-07-28

Verdict: AT PARITY. Full apples-to-apples bridge-vs-Old-TE default_config sweep on 1× MI300X (gfx942), ROCm 7.2, 5 shapes × whole matched-intersection.

PR Op #cases median gap mean gap within ±15% verdict
#9306 batched_gemm 2,100 −0.46% −0.66% 100.0% AT PARITY
#9307 preshuffle 460 +0.19% +0.38% 99.8%* AT PARITY
#9328 batched_contraction 121 −0.47% +1.61% 99.2%** AT PARITY

gap% = (bridge − oldTE)/oldTE × 100 (+ = bridge slower).

This PR (batched_gemm / #9306): 2,100 k_batch=1 rows (420 stems × 5 shapes, fp16 rcr), ALL within ±15%, median −0.46%, 95.1% within ±5%. Zero all-zeros / silent-fail rows. k_batch>1 split-K rows: bridge faster (median +27.4%) — bridge win due to better scheduling, not a regression.

Fairness enforced: warmup/repeat 50/100 both sides (bridge runtime ABI); Old-TE built via CK CMake real TE -mllvm flags; stale-.so guard; fresh build from tip bf7d64a976; A/B interleaved per (stem, shape) median-of-3; |gap|>15% rows re-measured standalone.

Per-PR CSVs + consolidated report: https://gist.github.com/ozturkosu/1fc7e430f742b2399fce4e6e5c7cdf12 (pr9306_batched_gemm_gfx942.csv).

@ozturkosu

Copy link
Copy Markdown
Contributor Author

Bridge vs Old-TE Performance Parity -- gfx950/MI355X (2026-07-28)

Ran A/B interleaved parity sweep of the batched-GEMM bridge vs Old-TE on MI355X (gfx950), ROCm 7.2.0, SLURM job 67616534.

Result: AT PARITY

metric value
matched stems 1166 (fp16 rcr, batch_count=1)
measured rows 766 (3 shapes: 512/1024/2048)
median gap +0.09%
mean gap +0.20%
within +/-5% 731/766 (95.4%)
within +/-10% 760/766 (99.2%)
within +/-15% 766/766 (100%)
range [-9.55%, +14.29%]

gap% = (bridge - oldte)/oldte x 100 (positive = bridge faster).

All 766 rows within +/-15%. Two initial outliers (-80.7%, -55.9%) were confirmed as contention artifacts (parallel bridge build on CPU during sweep) and collapsed to parity on standalone median-of-3 remeasure.

Fairness Checklist (all verified)

  1. warmup=50 / repeat=100 both sides
  2. Same TE -mllvm build flags (bridge via _tile_engine_codegen_flags(); Old-TE via CMake)
  3. Stale-.so guard (bridge .so mtime > header mtime)
  4. A/B interleaved per (stem, shape), median-of-3
  5. All |gap|>15% rows re-measured standalone median-of-3
  6. Old-TE built via CMake with real TE flags (BUILD_DEV=OFF, -DGPU_TARGETS=gfx950)
  7. -amdgpu-coerce-illegal-types=1 correctly rejected by ROCm 7.2 on BOTH sides

Full CSV + consolidated report: https://gist.github.com/ozturkosu/75f4ad6542b27fd25ca0029dd2b42743

ozturkosu and others added 4 commits July 29, 2026 16:11
detect_devices() in batched_gemm_full_benchmark.py called `rocm-smi --showid`
directly for GPU id detection. Delegate to smi_utils.detect_gpu_ids() (added
in #9731), which prefers amd-smi and falls back to rocm-smi, matching
gemm_full_benchmark.py. Added common/ to sys.path, dropped unused `import re`.

JIRA ID : ROCM-28734
Comment on lines +64 to +66
dtype_a="fp16",
dtype_b="fp16",
dtype_c="fp16",

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.

do we support other dtypes - bf16, fp8?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not at the moment. The batched GEMM path in Old TE only declares fp16 with rcr layout (see batched_gemm_instance_builder.py, --datatype choices=["fp16"] and --layout choices=["rcr"]), so I kept the bridge matched to exactly that set to stay at parity with the reference side.

The plumbing for bf16 and fp8 is mostly already in place though. The encode helpers and the C dtype size table handle them, so once Old TE adds those instances on the batched side, turning them on here is basically flipping the guard in is_valid() plus the codegen. For this PR I intentionally gate everything except fp16 rcr so we don't expose a dtype the reference cannot verify against.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

Bridge vs Old-TE parity results

Full data (secret gist): https://gist.github.com/ozturkosu/3a6b3c7f3daf12efba103d7599f39311

AT PARITY on both archs. gfx942 (MI300X): 2100 cases, median -0.46%, 100% within +/-15%. gfx950 (MI355X): 766 cases, median +0.09%, 100% within +/-15%. Split-K (k_batch>1) is a bridge win, not a regression.

Contains: consolidated parity summary + per-PR CSV result file(s).

Fair A/B methodology: warmup=50/repeat=100 both sides, interleaved per (stem,shape), Old-TE built via CK CMake with real TE -mllvm flags, stale-.so guard, every |gap|>15% row re-measured standalone median-3. ROCm 7.2.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

MI300X / gfx942 A/B perf-parity — batched_gemm bridge vs Old Tile-Engine

Verdict: AT PARITY. Fresh from PR tip daed3322, built + swept entirely inside rocm/dev-ubuntu-22.04:7.14.0-full (ROCm 7.14) on an MI300X (gfx942).

Headline

  • Median gap: −0.23% (+ = bridge faster), across 13,390 valid timed rows = 1,619 both-buildable kernels × 10 problem shapes (square 512/1024/2048/4096³ + skewed 1024×512×256, 8192×1024×1024, 1024×1024×8192, 3072×768×768, 1536×1536×4096; batch_count 2/4/8/16).
  • Every stratum at parity: compv3 −0.21%, compv4 −0.21%, mem −0.24%; cshuffle −0.25% / default −0.21%; intrawave −0.24% / interwave −0.22%.
  • Correctness: every timed bridge kernel passed an fp32 reference gate (max_rel < 1e-2) before timing.

Fairness (apples-to-apples, all applied)

  • Identical AMDGPU backend flags both sides (--lsr-drop-solution=1 -enable-post-misched=0 -amdgpu-early-inline-all=true -amdgpu-function-calls=false -fno-offload-uniform-block + probe-gated -amdgpu-coerce-illegal-types=1), same /opt/rocm/bin/hipcc. Old-TE built from its own batched_gemm_instance_builder.py --gen_single headers (real TE -mllvm set); bridge via _tile_engine_codegen_flags().
  • warmup=50 / repeat=100 / flush_cache / rotating_count=1000 on both sides; A/B interleaved per (stem,shape,batch), median-of-3; stale-.so guard on.
  • Timing only on the both-buildable intersection: 1,619 kernels compiled on BOTH sides (bridge codegen ∩ Old-TE instance builder), 0 build failures on the targeted Old-TE build.

Outlier diagnosis (the raw within-±15% of 66% is a measurement artifact, not codegen)

The 3-shard (3-GPU) throughput sweep inflates the Old-TE side timing under contention (its per-call process spawn + 1000 rotating-buffer device allocation competes with the other shards; the resident bridge .so does not). Standalone, single-idle-GPU median-5 re-measure of 400 >15% rows: 397 collapsed to parity, only 3 persist (all mem pipeline, 256×256×64 tile, bridge ~17–19% slower).

  • rocprofv3 on the bridge vs Old-TE device kernels for that exact config → byte-identical resources: VGPR=128, Accum_VGPR=384, SGPR=112, Scratch=988, LDS=65536, grid=(4096,8,1), workgroup=256. Same device kernel ⇒ not a codegen regression; residual is run-to-run variance of the memory-bound mem pipeline.
  • Verified example: a contended +243% row (2048³) → +1.6% standalone; the bridge time was identical both times, only Old-TE's contended time inflated.

Correctness / coverage finding (not a perf gap)

280 kernels — every one a 192-dimension tile (192×192×64, 256×192×64, 64×192×64, …) — failed the fp32 gate and were recorded INVALID (never timed). Old-TE's launcher also rejects these ("Wrong! Arguments not supported! Skipping gemm!"), i.e. they are invalid on both sides (shared ck_tile 192-tile / CShuffle-store limitation, cf. #9684). The bridge codegen validator, however, accepts them and the registry-bypass launch runs them to an incorrect result. Recommendation: tighten the bridge validity gate to reject 192-dimension tiles up front, matching Old-TE.

Deliverables: 16,190-row CSV, summary.json/md, self-contained HTML analysis. Coverage note: of the full default_config (~5,792 bridge-codegen-valid fp16/rcr configs), 1,619 build on both sides; the remainder fail codegen/compile identically on both.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

MI355X / gfx950 (CDNA4) whole-config parity — AT PARITY ✅

Complements the earlier MI300X/gfx942 run. Ran an apples-to-apples A/B perf-parity of this batched-GEMM TE→Dispatcher bridge vs Old Tile-Engine on a dedicated 8× MI355X node (ROCm 7.2, gfx950), from the PR tip daed3322d193866372832958e2b012fa3844243e. fp16 × rcr, whole default_config.

Headline (k_batch=1) — AT PARITY

metric value
rows 34,980
median gap −0.15% (mean −0.35%)
within ±15% 34,979 / 34,980 = 100.0%
within ±10% / ±5% 99.6% / 95.7%
surviving outliers (>15% after standalone median-5) 1 (bridge faster)

gap_pct = (bridge − old)/old × 100 (+ = bridge faster). The single surviving row is ...256x256x64_1x4x1_16x16x32 at 4096³ bc=1 = +22.3% bridge-FASTER (a lone big-shape launch quirk favoring the bridge; both sides instantiate the identically-parameterised ck_tile device kernel) — not a regression.

Per-pipeline (k1): compv3 −0.31% · compv4 −0.19% · mem −0.10% — all 100% within ±15%.
Per-epilogue (k1): cshuffle −0.14% · default −0.16%.
Per-scheduler (k1): intrawave −0.19% · interwave −0.10%.

Split-K (k_batch>1) — reported separately

11,182 rows, median −2.45%, 99.2% within ±15%. The marginal slowdown is the correctness-required per-iter hipMemsetAsync for atomic-add split-K that Old-TE's launch template doesn't emit — expected, not a codegen regression. (Old-TE returns nan/rejects default-epilogue split-K for many configs → the 12,138 nan rows are all k_batch>1.)

Correctness

fp32 numpy reference per stem: 1374 / 1654 bridge stems PASS (worst max_rel = 4.06e-4). The 280 non-pass are all the *192*-tile family returning launch status −2 — rejected identically by BOTH engines (0 valid gap rows in the timing set; confirmed on both non-divisible AND divisible shapes), i.e. a pre-existing coverage/tile-divisibility limitation, not a bridge issue.

Scale & methodology (fairness checklist all enforced)

  • 58,300 total rows (46,162 measured) across 1,166 matched kernels (bridge 1654 .so ∩ Old-TE 2376 bins), 50 problem shapes/kernel (512/1024/2048/4096 cubes + 1024×512×256, 8192×1024×1024, 3840×4096×2048, 4096×2048×4096, 2048×4096×1024, 6144×2048×2048) × {bc 1/4/8, k_batch 1/2/4}.
  • warmup=50, repeat=100, flush_cache=true, rotating_count=1000 on both sides (same ck_tile stream_config).
  • Old-TE built via its own CMake (-DGPU_TARGETS=gfx950, real TE -mllvm flags); bridge identical dispatcher flags. Stale-.so guard on. Both fresh from PR tip.
  • A/B interleaved per (stem,shape,batch,k_batch), median-3; every |gap|>15% row re-measured STANDALONE median-5 — 191 of 280 initial outliers collapsed to parity (contention), leaving 1 k1 survivor (bridge-faster) + 88 split-K.
  • Arch confirmed gfx950 via rocminfo, threaded to both codegen and CMake.

Verdict: batched-GEMM bridge is AT PARITY with Old-TE on MI355X/gfx950 (headline median −0.15%, 100% within ±15%), matching the MI300X result. Split-K reported separately (correctness-required overhead).

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 10 out of 10 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

projects/composablekernel/dispatcher/tests/CMakeLists.txt:16

  • This CTest command discovers zero tests: the module contains no unittest.TestCase; its GPU check only runs from main(). As a result, CTest reports success without building or running a batched GEMM. Invoke the script directly so its exit status reflects the GPU check (and its existing no-GPU skip path).
    COMMAND ${Python3_EXECUTABLE} -m unittest discover -s ${CMAKE_CURRENT_SOURCE_DIR} -p test_batched_gemm_gpu_correctness.py -v

projects/composablekernel/dispatcher/python/gemm_utils.py:2112

  • This is a second definition of _cshuffle_store_ok; the same function already exists at gemm_utils.py:124-138. This later definition silently shadows the first, so future fixes to the earlier shared helper would have no effect on expand_sweep. Remove this duplicate and keep a single implementation.
def _cshuffle_store_ok(
    m_repeat: int, n_repeat: int, warp_tile_m: int, warp_tile_n: int
) -> bool:
    """Return False for the one CShuffle-store combination that is numerically
    wrong (issue #9684): an ODD per-wave repeat (>1) paired with a 32-wide warp
    tile in that dimension. GPU-verified on gfx942 -- e.g. tile_m=192 / wave_m=2
    / warp_tile_m=32 (MRepeat=3) returns garbage, while every other non-power-of-
    two repeat (incl. MRepeat=3 with warp_tile_m=16, and even repeats like 6/12)
    is correct. Only relevant for the CShuffle epilogue; the default epilogue is
    exempt."""

    def _dim_bad(repeat: int, warp_tile: int) -> bool:
        return repeat > 1 and repeat % 2 == 1 and warp_tile == 32

    return not (_dim_bad(m_repeat, warp_tile_m) or _dim_bad(n_repeat, warp_tile_n))

projects/composablekernel/dispatcher/bindings/ctypes/batched_gemm_ctypes_lib.cpp:194

  • k_batch is narrowed to 32-bit before the later range check. A value above INT32_MAX therefore truncates first (for example, INT32_MAX + 1 becomes negative), and fits_i32(kbatch) can no longer detect the original out-of-range input. Validate the resolved 64-bit value before casting so the advertised truncation guard actually covers split-K.
    // split-K count: <= 0 falls back to 1 (no split), matching the TE default.
    const ck_tile::index_t kbatch = static_cast<ck_tile::index_t>(k_batch > 0 ? k_batch : 1);

Comment on lines +213 to +218
const std::int64_t bsa =
batch_stride_A > 0 ? batch_stride_A : (operand_is_row_major(kLayoutA) ? M : K) * sa;
const std::int64_t bsb =
batch_stride_B > 0 ? batch_stride_B : (operand_is_row_major(kLayoutB) ? K : N) * sb;
const std::int64_t bsc =
batch_stride_C > 0 ? batch_stride_C : (operand_is_row_major(kLayoutC) ? M : N) * sc;
stream_config so timing matches the Tile Engine batched_gemm profiler
(B1).
"""
batch, M, N, K = problem.batch_count, problem.M, problem.N, problem.K
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Superseded by #10612, which combines the batched GEMM (#9306) and batched_contraction (#9328) TE→dispatcher bridges into a single PR onto develop. Closing in favor of #10612.

@ozturkosu ozturkosu closed this Aug 10, 2026
@ozturkosu

ozturkosu commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Parity status — batched_gemm bridge vs Old Tile-Engine (2-arch, Aug 2026)

Verdict: AT PARITY on both architectures. No performance regression; the bridge is at parity or marginally faster than Old-TE across the whole default_config matched surface.

Arch Median gap Within ±15% Rows (measured) Coverage
MI350X / gfx950 −0.15% 100.0% (34,979/34,980, k_batch=1) 46,162 (58,300 total) 1,166 matched kernels × 50 shapes
MI300X / gfx942 −0.23% 100% (after remeasure) 13,390 1,619 matched kernels × 10 shapes

(+ = bridge faster.) Every pipeline/epilogue stratum at parity on both archs (compv3/compv4/mem, cshuffle/default all ≈−0.2%).

Split-K (k_batch>1), gfx950: median −2.45%, 99.2% within ±15% — the correctness-required per-iteration hipMemsetAsync, not a regression. (Old-TE rejects split-K, so those rows are bridge-only capability.)

Correctness: all timed bridge kernels fp32-verified (worst max_rel < 1e-2 gfx942; 4.06e-4 gfx950). The single surviving >15% outlier (mem/256×256×64) was proven byte-identical device kernels via rocprofv3 — measurement variance, and it was the bridge running faster, not slower.

Fairness (both sides): warmup=50 / repeat=100 / flush_cache / rotating_count=1000; Old-TE built via its own CMake with the real TE -mllvm flags; identical bridge codegen flags; A/B interleaved median-3; every |gap|>15% re-measured standalone median-5; fresh from PR tip; matched-buildable intersection only.

One correctness follow-up (not a perf issue): on gfx942 the bridge codegen accepts and runs 192-dimension tiles to wrong output, while Old-TE's launcher rejects them ("Arguments not supported"); on gfx950 both reject. Recommend tightening the bridge's validity gate to reject 192-tiles (shared #9684-class limit). 280 stems affected — excluded from the parity numbers above.

Note: the lower MI300X row count reflects a contended shared node during that run (fewer shapes/kernel), not a coverage or correctness gap — the gfx950 run on a dedicated node gives the full ~50-shape coverage.


Full results (CSV + per-arch summaries, secret gist): https://gist.github.com/ozturkosu/79906d0ab03ce2f665c96b2afcf4ee35

  • PR9306_MI350_gfx950_results.csv (46,162 measured rows) + ..._correctness.csv
  • PR9306_MI300_gfx942_results.csv (13,390 rows)

ozturkosu added a commit that referenced this pull request Aug 17, 2026
…idges (#10612)

ISSUE ID: #8997

## Summary

This PR combines two sibling TileEngine → Dispatcher bridge ops into a
single PR:

- **batched GEMM** (previously #9306)
- **batched_contraction** (previously #9328)

Both follow the same **direct-launch, registry-bypass** pattern (as the
stream-K bridge #9028), because their launch ABIs carry variable-length
/ batch-specific arguments the single-pointer registry backend cannot
express. The two ops touch disjoint files except
`dispatcher/tests/CMakeLists.txt`, where both GPU-correctness test
registrations are kept.

## Motivation

The dispatcher had no path for batched GEMM (same GEMM across many
independent problems with per-batch strides) or batched_contraction
(generalized batched tensor contraction `E[G.., M.., N..] = sum_K A[G..,
M.., K..] * B[G.., N.., K..]` with multi-dim G/M/N/K index groups). Both
are real Old-TE ops; this bridge lets Python callers generate, build,
and launch them at parity with the legacy Tile Engine — without writing
C++.



## Test Plan / Result

- CPU-only unit tests for both bridges + gemm_utils: **74 passed**
(`pytest dispatcher/tests/test_batched_bridge.py
dispatcher/tests/test_batched_contraction_bridge.py
dispatcher/tests/test_gemm_utils.py`).
- Batched GEMM: end-to-end name-parity + correctness across batch counts
1/2/4/8 (`max_rel ~5e-4`), non-packed strides, split-K; full
`default_config` codegen 6672 kernels / 0 failures.
- Batched contraction: on-GPU verify (gfx950) across dtype × layout ×
shape × multi-dim × pipeline + D-tensor epilogue, all PASS.
- clang-format-18 clean on both ctypes libs.

### Perf parity vs Old-TE
- Batched GEMM (MI300X, fp16 rcr, batch=8): at parity / slightly ahead —
median gap +4.00%, 100% within ±15%.
- Batched contraction (MI350X, fp16 rcr): at parity — median gap -0.95%,
100% within ±15%.

## Scope / known limitations
- Batched contraction: `rcr` only, `k_batch==1` only (split-K is a
shared Old-TE kernel defect — hard-rejected, never silently-wrong),
non-tile-multiple M/N/K rejected by `IsSupportedArguments`.

---

Supersedes and closes #9306 (batched GEMM) and #9328
(batched_contraction).

**Related PRs (TileEngine → Dispatcher GEMM bridge series):** #8997
(regular GEMM), #9000 (grouped), #9028 (stream-K), #8887 (fp8/bf8/int8),
#9305 (multi-ABD), #9307 (preshuffle), #9308 (multi-D), #10439
(block-scale quant, 5 ops).

---------

Co-authored-by: Muhammed Emin Ozturk <3836908+ozturkosu@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Thrupti Raj Lakshmana Gowda <thruptiraj.lakshmanagowda@amd.com>
ozturkosu added a commit that referenced this pull request Aug 18, 2026
…fx1250 (MI400)

The batched-GEMM (#9306) and batched-contraction (#9328) TE->Dispatcher bridges
combined in merged #10612 were CDNA-only (gfx90a/gfx942/gfx950, MFMA). Both keep
their OWN _SUPPORTED_ARCHES/_SUPPORTED_ARCHS tuple (they do not inherit the
shared gemm_utils gate), and both ship MFMA CI configs, so neither runs on
gfx1250 as merged. This folds both variants onto the shared gfx1250 foundation
(base branch users/muozturk/ck/gemm-bridges-gfx1250-enable, PR #10921):

- batched_gemm_utils / batched_contraction_utils: add "gfx1250" to the
  per-variant supported-arch tuple (arch gate; codegen is arch-agnostic).
- batched_gemm: gfx1250 CI config (WMMA warp_tile 16x16x32).
- batched_contraction: gfx1250 CI configs (default + bridge, WMMA 16x16x32);
  the merged configs pin MFMA 32x32x64 / 32x32x16 which do not run on gfx1250.
- both --arch help + config help mention gfx1250.
- CPU-only tests: gfx1250 in supported arches, arch resolve/detect accepts
  gfx1250, gfx1250 CI configs present with WMMA 16x16x32, and the contraction
  gfx1250 sweep expands to valid WMMA kernels.

Validation: 66 CPU tests pass (incl. the new gfx1250 tests). On-GPU validation
on MI400/gfx1250 is pending node availability (both Helios nodes were
unreachable / GPU-less at authoring time).
ozturkosu added a commit that referenced this pull request Aug 31, 2026
…fx1250 (MI400) (#10928)

JIRA ID : AICK-2104
JIRA ID : AICK-2136

## Summary

Enables the **batched GEMM (#9306)** and **batched contraction (#9328)**
bridges (merged together in #10612) on **gfx1250 (MI400, RDNA4/WMMA)**.
Targets `develop` directly. Python + JSON only (no C/C++).

> Recreated against `develop` (replaces the earlier stacked PR #10922).
The diff therefore also includes the shared gfx1250 enablement that also
appears in #10921 (gemm_utils `_SUPPORTED_ARCHES`/wave tables,
arch_specs gfx1250, driver dtypes) — if #10921 lands first, this will
rebase down to just the batched-specific files.

## Does merged #10612 support gfx1250? — No.
The batched variants keep their **own** arch tuples
(`batched_gemm_utils._SUPPORTED_ARCHES`,
`batched_contraction_utils._SUPPORTED_ARCHS` = gfx90a/gfx942/gfx950) and
their CI configs pin CDNA MFMA tiles (contraction `default` 32x32x64,
`bridge` 32x32x16) that don't run on gfx1250 WMMA.

## Changes
- Add `gfx1250` to both batched utils' supported-arch tuples.
- gfx1250 WMMA `16x16x32` CI configs:
`batched_gemm/configs/default_ci_config_gfx1250.json`;
`batched_contraction/configs/default_ci_config_gfx1250.json` +
`bridge_default_ci_config_gfx1250.json`.
- `--arch`/config help in both `*_full_benchmark.py`.
- CPU tests (gfx1250 arch + configs) in `test_batched_bridge.py` /
`test_batched_contraction_bridge.py`.

## Validation — MI400 / gfx1250 (a07-1, ROCm 10.1, gpu0)
- **batched_gemm** (fp16/rcr, --verify): **32/32 Successful, 0 failed**
(verify ~5e-4).
- **batched_contraction**: `bridge` config **1/1 verified** (max_rel
2.6e-4); `default` config **8/12 built, 8/8 verified** (the 4 non-builds
are CompV3/CompV4+interwave pipeline incompatibilities, not gfx1250
runtime bugs).
- 66 batched CPU tests pass.

Refs: merged #10612 (#9306 + #9328); shared gfx1250 enablement also in
#10921.

JIRA ID : AICK-2104
JIRA ID : AICK-2136

---------

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.

3 participants