Skip to content

[CK_TILE] Add TE -> Dispatcher bridge GEMM example#8193

Closed
ozturkosu wants to merge 11 commits into
muozturk/dispatcher-gemm-layoutsfrom
muozturk/dispatcher-gemm-bridge-example
Closed

[CK_TILE] Add TE -> Dispatcher bridge GEMM example#8193
ozturkosu wants to merge 11 commits into
muozturk/dispatcher-gemm-layoutsfrom
muozturk/dispatcher-gemm-bridge-example

Conversation

@ozturkosu

@ozturkosu ozturkosu commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds dispatcher/examples/gemm/python/12_te_bridge.py, the first runnable example that exercises the Tile Engine -> Dispatcher bridge (dispatcher/python/gemm_utils.py) rather than the Dispatcher's native ctypes Registry (examples 01-11).
  • Builds via setup_multiple_gemm_dispatchers and runs via GpuGemmRunner, sweeping fp16 and bf16 across the row/col layout combinations the universal GEMM supports (row-major C), validating each result against a NumPy reference.
  • Closes the last FMHA-parity gap from gemm_pr_evaluation.md: "FMHA shipped runnable examples; we shipped none."

Context

This is the final PR in the stack that brings the regular-GEMM bridge

PR Gap closed
#8123 TE -> Dispatcher bridge (anchor)
#8187 trait-derived registry key (was hard-coded fp16/rcr)
#8190 bf16 support (was fp16-only)
#8191 multi-layout support (was rcr-only)
this runnable example exercising the bridge

Validation (gfx942 / MI300X, M=N=K=512)

fp16/rcr   max_rel=2.46e-04  PASS
fp16/rrr   max_rel=2.46e-04  PASS
fp16/ccr   max_rel=2.46e-04  PASS
fp16/crr   max_rel=2.46e-04  PASS
bf16/rcr   max_rel=3.93e-03  PASS
bf16/rrr   max_rel=3.93e-03  PASS
6/6 passed

Test plan

  • python3 dispatcher/examples/gemm/python/12_te_bridge.py builds 6 kernels and passes 6/6 vs NumPy reference on gfx942
  • Reviewer confirms example placement/style alongside 01-11

Examples 01-11 drive the Dispatcher's native ctypes Registry; none exercise
the Tile Engine -> Dispatcher bridge (dispatcher/python/gemm_utils.py). Add
12_te_bridge.py, which builds via setup_multiple_gemm_dispatchers and runs via
GpuGemmRunner across fp16/bf16 and the row/col layout combinations the
universal GEMM supports (row-major C), validating each against a NumPy
reference. Closes the last FMHA-parity gap (runnable example scripts).

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

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 new runnable Python example under the CK Tile dispatcher GEMM examples to exercise the Tile Engine → Dispatcher bridge path (dispatcher/python/gemm_utils.py) end-to-end: codegen + hipcc build of per-config .so files, then GPU execution and validation against a NumPy reference.

Changes:

  • Introduces 12_te_bridge.py as the first GEMM example that uses setup_multiple_gemm_dispatchers() + GpuGemmRunner (bridge path) instead of the registry-driven examples (01–11).
  • Sweeps a fixed algorithm across multiple (dtype, layout) pairs (fp16 and bf16; row-major C) and validates outputs against a CPU reference.

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

Comment thread projects/composablekernel/dispatcher/examples/gemm/python/12_te_bridge.py Outdated
Comment on lines +34 to +40
from gemm_utils import ( # noqa: E402
GemmKernelConfig,
GemmProblem,
GpuGemmRunner,
setup_multiple_gemm_dispatchers,
)

