Skip to content

[CK_TILE] Add Tile Engine -> Dispatcher bridge for GEMM#8123

Closed
ozturkosu wants to merge 15 commits into
developfrom
muozturk/dispatcher-gemm-bridge
Closed

[CK_TILE] Add Tile Engine -> Dispatcher bridge for GEMM#8123
ozturkosu wants to merge 15 commits into
developfrom
muozturk/dispatcher-gemm-bridge

Conversation

@ozturkosu

@ozturkosu ozturkosu commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the Tile Engine → Dispatcher bridge for GEMM, following the exact pattern FMHA and grouped conv already use: a single shared config dataclass owned by the dispatcher and imported by Tile Engine. There is no translator between two vocabularies — both sides share the one object whose .name is the runtime registry key.

Tile Engine: generate configs (search space)
        |
        v
[COMMON CONFIG STRUCT]  -- GemmKernelConfig (dispatcher-owned)
        |
        v
Dispatcher: codegen + build + runtime kernel
        |
        v
Tile Engine: run + benchmark

What's in this PR

  • dispatcher/python/gemm_utils.py (new) — the bridge:
    • GemmKernelConfig — the shared contract. .name reproduces the codegen KERNEL_NAME byte-for-byte. The warp/wave mapping (TE "warp" = wave count → wave_*; TE "warp_tile" = MFMA shape → warp_tile_*) lives in one place so it cannot drift.
    • setup_multiple_gemm_dispatchers() — codegen + hipcc → .so paths; CPU-only, parallel, returns paths (no GPU). Reuses ctypes_utils leaf helpers so codegen/compile have a single source of truth.
    • GemmProblem, GemmResult, GemmDispatcherLib, GpuGemmRunner, expand_sweep.
  • dispatcher/bindings/ctypes/gemm_ctypes_lib.cpp (upgrade) — adds the indexed multi-kernel ABI dispatcher_get_kernel_name_at(index, buf, size); the legacy single-kernel dispatcher_get_kernel_name is retained for backward compatibility.
  • tile_engine/ops/gemm/gemm_full_benchmark.py (new) — 3-phase TE driver (compile → load problems → benchmark). Generates no binaries.
  • tile_engine/ops/gemm/run_one_gemm_kernel.py (new) — disposable GPU worker for subprocess fault isolation (one bad kernel kills one worker, not the sweep).

Scope

Regular GEMM, fp16, rcr (Phase 1). stream_k / grouped_gemm / extra dtypes route through this same bridge in a later phase.

Verification

  • Name parity (end-to-end): GemmKernelConfig.name == generated .hpp stem == runtime registry name, confirmed on validated expand_sweep configs (6144 valid from the default config).
  • Build + ABI: one fp16/rcr kernel codegens, compiles, and links into a .so; the new dispatcher_get_kernel_name_at symbol is exported and the registry reports the expected name.

Test plan

  • Numeric parity (1024³ and 257³): bridge result == reference == old TE flow
  • Performance parity vs old TE flow (medians ≥10 runs, within ~2%)
  • Top-K fastest kernels match across old-TE and new-bridge flow
  • fp16/rcr sweep ≥99% pass (rejections explained)

Next Steps

Introduce the shared-config bridge that lets Tile Engine drive the GEMM
dispatcher the same way FMHA and grouped conv already do: one config
dataclass owned by the dispatcher, imported by Tile Engine, with no
translator between two vocabularies.

- dispatcher/python/gemm_utils.py: GemmKernelConfig (the common contract;
  .name mirrors the codegen KERNEL_NAME byte-for-byte), GemmProblem,
  GemmDispatcherLib, GpuGemmRunner, setup_multiple_gemm_dispatchers
  (codegen + hipcc -> .so paths, CPU-only/parallel), and expand_sweep.
- dispatcher/bindings/ctypes/gemm_ctypes_lib.cpp: add the indexed
  multi-kernel ABI dispatcher_get_kernel_name_at(index, buf, size);
  legacy single-kernel dispatcher_get_kernel_name retained.
- tile_engine/ops/gemm/gemm_full_benchmark.py: 3-phase TE driver
  (compile -> load problems -> benchmark) that generates no binaries.
- tile_engine/ops/gemm/run_one_gemm_kernel.py: disposable GPU worker
  for subprocess fault isolation.

Scope: regular GEMM, fp16, rcr (Phase 1). Name parity verified
end-to-end (config.name == generated .hpp stem == runtime registry name).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ozturkosu ozturkosu requested a review from a team as a code owner June 5, 2026 19:23
@ozturkosu ozturkosu marked this pull request as draft June 5, 2026 19:24
@ozturkosu ozturkosu self-assigned this Jun 5, 2026
@ozturkosu ozturkosu requested a review from Copilot June 5, 2026 20:13

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 Tile Engine ↔ Dispatcher bridge for GEMM, aligning GEMM with the existing FMHA/grouped-conv “shared config owned by dispatcher” pattern so Tile Engine can generate/search configs while the dispatcher handles codegen/compile/runtime dispatch.

Changes:

  • Introduces dispatcher/python/gemm_utils.py providing the shared GemmKernelConfig contract plus helpers to expand TE sweeps, build per-config .so libraries, and run kernels via a thin ctypes wrapper.
  • Extends the GEMM ctypes library ABI to support enumerating kernel names by index (dispatcher_get_kernel_name_at) while retaining the legacy single-kernel name API.
  • Adds Tile Engine-side benchmark driver + isolated subprocess worker for running GEMM kernels safely during sweeps.

Reviewed changes

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

File Description
projects/composablekernel/tile_engine/ops/gemm/run_one_gemm_kernel.py New isolated worker process that loads a compiled GEMM .so and runs one/batched kernels, returning JSON results.
projects/composablekernel/tile_engine/ops/gemm/gemm_full_benchmark.py New 3-phase benchmark driver (compile → load problems → benchmark via subprocess isolation) that consumes the dispatcher bridge.
projects/composablekernel/dispatcher/python/gemm_utils.py New dispatcher-owned GEMM bridge module defining GemmKernelConfig, sweep expansion, parallel build to .so, and runtime wrappers.
projects/composablekernel/dispatcher/bindings/ctypes/gemm_ctypes_lib.cpp Adds indexed multi-kernel name enumeration ABI while keeping the legacy single-kernel name function.

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

Comment on lines +270 to +276
except Exception as e:
print(f" Batch error: {e}")
try:
if proc and proc.poll() is None:
proc.kill()
except Exception:
pass
Comment on lines +59 to +61
parser.add_argument("--arch", default="gfx942")
parser.add_argument("--dtype", default="fp16")
parser.add_argument("--layout", default="rcr")

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.

Fixed in 35da60d9ff. Added module-level SUPPORTED_DTYPES/SUPPORTED_LAYOUTS and wired them into argparse choices=, so a value the Phase-1 runner can't honor now fails fast instead of silently benchmarking the wrong thing:

error: argument --dtype: invalid choice: 'bf16' (choose from 'fp16')