Comment on lines +99 to +112
def main() -> int:
parser = argparse.ArgumentParser(
description="Tile Engine -> Dispatcher bridge example",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--size", type=int, default=512, help="M=N=K (default 512)")
parser.add_argument(
"--rtol", type=float, default=2e-2, help="relative tolerance (default 2e-2)"
)
args = parser.parse_args()

problem = GemmProblem(M=args.size, N=args.size, K=args.size)
configs = [_config(dt, lay) for dt, lay in _CASES]

if not result.success:
print(f" {tag:10s} RUN FAILED (status {result.status})")
continue
ref = _emulate(A, dtype) @ _emulate(B, dtype)
Comment thread projects/composablekernel/dispatcher/examples/gemm/python/12_te_bridge.py Outdated
Muhammed Ozturk and others added 3 commits June 8, 2026 19:02
- Add --arch flag (defaults to rocminfo auto-detection) and thread it into
  GemmKernelConfig so the example is not pinned to gfx942.
- Complete the bf16 layout matrix (add ccr/crr) so _CASES matches the
  docstring's claim of fp16+bf16 across all four row-major-C combinations.
- Round the CPU reference's output to dtype_c as well, matching the GPU's
  back-store of C, so accuracy comparison is faithful end to end.

Validated 8/8 on gfx942 (fp16 max_rel 4.9e-4, bf16 3.9e-3).

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

Copy link
Copy Markdown
Contributor Author

Update — example hardened on three review items

Applied the Copilot suggestions on 12_te_bridge.py (commit 729bc3be75):

  1. --arch flag defaulting to detect_gpu_arch() (rocminfo auto-detect),
    threaded into GemmKernelConfig.gfx_arch — the example is no longer pinned
    to gfx942.
  2. Completed the bf16 layout matrix (added ccr, crr) so _CASES now
    matches the docstring's claim of fp16+bf16 across all four row-major-C
    combinations (8 cases total).
  3. Faithful output emulation: the CPU reference now rounds the accumulator
    to dtype_c as well, mirroring the GPU's back-store of C, so the accuracy
    check is honest end to end.

Validated 8/8 on gfx942 (fp16 max_rel 4.9e-4, bf16 3.9e-3).

Muhammed Ozturk and others added 3 commits June 9, 2026 13:34
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Add tests/test_gemm_parity.py: a GPU-gated end-to-end regression that
codegens + hipcc-compiles each (dtype, layout) the bridge supports and
checks Dispatcher GPU output against a NumPy reference across square,
rectangular, and awkward (non-tile-aligned) shapes. Skips cleanly when
hipcc / the dispatcher static lib / a GPU is unavailable so CPU-only CI
stays green. Parity uses global relative error (max|gpu-ref|/max|ref|).

Expand examples/gemm/python/12_te_bridge.py from a single dtype/layout
matrix into a three-demo gallery (matrix / shapes / sweep) covering the
full dtype+layout surface, padding-driven shape acceptance, and an
algorithm sweep ranked by measured TFLOPS.

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

Copy link
Copy Markdown
Contributor Author

GPU verification on MI300X (gfx942) — results + two bugs found

Ran the newly added tests/test_gemm_parity.py end-to-end on a real MI300X
(gfx942), at this branch's HEAD (46ab6e8313). The CPU-only paths were already
green (pytest collects 24 tests, test_gemm_utils.py passes, the example's
--help works); this is the first end-to-end GPU run, and it surfaced two
genuine bridge defects that the square-only validation in earlier PRs had masked.

Parity suite: 10/24 passed

  • square 512³: 8/8 PASS — fp16 max_rel ~4–5e-4, bf16 ~3.5–7e-3. The bridge
    is numerically correct here for every (dtype, layout).
  • rectangular & awkward: most non-rcr cases fail.

Bug 1 — non-native layouts use the wrong leading dimension on non-square problems

Isolated with a dimension-sweep probe (fp16, padded default algo, row-major C).
Cell = global max_rel; "ok" ≈ 4e-4, "FAIL" ≈ 1.4 (garbage):

shape rcr rrr crr ccr
square 512×512×512 ok ok ok ok
M≠K 1024×512×512 ok ok FAIL FAIL
N≠K 512×1024×512 ok FAIL FAIL ok
K≠M,N 512×512×256 ok FAIL FAIL FAIL
rect 1024×512×256 ok FAIL FAIL FAIL

The pattern is exact:

  • column-major A is read with the wrong leading dim when M ≠ K
    (crr, ccr fail the M≠K row),
  • row-major B is read with the wrong leading dim when N ≠ K
    (rrr, crr fail the N≠K row).

Only each operand's native orientation — row-major A, column-major B, i.e. the
rcr default — computes strides correctly. The others fall back to the
square-coincident stride, so they only happen to work when the mismatched
dimensions are equal. rcr is correct on every shape.

This is in the C ABI / dispatcher leading-dim plumbing (the kernel reads
correctly-laid-out host buffers with the wrong ld), not in the Python
runner or the test. Net: the layout support is validated for rcr; rrr/crr/ccr
are not yet correct for non-square problems.

Bug 2 — odd M/N throws at runtime

The awkward shape (257×129×512) returns status -1 (run threw) for every
layout, including rcr — which is otherwise correct on all shapes above. K=512 is
divisible by 8, so this is not the known K÷8 reduction-load constraint; it's the
odd N (129) hitting the vectorized C-store. Distinct from Bug 1.

What this means for this PR

test_gemm_parity.py is behaving correctly — it is catching real defects. But it
means the suite is red on a GPU runner (CPU-only CI still skips cleanly).
Options:

  1. Scope the suite to the validated surface now: keep rcr across all shapes,
    mark the non-square non-rcr and odd-dim cases xfail/skip with a comment
    pointing at the gap. Lands an honest, green regression suite that documents
    the limitation.
  2. Fix the leading-dim handling in the C ABI / dispatcher so rrr/crr/ccr
    work on non-square, then keep the full matrix. Larger, proper follow-up.

Suggest (1) to land green now and track (2) as the real fix. Either way, the
square-shape coverage (8/8) and the example gallery are solid.

GPU run: ctr-cx65-mi300x-30 (MI300X / gfx942), python3 tests/test_gemm_parity.py.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

How validity was checked (oracle = NumPy, not Tile Engine)

To be explicit about what the parity numbers above mean: validity is checked
against an independent NumPy CPU reference, not against the Tile Engine's
own output.

The exact check in tests/test_gemm_parity.py:

ref = _emulate(_emulate(A, dtype) @ _emulate(B, dtype), dtype)  # fp32 matmul, dtype-rounded
max_rel = max|gpu_out - ref| / max|ref|                         # global relative error
assert max_rel <= tol            # fp16 2e-3, bf16 1.5e-2

i.e. Dispatcher GPU output (through the bridge) vs NumPy. A/B are rounded
to the kernel's storage dtype on both input and output so the CPU math matches
what the GPU actually multiplies and stores. Error is global
(max|gpu-ref| / max|ref|), not per-element — K-length accumulation of zero-mean
inputs produces near-zero entries whose per-element ratios explode and carry no
signal.

Why NumPy and not a bridge-vs-TE comparison

This matters for interpreting Bug 1 (the leading-dim defect):

  • The bridge is the path the Tile Engine uses — both go through the same
    unified_gemm_codegen and the same Dispatcher::run, producing byte-identical
    kernels. A bridge-vs-TE comparison is essentially the same kernel compared
    against itself.
  • The leading-dim bug lives in that shared dispatcher/ABI stride path. So a
    bridge-vs-TE check would have both sides compute the same wrong answer, agree
    with each other, and falsely pass — masking exactly this bug. An
    independent oracle (NumPy) is what exposes it.

The failure is not in the test harness

Traced all four r/c operand cases: the Python runner lays each host buffer
out byte-for-byte in the orientation the kernel name claims, and the device copy
is a flat byte copy of the correct element count. So the bytes on the device are
correct; only the leading dimension the kernel reads with is wrong. That puts the
defect on the dispatcher/kernel side, not the comparison.

Follow-up in progress

NumPy is the right correctness oracle, but a bridge-vs-TE head-to-head on the
passing rcr surface
is still worth having — it confirms the bridge reproduces
the Tile Engine numerically where both are correct. Attempting that next on the
MI300X box.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

Bridge vs Tile Engine head-to-head (gfx942) — bridge reproduces TE on the rcr surface

Follow-up to the validity note above: the NumPy oracle proves the bridge is
correct, but a bridge-vs-Tile-Engine head-to-head confirms the bridge
reproduces the native Tile Engine numerically where both are correct. Ran
that on the MI300X (ctr-cx65-mi300x-30, gfx942).

I matched the bridge byte-for-byte to a pre-built native TE benchmark binary
(same dtype/layout/algorithm), so the only difference is the harness, not the
kernel:

path kernel name square 512³ rect 1024×512×256
Native TE (gemm_universal_...) …_compv4_default_intrawave_False_False_False_False_64x64x128_2x2x1_16x16x32 verdict correct verdict correct
Bridge (GpuGemmRunner) same name, minus the _universal_ harness prefix max_rel 4.62e-4 max_rel 3.25e-4

Reading the table:

  • The two kernel names are identical except the benchmark-harness prefix —
    confirming both paths go through the same unified_gemm_codegen and produce
    the same kernel.
  • Native TE validates against its own CK CPU reference (relative-error
    threshold 4.88e-4) → verdict correct on both shapes.
  • The bridge, built from the identical config, lands at 4.6e-4 / 3.3e-4
    against an independent NumPy oracle — the same ~e-4 band, both comfortably
    under TE's own 4.88e-4 threshold.

So where both are correct, the bridge matches the Tile Engine. This closes the
"follow-up in progress" item from the validity note: NumPy remains the
correctness oracle, and the bridge is now also confirmed to reproduce the native
TE on the passing rcr surface.

GPU run: ctr-cx65-mi300x-30 (MI300X / gfx942). Native: benchmark_gemm_universal_… -verify=1. Bridge: GpuGemmRunner vs NumPy, same config.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

Re-ran the parity suite on gfx942 — still 10/24 (layout fix is square-only)

Prompted by the "all layouts fixed" update on #8123, I re-ran the full suite
fresh on the MI300X (ctr-cx65-mi300x-30, gfx942) at this branch's HEAD
(46ab6e83137). My tree already contains the complete #8191 layout fix (its head
is an ancestor of this HEAD), so this exercises the exact code that update refers
to. Result is byte-for-byte the same: 10/24.

case square 512³ rectangular 1024×512×256 awkward 257×129×512
fp16/rcr PASS 4.6e-4 PASS 5.4e-4 RUN FAILED (-1)
fp16/rrr PASS 4.8e-4 FAIL 1.28 RUN FAILED (-1)
fp16/ccr PASS 5.1e-4 FAIL 1.32 RUN FAILED (-1)
fp16/crr PASS 4.2e-4 FAIL 1.55 RUN FAILED (-1)
bf16/rcr PASS 3.5e-3 PASS 5.0e-3 RUN FAILED (-1)
bf16/rrr PASS 6.5e-3 FAIL 1.40 RUN FAILED (-1)
bf16/ccr PASS 7.4e-3 FAIL 1.64 RUN FAILED (-1)
bf16/crr PASS 6.1e-3 FAIL 1.41 RUN FAILED (-1)

10 pass = all 8 square + the 2 rcr rectangular.

Why this doesn't contradict the #8123 "Fixed" update

  • That update's evidence — "fp16 rcr/rrr/ccr/crr max_rel 5.8e-4" — was measured on
    square problems. Every square case here passes too (all 8 green), so that
    number reproduces exactly.
  • On non-square, only rcr (each operand read in its native orientation) is
    correct. rrr/ccr/crr collapse to garbage (~1.3–1.6): the non-native
    leading dimension falls back to the square-coincident stride, so it only
    happens to be right when the mismatched dims are equal (Bug 1, the
    leading-dim defect).
  • awkward still throws -1 for every layout including rcr — that's the
    odd-N=129 vectorized C-store path (Bug 2), independent of Bug 1.