On this anchor PR the supported sets are fp16 / rcr. Propagated up the stack, widening each set to the branch's real capability: bf16 added in #8190, and rrr/crr/ccr in #8191 (column-major C stays out — ck_tile rejects it at build).

Comment on lines +523 to +525
for i in unique:
c = configs[i]
codegen_args.append(
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Phase 2 parity gate — all 4 test-plan items executed on gfx942 (MI300X), fp16/rcr

Everything below was produced through the bridge (expand_sweepGemmKernelConfigsetup_multiple_gemm_dispatchers codegen+hipcc → GpuGemmRunner in a disposable worker).

1. Numeric parity vs numpy fp32 reference — PASS

case M N K status max_rel
square baseline 1024 1024 1024 0 3.07e-04
awkward M 257 1024 512 0 2.51e-04
non-square 1536 2048 512 0 4.06e-04
large square 2048 2048 2048 0 4.02e-04

max_rel ~3–4e-04 = fp16 accumulation tolerance.

2. Performance medians (≥12 runs, +3 warmup) — STABLE

shape med_ms med_TFLOPS cv%
1024³ 0.027 80.8 0.9
2048³ 0.072 238.3 0.5
1536×2048×512 0.017 186.6 1.0
257×1024×512 0.015 18.0 2.9

CV ≤ 2.9% → reproducible.

3. Top-K fastest kernels — coherent & reproducible

Swept 48 configs → 32 unique kernels × 4 shapes. Top-1 is a 2x2x1-wave compv3 kernel on every shape; verified across two independent bridge runs:

shape top-5 overlap Jaccard
1024³ 5/5 1.00
2048³ 5/5 1.00
4096³ 4/5 0.67
1536×2048×512 3/5 0.43

Top-1 identical in both runs everywhere. Lower overlap on smaller shapes is boundary churn among near-ties (top-5 span < 1.5% TFLOPS).

Old-TE head-to-head: the legacy TE GEMM flow emits only .hpp codegen here — no compiled TE benchmark binary exists in this env and building the full engine is out of Phase-2 scope. Since the bridge reuses the same unified_gemm_codegen, kernels are byte-identical; ranking parity is shown via bridge reproducibility. Head-to-head vs a built TE binary is deferred.

4. fp16/rcr sweep pass rate — explained

  • Build: 32/48 built. 16 rejected at codegen, all 64x64x64_4x1x1_32x32x16 (wave 4 in M × warp_tile_m 32 = 128 > tile_m 64 — warps don't fit the block tile). Correctly declined; validate_kernel_config could pre-filter these (minor gap).
  • Run, pad-compatible shapes (1–4): 128/128 = 100% OK.
  • Run, M=257 (shape 5): 0/32, every kernel returns status -2 (no suitable kernel). default_config.json sets pad_*={false}, so M=257 (not ÷ tile_m=64) is correctly declined. Expected for no-pad kernels — the padded default kernel handles M=257 (see §1/§2), not a bridge fault.

Pass rate on shapes compatible with the swept kernels' pad settings: 100%.

Verdict

Phase-2 parity gate met: correct numerics, stable perf, coherent/reproducible top-K, fully-explained sweep. All rejections are the dispatcher faithfully surfacing real kernel constraints (no-pad divisibility, codegen geometry) shared with the legacy TE flow. Ready to route stream_k / grouped through the same bridge (Phase 3).

@ozturkosu

Copy link
Copy Markdown
Contributor Author

Status update — FMHA-gap closure (measured vs reference PR #5260)

Two of the generality gaps called out when benchmarking this bridge against the
FMHA reference are now addressed as stacked draft PRs on top of this one:

Gap vs FMHA #5260 Status PR
#1 — hard-coded KernelKey (registry misreported every kernel as fp16/rcr/128x128x32/compv4-cshuffle) Fixed — key now derived from the kernel's real codegen traits #8187
#2 — regular path fp16-only Fixed — bf16 added (dependency-free uint16 encoding); dtype detected from the compiled kernel name #8190 (stacked on #8187)

Both validated on gfx942 (MI300X):

Suggested land order: #8123#8187#8190.

Remaining toward full parity with the reference: layouts beyond rcr, and
driving the stack through review to merge.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

Update — third generality gap closed (layouts)

Following the gap-closure table above, the rcr-only gap is now also addressed:

Gap vs FMHA #5260 Status PR
#1 — hard-coded KernelKey Fixed #8187
#2 — fp16-only Fixed #8190
#3 — rcr-only Fixedrrr/rcr/crr/ccr all supported (layout-aware host transpose) #8191 (stacked on #8190)

Validated on gfx942 (MI300X), numeric vs CPU ref: fp16 rcr/rrr/ccr/crr max_rel 5.8e-4; bf16 rrr 7.8e-3 (dtype × layout compose). Column-major C is rejected at build by ck_tile's universal GEMM (row-major C only) — a kernel limitation, not a host issue.

Full stack land order: #8123#8187#8190#8191. All three FMHA generality gaps (hard-coded key, fp16-only, rcr-only) are now closed.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

Update — final gap closed (runnable example); stack complete

The last item from the FMHA-parity evaluation (gemm_pr_evaluation.md) — "FMHA shipped runnable examples; we shipped none" — is now addressed. Examples 01-11 drive the Dispatcher's native ctypes Registry; none exercised this bridge. New PR #8193 adds 12_te_bridge.py, which builds via setup_multiple_gemm_dispatchers and runs via GpuGemmRunner across fp16/bf16 and all four supported layouts, validating each vs a NumPy reference.

Full FMHA-parity stack (each PR stacked on the previous):

Gap vs FMHA #5260 Status PR
TE -> Dispatcher bridge (anchor) done this (#8123)
Hard-coded fp16/rcr registry key done #8187
fp16-only (no bf16) done #8190
rcr-only (no layouts) done #8191
No runnable example done #8193

Validation for #8193 (gfx942/MI300X, M=N=K=512): 6/6 pass — fp16 max_rel=2.5e-4, bf16 max_rel=3.9e-3.

All architectural generality gaps and the example gap are closed. The only remaining parity item is merge (#4 in the rubric), which needs human review — these are all still draft.

setup_multiple_gemm_dispatchers built its per-config codegen args without
the config's variant, and _generate_single_kernel_subprocess hard-coded
--variants standard. A GemmKernelConfig(variant='preshuffle'/'multi_d')
passed to the bridge would therefore be code-generated as a standard
kernel, while its name (and the hpp_glob_pattern derived from it) still
carried the variant suffix -- so the lookup could never match the emitted
header. Pass the variant through; the subprocess default stays "standard"
so all existing callers are unaffected.

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

Copy link
Copy Markdown
Contributor Author

Update — variant now threaded through the bridge codegen path (Copilot review item)

Addresses the automated-review flag that setup_multiple_gemm_dispatchers()
built its codegen args but ctypes_utils._generate_single_kernel_subprocess
hard-coded --variants standard. A GemmKernelConfig(variant='preshuffle'/'multi_d')
handed to the bridge would have been code-generated as a standard kernel,
even though its name (and the hpp_glob_pattern derived from it) already
carries the variant suffix — so the post-codegen lookup could never match the
emitted header.

Fix (commit 92a8d9f039):

  • gemm_utils.py::setup_multiple_gemm_dispatchers now passes "variant": c.variant
    in each per-config codegen arg dict.
  • ctypes_utils.py::_generate_single_kernel_subprocess now uses
    args.get("variant", "standard") for --variants.

Backward-compatible: the subprocess helper is shared, pre-existing develop
infra (introduced in #5168). The .get(..., "standard") default means every
existing caller that does not pass a variant key behaves exactly as before —
verified that a default GemmKernelConfig still resolves to standard, and the
10 CPU unit tests still pass. Only the bridge path changes: it now generates the
kernel the config actually asked for.

Fix committed here and propagated up the stack (#8187#8190#8191#8193).

…dler

If subprocess.Popen() itself raises, the generic except handler referenced
proc before it was bound, masking the real error with an UnboundLocalError.
Initialize proc = None before the try; the handler already guards with
`if proc and proc.poll() is None`.

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

Copy link
Copy Markdown
Contributor Author

Update — fixed UnboundLocalError risk in the GEMM benchmark (Copilot review item)

Addresses the flag on tile_engine/ops/gemm/gemm_full_benchmark.py: in the
batch loop's generic except Exception handler, proc was referenced before
binding. If subprocess.Popen(...) itself raised (no GPU, bad interpreter,
etc.), the handler would throw UnboundLocalError and mask the real error.

Fix (commit 8f012bdb98): initialize proc = None before the try; the
handler already guards with if proc and proc.poll() is None, so a failed
Popen now reports the actual exception. Propagated up the stack
(#8187#8190#8191#8193).

Copilot review item: the driver exposed unrestricted --dtype/--layout while
the Phase-1 worker hard-codes fp16 inputs and an rcr (column-major B) host
transpose. Passing e.g. --dtype bf16 would codegen bf16 kernels but feed them
fp16 data, silently benchmarking the wrong thing. Add SUPPORTED_DTYPES/
SUPPORTED_LAYOUTS and wire them into argparse choices so a mismatch fails fast.

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

Copy link
Copy Markdown
Contributor Author

How to use the GEMM bridge

Three entry points, all driven by the single shared GemmKernelConfig whose .name is the runtime registry key. Phase-1 scope on this PR is fp16 / rcr; bf16, additional layouts, and a runnable example arrive in the stacked PRs (#8190 / #8191 / #8193).

Prereq: build the dispatcher first so dispatcher/build/libck_tile_dispatcher.a exists (cmake + make).

A. CLI sweep — the benchmark driver

cd projects/composablekernel/tile_engine/ops/gemm
python3 gemm_full_benchmark.py gemm_universal/configs/default_config.json \
    --arch gfx942 --dtype fp16 --layout rcr --csv gemm_results.csv
# optional: --problems shapes.json  --workers 8  --batch-size 20  --max-kernels 50

Three phases: compile (CPU, parallel — returns .so paths only, no GPU) -> load M,N,K problems -> benchmark each kernel in a disposable worker subprocess (one GPU fault takes down one worker, not the sweep). Results stream to the CSV.

--dtype / --layout are now constrained to what the runner actually supports (choices), so a mismatch fails fast instead of silently benchmarking the wrong thing:

error: argument --dtype: invalid choice: 'bf16' (choose from 'fp16')

B. Programmatic — the gemm_utils.py bridge API

import numpy as np
from gemm_utils import (
    GemmKernelConfig, GemmProblem, GpuGemmRunner, setup_multiple_gemm_dispatchers,
)

cfg = GemmKernelConfig()                        # fp16/rcr defaults, 128x128x32, 2x2x1 wave
(so,) = setup_multiple_gemm_dispatchers([cfg])  # CPU codegen + hipcc -> .so path (no GPU)
runner = GpuGemmRunner(so)                       # loads .so, reads its registry name

M = N = K = 1024
A = np.random.randn(M, K) * 0.1                  # pass logical A(MxK), B(KxN) row-major;
B = np.random.randn(K, N) * 0.1                  # the kernel name dictates dtype + layout
res = runner.run(A, B, GemmProblem(M, N, K))
print(res.status, f"{res.tflops:.1f} TFLOPS", res.kernel_name)   # status 0 == OK
  • setup_multiple_gemm_dispatchers([...]) is the build half — pure CPU, parallel, returns .so paths aligned to input order (None for configs that failed to codegen/compile).
  • GpuGemmRunner.run(A, B, problem) is the run half — handles the host transpose and dtype encoding internally based on the compiled kernel name.
  • cfg.name reproduces the codegen KERNEL_NAME byte-for-byte == the runtime registry key.

Accuracy note: use global relative error max|out-ref| / max|ref|, not per-element — K-length accumulation produces near-zero reference entries where a per-element ratio explodes.

C. Sweep expansion from a TE config

from gemm_utils import expand_sweep
configs = expand_sweep("gemm_universal/configs/default_config.json", "gfx942",
                       dtype="fp16", layout="rcr")   # -> List[GemmKernelConfig]

Expands the TE tile_config / trait_config cross-product, drops invalid combinations via the dispatcher's validator, and collapses duplicates by .name. Hand the list straight to setup_multiple_gemm_dispatchers.

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.

it will be good to include example config files to sweep. And later include the json used in nightly tests.

)

# ========================================================================
# Phase 3: Benchmark via subprocess (serial GPU, batched)

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.

please see if you can launch on multiple GPU devices in parallel. the serial gpu design in Conv might be an inferior design compared to fmha launcher.

@yraparti

yraparti commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

the code is still not organized as per the gemm variant. Also please add a README once it is done.
Maybe depricate the old instance builder once it reaches parity.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

the code is still not organized as per the gemm variant. Also please add a README once it is done. Maybe depricate the old instance builder once it reaches parity.

Parity check is done. Look at #8193 (comment)

However there is a validity issue for (rrr, crc, ccr) maybe we can land this PR now as only rcr and then focus on remaning format.

#8193 (comment)

Addresses the three review items on the TE->Dispatcher GEMM bridge driver,
scoped to this foundation PR's fp16/rcr surface (bf16/layouts follow in the
#8190/#8191 stack):

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 drops 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 with 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);
     override with --devices (count, explicit ids, or all).

3. Variant organization + README + deprecation note
   - --variant selects the per-variant configs/ directory.
   - New README "Dispatcher Bridge Workflow" section: scripts, per-variant
     config layout, run examples, multi-GPU explanation, supported surface
     (fp16/rcr here), and a deprecation note for the legacy
     *_instance_builder.py generators.

Driver --dtype/--layout choices stay fp16/rcr to match this PR's dispatcher
host path; run_one_gemm_kernel.py (fp16 host gen) is unchanged.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Layout × awkward-size sweep: old-TE ground truth + root cause

Following up on the "full layout × dtype gfx942 sweep failed for rrr/rcr/crr/ccr at awkward sizes like 257×129×512" note. I re-ran those shapes on old TE built from develop (fp16-only build, one padded kernel per layout: 128x128x32_2x2x1_32x32x16, pad_m/n/k=true, compv3_cshuffle_intrawave) with CK_TILE_LOGGING=1.

Ground truth (old TE, develop branch)

shape (M×N×K) rcr rrr crr ccr tests
256×128×512 PASS PASS PASS PASS control (all aligned)
264×136×512 PASS PASS PASS PASS %8 ok but not tile-aligned → padding works
257×128×512 PASS PASS REJECT A/M REJECT A/M M odd
256×129×512 REJECT C/N REJECT B/N REJECT B/N REJECT C/N N odd
257×129×512 REJECT C/N REJECT B/N REJECT A/M REJECT A/M the "awkward size"

Rejections are the kernel's own raw strings, e.g. [CK_TILE_ERROR] N is not a multiple of vector load size for C tensor!.

Conclusion: this is not a bridge bug. Old TE rejects 257×129×512 on every layout using the identical kernel and IsSupportedArgument the bridge dispatches to. The bridge faithfully reproduces a documented ck_tile constraint.

Root cause

fp16 vector width = 16 B / 2 B = 8 elements. universal_gemm_kernel.hpp::IsSupportedArgument requires the contiguous dimension of each operand to be % 8 == 0. This check is unconditional — padding (pad_m/n/k) does NOT relax it. Padding only relaxes the % PerBlock tiling check (that's why 264×136 passes despite not being tile-aligned).

Which dim is contiguous depends on layout (3-char = A-major, B-major, C-major):

operand RowMajor → check ColMajor → check
A K%8 (=512, always ok here) M%8
B N%8 K%8 (=512, always ok here)
C N%8 M%8

Checks fire in order A → B → C, so the reported dim is the first violator:

  • C is row-major (__r) in all four layouts → N%8 is required by every layout. N=129 ⇒ all reject.
  • A col-major (c__ = crr, ccr) → M%8. M=257 ⇒ crr/ccr reject on A first (masking the N failure).
  • B row-major (_r_ = rrr, crr) → N%8. N=129 ⇒ rrr reports B.

Every cell in the table matches this rule.

Fix options

  1. (Recommended) Host-side pad to vector width. Round M and N up to the next multiple of 8 (257→264, 129→136), run the GEMM, slice the output back to 257×129. Throwaway rows/cols are discarded; cost is a small alloc + copy, negligible vs. the GEMM. Makes arbitrary shapes work transparently through the bridge with no kernel changes.
  2. Scalar-store fallback kernel. Emit a VectorSizeC=1 variant selected when N%8≠0 — supports any shape at lower store bandwidth.
  3. Document the constraint. fp16 universal GEMM requires each operand's contiguous dim to be a multiple of 8 (deliberate 16-byte vectorized-access HW design), not a defect.

Reproduction

Built from a develop git worktree, fp16-only, via a minimal gemm_universal/configs/parity_layouts.json (1 padded kernel/layout) and GEMM_UNIVERSAL_CONFIG_FILE. Driver: run_layout_parity.sh (5 shapes × 4 layouts, CK_TILE_LOGGING=1).

…duler

The codegen arch filter hard-coded pipeline=compv4 / scheduler=intrawave when
validating tile geometry, ignoring each config's actual traits. Since compv4
has the strictest MFMA constraints, tiles legal under mem/compv3 were wrongly
rejected -- collapsing the generated fp16/rcr set from ~1520 to 512 kernels
(compv3 and mem each decimated ~5x; compv4 roughly preserved).

Thread the trait's real pipeline/scheduler into _is_tile_arch_valid at both
call sites; the tile pre-filter now keeps a tile if it is legal under any
configured pipeline/scheduler, with the precise per-trait check deferred to
_get_configs_for_variant. Verified on the 6144 fp16/rcr config population:
emitted kernels 512 -> 1520 (compv3 464, compv4 128, mem 928), and a
previously-rejected compv3 64x64x192 config now generates a header end-to-end.

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

Copy link
Copy Markdown
Contributor Author

Codegen arch filter: validate tiles against the real pipeline/scheduler (commit 4d14777)

Bug

While investigating why the bridge built far fewer fp16/rcr kernels than old Tile-Engine (522 .so vs 2385 old-TE binaries), I traced the dominant cause to the codegen arch filter.

_is_tile_arch_valid in dispatcher/codegen/unified_gemm_codegen.py hard-coded pipeline="compv4" / scheduler="intrawave" when checking tile geometry, and the trait was never threaded in (_get_configs_for_variant and the _get_tile_configs pre-filter both called it trait-blind). Because compv4 carries the strictest MFMA-geometry constraints, tiles that are perfectly legal under mem or compv3 were judged as if they were compv4 and dropped.

Evidence (fp16/rcr, gfx942)

Two validators disagreed:

Stage Count
validate_kernel_config (dispatcher's own validator) 6144
codegen arch filter as it actually ran (hard-coded compv4) 512
codegen arch filter using each config's real pipeline/scheduler 1520
old-TE instance builder (no gfx942 arch filter at all) 2385

Generated-header counts on disk matched the bug signature — compv4 roughly preserved, compv3/mem decimated ~5x:

pipeline bridge (buggy) old-TE
compv4 141 224
compv3 141 721
mem 272 1440

Fix

Thread the trait's real pipeline/scheduler into _is_tile_arch_valid at both call sites. The tile pre-filter now keeps a tile if it is legal under any configured pipeline/scheduler, deferring the precise per-trait check to _get_configs_for_variant. compv4/intrawave remain the fallback defaults when no trait is supplied (e.g. preshuffle still forces its own).

Verification

  • On the 6144-config population the emitted set goes 512 → 1520 (compv3 464, compv4 128, mem 928).
  • A previously-rejected compv3 ... 64x64x192 config now generates a header end-to-end through the real codegen subprocess. (Bonus: some tile_k=192 failures previously attributed to a "separate codegen bug" were in fact this same hard-code.)

Residual gap

1520 vs old-TE's 2385 is expected: old TE applies no gfx942 arch filter, so it builds whatever its instance builder enumerates (some of which may be arch-invalid on gfx942). The bridge's ArchFilter is deliberately conservative; closing the rest is a separate question about arch-filter strictness, not this bug.

…artifact, not a speedup)

The sweep's >=20% "bridge faster" cells (all compv4/intrawave/1024^3) are NOT a
bridge speedup. Proven on MI300X: the device kernel is byte-identical (same
ck_tile::kentry<1,GemmKernel<...>> symbol, rocprof), and through any uniform
harness it runs at the same speed on both sides.

Ruled out empirically: kernel, compiler/flags (4 toolchain rebuilds incl.
clang++-HIP with old-TE flags all give ~189), all bench knobs
(warmup/repeat/flush/rotating/timer), allocation/placement (DeviceMem, 4GB
decoys), and stale timing headers (byte-identical across trees). rocprof
confirms the slowdown is real device time (13.78us vs 11.34us): old TE's
*standalone benchmark binary* runs the identical kernel ~18-20% slower purely
due to that process's GPU clock/execution state (+8% stall cycles under PMC,
plus ~13% lower sustained SCLK).

Fix:
- ab_same_harness.py: apples-to-apples A/B that builds the old-TE kernel into a
  .so and runs BOTH it and the bridge .so through the SAME worker. Gap collapses
  to ~+/-0.5% at 1024^3 (was +20..+24% vs the standalone binary). Proof in
  ab_same_harness.out.
- diagnose.md sec.4 rewritten: the prior hipcc-vs-clang++ toolchain theory is
  disproven and replaced with the evidence above.
- generated_tile_backend.hpp: correct the misleading comment that claimed
  matching bench knobs makes bridge-vs-old-TE "apples-to-apples".

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

Copy link
Copy Markdown
Contributor Author

The >=20% "bridge faster" cells are a benchmark-harness artifact, not a speedup (commit db55f7e)

Followed up on the suspicious cells in the fp16/rcr sweep (all 16 are compv4 + intrawave + 1024^3, +20..+24%). Investigated on MI300X (gfx942). The prior diagnose.md theory (hipcc-vs-clang++ toolchain asymmetry) is disproven. The device kernel is byte-identical and runs at the same speed on both sides; the gap comes entirely from old TE's standalone benchmark binary under-measuring its own kernel.

Ruled out (each tested on-GPU)

Hypothesis Test Result
Different kernel rocprof symbol identical kentry<1,GemmKernel<...>>, 150 dispatches, gap=0, no flush kernels either side
Compiler/flags rebuilt bridge kernel 4 ways incl. clang++ -x hip + old-TE flags; compiled old-TE header via bridge path all 187-189 TFLOPS (never 156)
Bench knobs toggled warmup/repeat/flush_cache/rotating_count/timer on both no effect (bridge ~188, old-bin ~156); defaults already identical, stream_config fields line up
Allocation/placement DeviceMem vs raw hipMalloc; +4GB decoy allocs all 190
Stale timing code diff all host timing headers across trees; compile mini vs both trees byte-identical; both 190
Measurement bug rocprof hardware timestamps slowdown is real device time: 13.78us vs 11.34us

What it actually is

A clean standalone harness measures the old-TE kernel at 189-194; old TE's standalone benchmark binary measures the same kernel at 156. PMC counters: old-TE kernel = 366k cycles vs 339k (+8% stall cycles); non-PMC wall ratio (1.22) > cycle ratio (1.08) -> remaining ~13% is lower sustained SCLK in that process. Shape-selective because large shapes hit the power cap and clocks converge; 1024^3 has headroom so the two processes' DPM governors diverge most.

Fix: measure both kernels through the SAME harness (ab_same_harness.py)

Builds the old-TE kernel into a .so from old TE's own generated header and runs both it and the bridge .so through the same run_one_gemm_kernel.py worker. Gap collapses to ~+/-0.5%:

shape bridge oldTE gap%
512^3 38.77 38.78 -0.01
1024^3 189.19 189.41 -0.12
2048^3 295.59 297.10 -0.51
4096^3 369.48 369.85 -0.10

So bridge and old TE run the same kernel at the same speed. Also corrected the misleading "matching bench knobs => apples-to-apples" comment in generated_tile_backend.hpp. Full evidence in diagnose.md §4.

Resolve CMakeLists.txt conflict in tile_engine/ops/gemm: keep develop's
expanded op list and new add_subdirectory entries while preserving this
branch's retirement of legacy gemm_universal (dropped from both budget
foreach loops and from add_subdirectory).
Remove dispatcher/parity_diag/regression/diagnose.md from the PR; the
content now lives on Confluence (MLSE) as a child page under the fp16/rcr
A/B sweep report:
https://amd.atlassian.net/wiki/spaces/MLSE/pages/1737132108
@ozturkosu ozturkosu marked this pull request as ready for review June 12, 2026 00:40
@ozturkosu ozturkosu requested a review from a team as a code owner June 12, 2026 00:40
@ozturkosu ozturkosu requested a review from Copilot June 12, 2026 02:31

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

Comment thread projects/composablekernel/dispatcher/parity_diag/regression/ab_same_harness.py Outdated
Comment thread projects/composablekernel/dispatcher/parity_diag/regression/ab_same_harness.py Outdated
- ab_same_harness.py: derive ROOT from __file__ and take old-TE header dir
  from OLD_TE_GEN env var (was hard-coded dev paths); drop unused statistics import
- generated_tile_backend.hpp: make env_bool case-insensitive (handles
  False/Off) and align its comment
- gemm_full_benchmark.py: clarify in --devices help and resolve_devices
  docstring that a bare digit is a count; a single id needs the comma form (5,)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ozturkosu added a commit that referenced this pull request Jun 12, 2026
…ridge

# Conflicts:
#	projects/composablekernel/dispatcher/python/gemm_utils.py
ozturkosu added a commit that referenced this pull request Jun 12, 2026
The Stream-K bridge (#8136) was branched at #8123's first commit, so it
lacked all subsequent regular-GEMM bridge improvements (arch-validated
tile filtering, the develop merge + legacy gemm_universal retirement,
benchmark-param/--verify work on the shared driver, README). Merge the
current #8123 HEAD to pick those up; the Stream-K-specific analogues that
live in the duplicated driver/worker/ctypes lib are ported in follow-up
commits.

Sole conflict: dispatcher/python/gemm_utils.py variant threading. Kept
the Stream-K routing (_ctypes_source_name -> streamk_gemm_ctypes_lib.cpp,
.name _streamk suffix, variant through codegen_args/expand_sweep) and
adopted #8123's explanatory comment.
ozturkosu added a commit that referenced this pull request Jun 12, 2026
The Stream-K bridge keeps its own driver, worker and ctypes lib, so the
regular-GEMM bridge improvements that landed on #8123 after this branch
forked did not arrive via the merge. Port the Stream-K-specific analogues:

- streamk_gemm_ctypes_lib.cpp: benchmark knobs now default to old-TE's
  warmup=50/repeat=100 (was 3/10 -- a cold, un-ramped clock, the root of
  #8123's spurious "perf gap") and are env-overridable via
  CK_TILE_BENCH_WARMUP/REPEAT/FLUSH/ROTATING. Unlike the regular path,
  rotating_count defaults to 1: the Atomic preprocess re-zeros only the
  original C buffer, so rotating C would corrupt the accumulation.
- streamk_gemm_full_benchmark.py: fan the (kernel x problem) work across
  every visible GPU (device-pinned HIP_VISIBLE_DEVICES workers, --devices,
  device CSV column), add the --verify/--verify-tol fp32-reference gate, and
  constrain --dtype/--layout to the supported fp16/rcr surface. Also fixes a
  latent proc-unbound error in the batch handler.
- run_one_streamk_gemm_kernel.py: add the fp32 numpy reference check
  (global max|out-ref|/max|ref|, verified/max_rel) behind --verify.
- README: document the Stream-K bridge driver/worker, flags, _streamk name
  suffix, fp16 Atomic tolerance, and the rotating_count divergence.

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

Copy link
Copy Markdown
Contributor Author

GEMM Parity — Old TE engine vs Dispatcher bridge (PR #8123)

Head-to-head performance + validity comparison between the old Tile-Engine
GEMM benchmark
(built GEMM-only from a develop worktree) and the new
TE→Dispatcher bridge
(single-kernel .so via unified_gemm_codegen + hipcc,
driven through run_one_gemm_kernel.py).

Scope — the buildable-common fp16/rcr set: every kernel that
tile_engine/ops/gemm/configs/default_config.json expands to and for which an
old-TE benchmark_gemm_universal_* binary exists → 1192 kernels, each run at
5 problem configurations = 5960 instances.

Shapes (M×N×K) — every dim is a multiple of 768 = lcm(64,128,192,256) so every
tile geometry divides it (no spurious no-pad rejections), and 768 % 8 = 0 keeps the
fp16 vectorized K-reduction valid:
768³, 1536³, 3072³, 3072×768×1536 (M-heavy), 768×3072×1536 (N-heavy).

Matched knobs (both engines) — warmup=50, repeat=100, flush_cache=true,
rotating_count=1000 (bridge via CK_TILE_BENCH_* env; old-TE via -warmup/-repeat,
its flush/rotating defaults already match). Old-TE perf uses -verify=0; old-TE
correctness uses a separate -verify=1 cold run.

Artifacts: dispatcher/parity_diag/latestparidity.csv (all 5960 rows),
full_parity.py (harness).


Coverage

metric value
kernels 1192
shapes per kernel 5
instances (kernel × shape) 5960
bridge produced perf 5960 / 5960
old-TE produced perf 5830 / 5960
no-compare (old-TE perf n/a) 130

The 130 no-compare instances are exactly the 26 192×*_2x2x1_32x32x16 kernels
(× 5 shapes) — see Validity below; the old-TE binary rejects them at runtime.

Validity — both engines, independent

check pass fail
bridge vs fp32 numpy max_rel ≤ 2e-2 (per instance, 5960) 5840 120
old-TE self--verify=1 (per kernel, 1192) 1166 26

Functional parity is clean. Every validity failure on both engines is the
same degenerate geometry family — 192-wide tiles paired with 2x2x1_32x32x16
warps
(wave coverage 2×32 = 64 ≠ 192). Old-TE fails 26 of these; the bridge fails
24 (the bridge is correct on the two compv4_…_192x64x64 cases that old-TE gets
wrong). No kernel that old-TE computes correctly is computed incorrectly by the
bridge. This family is a shared codegen issue, not a bridge regression.

Performance parity — overall (gap = (bridge − oldTE) / oldTE)

Over the 5830 comparable instances:

stat value
median gap +0.51%
mean gap +3.29%
stdev 14.49%
within ±2% 22.3%
within ±5% 45.7%
within ±10% 76.9%
within ±20% 90.5%
min / max −50.8% / +204.8%

The distribution is centered on parity (median ≈ 0) but has a wide, two-sided
spread
: the bridge and old-TE are not performance-identical for a meaningful
fraction of configs.

Per shape

shape n median mean stdev ±10% ±20%
768×768×768 1166 +5.11% +7.68% 11.83% 74%
1536×1536×1536 1166 −2.80% +0.77% 15.06% 75%
3072×3072×3072 1166 +3.57% +6.22% 13.88% 81%
3072×768×1536 1166 −2.83% +0.81% 14.93% 76%
768×3072×1536 1166 −2.79% +0.99% 14.90% 78%

Dispersion is not just a small-shape launch-overhead artifact: the large,
compute-bound 3072³ shape still shows stdev ≈ 13% and ~33% of instances outside
±10%, so a real fraction of name-matched kernels run at genuinely different speeds
on the two paths.

Where the divergence concentrates

  • mem-pipeline 256x256x64 family — the largest, reproducible gaps in both
    directions. Manually re-checked mem_cshuffle_intrawave_…_True_256x256x64_2x2x1_32x32x16
    @3072³: old-TE 85.4 / 85.3 / 85.5 TFLOPS vs bridge 263.3 / 263.0 / 262.3
    (+205%, stable across 3 runs each). The sibling mem_default_…_256x256x64 runs the
    other way (≈ −50%). Correct output on both sides — so this is a scheduling /
    effective-kernel divergence
    , not a numerical bug.
  • compv3 192-wide tiles (e.g. 192x256x64, 192x128x64) — bridge consistently
    ~+40–45% faster at 3072³.
  • Some large-tile compv3 (e.g. 128x64x128, 64x64x128) — bridge −20…−27%
    slower at 3072³.

10 largest deficits / surpluses (full list in CSV)

kernel shape bridge oldTE gap
mem_default_intrawave_…_True_256x256x64_2x2x1_32x32x16 3072×768×1536 31.1 63.3 −50.8%
mem_default_intrawave_…_True_256x256x64_2x2x1_32x32x16 3072³ 125.5 254.0 −50.6%
mem_cshuffle_interwave_…_False_192x64x64_4x1x1_16x16x16 3072³ 228.9 340.1 −32.7%
compv3_cshuffle_…_True_128x64x128_2x2x1_32x32x16 3072³ 253.9 349.2 −27.3%
mem_cshuffle_intrawave_…_True_256x256x64_2x2x1_32x32x16 3072³ 263.6 86.5 +204.8%
mem_cshuffle_intrawave_…_True_256x256x64_2x2x1_32x32x16 768³ 18.4 6.6 +179.8%
mem_default_intrawave_…_True_256x256x64_4x1x1_32x32x16 3072³ 233.8 87.0 +168.8%
compv3_default_…_False_192x256x64_2x2x1_16x16x16 3072³ 445.3 311.1 +43.1%

Bottom line

  • Validity: parity confirmed. Bridge and old-TE produce the same correct results
    everywhere except one shared degenerate tile/warp family (which the bridge actually
    handles slightly better).
  • Performance: median parity (+0.5%), but not kernel-for-kernel identical. ~91% of
    comparisons fall within ±20% and most divergence is benign, but a real subset —
    led by the mem-pipeline 256x256x64 family (reproducible 2–3×) — shows the
    two paths are not emitting/launching performance-equivalent kernels for every
    config. These are the concrete follow-ups for closing GEMM perf parity; they do not
    affect correctness.

The bridge compiled GEMM kernels with only
-mllvm -enable-noalias-to-md-conversion=0, omitting the AMDGPU
inlining/codegen flags Tile Engine passes (-amdgpu-early-inline-all=true,
-amdgpu-function-calls=false, --lsr-drop-solution=1, -enable-post-misched=0,
-fno-offload-uniform-block). Those flags change register allocation and thus
occupancy. Persistent GEMM kernels size their grid by occupancy
(UniversalGemmKernel::MaxOccupancyGridSize = #CUs x occupancy), so the mismatch
produced large perf gaps vs Tile Engine on persistent tiles (up to ~-73% on
some mem-pipeline 256x256x64 / 192x64x128 kernels) while non-persistent kernels
(fixed problem-sized grid) were unaffected.

Matching the flags restores parity: on the full persistent buildable-common
fp16/rcr subset (596 kernels x 5 shapes), the bridge-vs-Tile-Engine gap
collapses to a median ~-2% with no kernel beyond +/-20% (was -51.7% .. +203.8%).
Numerically unchanged (max_rel ~3.5e-4).

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

Copy link
Copy Markdown
Contributor Author

Perf parity fix — match Tile Engine's AMDGPU codegen flags (d04623df41)

Symptom

A full A/B sweep (bridge vs old Tile Engine, fp16/rcr, 1192 kernels × 5 shapes) had most kernels at parity but a tail of large per-kernel gaps — up to −51.7% and +203.8% — and every outlier was a persistent=True kernel (mem pipeline 256x256x64 / 192x64x128 tiles especially). Non-persistent kernels were within noise.

Root cause

The bridge compiled GEMM kernels with only -mllvm -enable-noalias-to-md-conversion=0, while Tile Engine also passes these AMDGPU codegen flags:
-mllvm -amdgpu-early-inline-all=true -mllvm -amdgpu-function-calls=false -mllvm --lsr-drop-solution=1 -mllvm -enable-post-misched=0 -fno-offload-uniform-block.

They steer inlining / register allocation, which changes kernel occupancy. Persistent GEMM sizes its grid by occupancy (UniversalGemmKernel::MaxOccupancyGridSize = #CUs × hipOccupancyMaxActiveBlocksPerMultiprocessor), so the flag mismatch changed the launched grid and produced the gaps. Non-persistent kernels use a fixed problem-sized grid → unaffected. Demangling the compiled kernels confirmed the device-code template signature is identical on both paths; only the compile flags differed.

Secondary note: the original sweep's old-TE baseline was built from a stale develop checkout predating CK #5854/#6744, which made a few persistent kernels anomalously slow in the baseline — that produced the apparent "+200% surpluses". Rebuilding the baseline on current develop removes those.

Proof (single kernel, @3072³, same GPU / ROCm / ck_tile)

kernel old-TE bridge (before) bridge (after fix)
mem_default …True_256x256x64_2x2x1_32x32x16 255.4 123.7 (−51.6%) 262.2 (+2.7%)
mem_cshuffle …True_256x256x64_1x4x1_32x32x16 267.6 72.7 (−72.8%) 266.8 (−0.3%)

Fix

d04623df41 adds the same -mllvm flag set to the bridge compile in gemm_utils.py. Output numerically unchanged (max_rel ~3.5e-4).

Confirmation — full persistent subset (fixed bridge vs current-develop Tile Engine)

596 persistent fp16/rcr kernels × 5 shapes, matched knobs (warmup 50 / repeat 100 / flush / rotating 1000):

  • median gap −2.1%, 98.6% within ±10%, 0 kernels beyond ±20% (was −51.7% … +203.8%)
  • mem pipeline specifically: median −2.2%, range −10.2 / +8.6%, 0 beyond ±20%

The residual ~±10% is ordinary codegen dispersion present on both engines, not a bridge effect.

@ozturkosu

Copy link
Copy Markdown
Contributor Author

Confirmation complete — full persistent subset (596 kernels × 5 shapes)

The post-fix A/B run finished (fixed bridge vs Tile Engine, both built from current develop ck_tile, matched knobs). 2915 instances with a gap:

metric before fix (persistent) after fix
median gap +0.9% −1.2%
stdev 16.2% 3.6%
within ±10% ~78% 97.7%
kernels beyond ±20% many 0
min / max −51.7% / +203.8% −19.4% / +9.9%

The mem-pipeline 256x256x64 / 192x64x128 outliers are eliminated. The worst residual (~−15…−19%) is a handful of skinny-tile compv3 256x64x64 / 64x256x64 kernels at rectangular shapes — ordinary per-kernel codegen dispersion present on both engines, not the systematic 2× gap the flags caused. Bridge is now performance-equivalent to Tile Engine on the persistent path.

ozturkosu added a commit that referenced this pull request Jun 16, 2026
…ridge (#8261)

Brings #8261 current with #8123's latest work: arch-validated tile filtering,
matching Tile Engine AMDGPU -mllvm codegen flags, the --verify fp32-reference
gate, perf-gap diagnosis correction, Copilot nits, and the develop catch-up.

Conflict resolution (tile_engine/ops/gemm):
- Adopted #8123's flat op-root configs/ layout for gemm_universal (gemm_universal/
  removed; matches the fmha/grouped_conv bridge convention and the on-disk rename).
- Kept #8261's superset support surface: SUPPORTED_DTYPES=(fp16,bf16),
  SUPPORTED_LAYOUTS=(rcr,rrr,crr,ccr) in driver + README, since #8261 already
  wires bf16 and all four layouts through the dispatcher host path.
- Took #8123's --verify driver/worker wiring and device-count semantics.
- example_problems.json: identical content, kept at configs/.
@ozturkosu

Copy link
Copy Markdown
Contributor Author

Superseded by #8479 — work moved there

Closing this PR. The GEMM bridge work has been moved to #8479
(muozturk/gemm-bridge-all-layouts-bf16).

This branch's history had accumulated unrelated cross-project commits through
repeated develop merges, making the PR hard to review. #8479 is a single clean
commit off the latest develop
containing only the GEMM-bridge files
(35 files, all under projects/composablekernel/).

All functionality from this PR is preserved in #8479: the TE → Dispatcher regular
GEMM bridge with all four layouts (rcr/rrr/crr/ccr) and fp16 + bf16,
trait-derived registry KernelKey, --verify correctness gate, Tile Engine
AMDGPU codegen-flag parity, multi-GPU benchmarking, the runnable example, and the
parity/unit tests.

Please review #8479 instead.

@ozturkosu ozturkosu closed this Jun 16, 2026
ozturkosu added a commit that referenced this pull request Jun 30, 2026
Re-stacks the grouped_gemm bridge on top of the all-layouts GEMM bridge
(muozturk/gemm-bridge-all-layouts-bf16, #8479) instead of the now-closed
#8123 base. Squashes the prior grouped-branch history (was 5e665a3..
d075b90 on the old base) into the net grouped delta replayed onto #8479.

Contents:
- codegen: grouped variant + launch generator (arch_filter.py,
  unified_gemm_codegen.py), plus the standalone 02_grouped_gemm_driver.cpp
  and README sections (absorbed from the closed #8075).
- bridge: multi-problem registry-bypass ctypes ABI
  (grouped_gemm_ctypes_lib.cpp), GpuGroupedGemmRunner + dtype/layout codecs
  (gemm_utils.py), TE driver/worker harness, GROUPED_GEMM_BRIDGE.md.

Kept both #8479's GEMM_KEY_* registry descriptors and grouped's
ALayout/BLayout/CLayout single-include exports. Coverage: {rcr,rrr,ccr,crr}
x {fp16,bf16,fp8,bf8}; validated at Old-TE parity on MI300X/gfx942.
ozturkosu added a commit that referenced this pull request Jul 7, 2026
…8997)

> Re-opened from #8479 with a compliant branch name
(users/muozturk/ck-tile/gemm-bridge-all-layout-bf16-fp16). Supersedes
#8479.

## Summary

This PR routes the **Tile Engine (TE) regular-GEMM sweep through the
Dispatcher**,
making the Dispatcher the single source of truth for **codegen → build →
runtime**
while the Tile Engine keeps only the **config search space** and the
**benchmark
loop**. It is the consolidated, **single-commit** GEMM bridge covering
**all four
layouts (`rcr`/`rrr`/`crr`/`ccr`)** and **both `fp16` and `bf16`**.

It is a clean re-roll of the earlier bridge work (previously split
across
#8123 + the stacked key/bf16/layouts/parity/example PRs and consolidated
in
#8261). Those branches accumulated unrelated cross-project commits
through repeated
`develop` merges; **this branch is a single clean commit off the latest
`develop`**
containing only the GEMM-bridge files. It supersedes and replaces #8123
/ #8261.

## Motivation

The Tile Engine historically owned its own codegen/build/runtime for
GEMM
(`tile_engine/ops/gemm/gemm_universal/`). The consolidation goal is for
the
**Dispatcher** to own all of that — exactly as it already does for
**FMHA** and
**Grouped Conv** — so there is one kernel-generation/build/runtime path
and the
TE shrinks to a config+benchmark frontend. This PR brings regular GEMM
in line
with that reference binding.

## The binding (mirrors the FMHA/Conv reference, six stages)

1. **Config JSON (TE side)** — the sweep search space lives in
   `tile_engine/ops/gemm/configs/` (flat op-root layout, matching the
   `fmha/` and `grouped_conv/` bridges).
2. **Codegen (Dispatcher)** —
`dispatcher/codegen/unified_gemm_codegen.py` emits
   one fully-typed `.hpp` per kernel; `GemmKernelConfig.name` reproduces
`KERNEL_NAME` **byte-for-byte** (the thread tying config → kernel →
runtime).
3. **Compile to `.so`** — a single static `gemm_ctypes_lib.cpp` is
force-included
   (`-include <kernel.hpp>`); one `.so` per kernel.
4. **Flat `extern "C"` ABI** — `dispatcher_run_gemm(A, B, C, M, N, K,
time_ms)` +
the kernel-name enumeration entry points. **Host-pointer** memory model
(the C
lib `hipMalloc`s internally) — the FMHA-forward branch of the reference.
5. **Python ctypes wrapper** — `dispatcher/python/gemm_utils.py`
   (`GemmDispatcherLib` + `GpuGemmRunner`).
6. **TE driver (3 phases)** — `gemm_full_benchmark.py` (parallel
codegen+build →
`expand_sweep` → subprocess-isolated benchmark) + the disposable
per-kernel
   worker `run_one_gemm_kernel.py`.

## What's included

**Bridge core**
- `dispatcher/codegen/unified_gemm_codegen.py` — GEMM codegen,
byte-exact naming.
- `dispatcher/bindings/ctypes/gemm_ctypes_lib.cpp` — flat C ABI,
host-pointer model.
- `dispatcher/python/gemm_utils.py` — `GemmKernelConfig`, multi-kernel
build
(`setup_multiple_gemm_dispatchers`), `expand_sweep`,
one-`.so`-per-kernel.
- `tile_engine/ops/gemm/gemm_full_benchmark.py` +
`run_one_gemm_kernel.py` —
  3-phase, multi-GPU, subprocess-isolated driver/worker.

**Feature surface (the point of this PR)**
- **All four layouts** `rcr`/`rrr`/`crr`/`ccr` (row-major C only —
ck_tile rejects
  column-major C at build) with layout-aware host transpose.
- **`fp16` + `bf16`** (bf16 via uint16 byte-encoding; dtype derived from
kernel name).
- **Trait-derived registry `KernelKey`** — replaces the earlier
hard-coded
fp16/rcr key so the registry path generalizes across dtype/layout/tile.

**Correctness & performance hygiene**
- **`--verify`** opt-in fp32 numpy-reference gate (global
`max|out-ref|/max|ref|`),
`verified`/`max_rel` columns in the CSV; a mismatch counts as a failure.
- **Tile Engine AMDGPU `-mllvm` codegen-flag parity** (without these the
kernel
  builds with different occupancy and the timing diverges) and
  **arch-validated tile filtering** against the real pipeline/scheduler.
- **Multi-GPU** fan-out across all visible GPUs (`--devices`,
device-pinned
  `HIP_VISIBLE_DEVICES` workers).

**Example & tests**
- `dispatcher/examples/gemm/python/12_te_bridge.py` — runnable
end-to-end example.
- `dispatcher/tests/test_gemm_parity.py`, `test_gemm_utils.py`, and a
parity
  regression harness.

**Cleanup**
- Removes the legacy standalone `gemm_universal` build path
  (`gemm_universal_instance_builder.py`, `*_benchmark*.{py,cpp,hpp}`,
`gemm_universal/CMakeLists.txt`) and the old
`test/ck_tile/gemm_tile_engine/`
  harness; promotes the sweep configs to the flat op-root `configs/`.

## Design decisions (consistent with the reference)

- **Host-pointer memory ownership** (C lib owns device memory) — matches
FMHA-forward; the Python runner passes host numpy arrays straight
through.
- **One `.so` per kernel** — packaging choice; the multi-kernel name ABI
is
retained (`get_kernel_name_at(0)` reports the single kernel), so the
Python
  enumeration path is unchanged from FMHA/Conv.
- **Flat `configs/`** at the op root — matches the
`fmha/`/`grouped_conv/`
convention; the not-yet-bridged variants keep their per-variant
`configs/`
  dirs, selected by `--variant`.

## Validation (gfx942 / MI300X)

- Bridge build + benchmark + `--verify` across **`fp16` and `bf16`** and
**all
  four layouts**, checked against an fp32 numpy reference (`A @ B`).
- **Name parity** holds end-to-end: each `.so`'s reported runtime name
equals
  `GemmKernelConfig(...).name`.
- bf16 passes under a widened fp16/bf16 tolerance; fp16 within the
standard
  `max_rel` gate.

## Test plan

- [ ] `gemm_full_benchmark.py --verify` over
`configs/default_ci_config.json` for
      `fp16` and `bf16`, each of `rcr`/`rrr`/`crr`/`ccr`.
- [ ] `unified_gemm_codegen.py` emits a header whose stem ==
`GemmKernelConfig.name`.
- [ ] `setup_multiple_gemm_dispatchers` builds + links each config
against
      `gemm_ctypes_lib.cpp`.
- [ ] `pytest dispatcher/tests/test_gemm_parity.py
dispatcher/tests/test_gemm_utils.py`.
- [ ] `examples/gemm/python/12_te_bridge.py` runs end to end.

## Notes

- Single clean commit off the latest `develop`; the diff is **35 files,
all under
`projects/composablekernel/`** (dispatcher + tile_engine/ops/gemm +
test/ck_tile).
- **Supersedes #8123 and #8261**, which will be closed.
- Stream-K (#8136) and grouped GEMM are separate bridge efforts, not in
this PR.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Muhammed Ozturk <muozturk@ctr2-alola-ctrl-01.amd.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