Net: the layout support is correct for square; rrr/crr/ccr are not yet
correct on non-square
problems. The parity suite is doing its job — catching a
real defect that square-only validation masked.

GPU run: ctr-cx65-mi300x-30 (MI300X / gfx942), python3 tests/test_gemm_parity.py.

Addresses the three review items on the TE->Dispatcher GEMM bridge driver:

1. Example configs to sweep
   - gemm_full_benchmark.py defaults to the selected variant's
     configs/default_ci_config.json (small CI sweep) when no config is
     passed, and to configs/example_problems.json when --problems is
     omitted; configs/default_config.json remains the full sweep.
   - New gemm_universal/configs/example_problems.json (square / rectangular
     / large M,N,K). Nightly-test JSON is meant to drop into the same
     configs/ dir -- no driver change needed.

2. Multi-GPU launch in parallel (supersedes grouped_conv's serial-GPU design)
   - Phase 3 fans the (kernel x problem) work across every visible GPU:
     one worker thread per device pulls batches from a shared queue and
     spawns a disposable subprocess pinned via HIP_VISIBLE_DEVICES, so an
     N-GPU box runs ~Nx faster while keeping per-batch fault isolation.
   - Devices auto-detected (HIP_VISIBLE_DEVICES, then rocm-smi / amd-smi);
     --devices overrides with a count (4) or explicit ids (0,2,5).
   - CSV gains a "device" column recording which GPU produced each row.

3. Organize per variant + README
   - --variant arg with a VARIANT_CONFIGS map
     (gemm_universal/gemm_multi_d/gemm_preshuffle/grouped_gemm) selecting
     the variant's configs/ dir; gemm_universal is wired today, the rest
     are registered for when their bridge paths land.
   - README gains a "Dispatcher Bridge Workflow" section (scripts, config
     layout, run examples, multi-GPU, supported dtype/layout) and a note
     that the legacy *_instance_builder.py is slated for deprecation once
     the bridge reaches parity.

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

Copy link
Copy Markdown
Contributor Author

Closing this draft PR as its changes are fully superseded by #8479 ([CK_TILE] TE -> Dispatcher GEMM bridge — all layouts + fp16/bf16).

PR #8479 is the consolidated, single-commit re-roll of the entire GEMM bridge work that was previously split across the stacked draft series (#8187#8190#8191#8193). Every file touched by this PR is included in #8479 at the same or an updated path. Please track further work in #8479.

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