Files
composable_kernel/dispatcher/examples/gemm/cpp/02_grouped_gemm_driver.cpp
Muhammed Emin Ozturk 6648115aed [rocm-libraries] ROCm/rocm-libraries#9000 (commit 9faa8de)
feat(ck-tile): add grouped GEMM variant to TE to dispatcher
 bridge (#9000)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

> Re-opened from #8130 with a policy-compliant branch name
(`users/muozturk/ck-tile/dispatcher-te-bridge-grouped-gemm`). Supersedes
#8130.

## What this PR does

Routes the **grouped_gemm** variant through the Tile Engine (TE) →
Dispatcher **bridge**: TE only generates configs and benchmarks; the
Dispatcher owns codegen, build, and runtime. This is the grouped
counterpart of the regular-GEMM bridge (#8123/#8479), the fp8/bf8/int8
bridge (#8887), and the Stream-K bridge (#8136).

**This PR now also contains the grouped Dispatcher codegen** that
previously lived in #8075 — that PR has been **closed in favor of this
one** to keep the grouped codegen in a single place (it was otherwise
duplicated across both).

## Why grouped needs special handling

Grouped GEMM is **multi-problem**: one launch runs a *list* of `(M, N,
K)` sub-problems with arrays of A/B/C device pointers.

1. The single-problem run path (`g_dispatcher->run` / `GemmHostArgs`)
cannot express a list of problems.
2. The generated registry wrapper (`generated_tile_backend.hpp::run()`)
hard-codes the single-problem launch and won't compile against a grouped
`SelectedKernel`.

So the grouped path **bypasses the registry**: a dedicated ctypes lib
calls the generated `SelectedKernel::launch(descs, stream)` directly and
reports the name from the compile-time `KERNEL_NAME` macro.

## Changes

**Codegen (absorbed from #8075)**
- `codegen/arch_filter.py` — `GEMM_GROUPED` operator tile constraints.
- `codegen/unified_gemm_codegen.py` — `GemmVariant.GROUPED`, the grouped
launch generator (DeviceMem internal workspace via `MakeKargs`,
persistent/non-persistent grid), `grouped` in `--variants`.
- `examples/gemm/cpp/02_grouped_gemm_driver.cpp` — standalone,
layout/dtype-generic grouped driver with per-group reference
verification.
- `codegen/README.md` + `examples/gemm/cpp/README.md` — grouped
sections.

**Bridge**
- `bindings/ctypes/grouped_gemm_ctypes_lib.cpp` — multi-problem,
registry-bypass C ABI; per-group device alloc/copy; strides derived from
the compile-time `ALayout/BLayout/CLayout`; warmup/repeat timing matched
to Old-TE (`CK_TILE_BENCH_WARMUP/REPEAT`).
- `python/gemm_utils.py` — `GroupedGemmProblem`/`GroupedGemmResult`,
`GpuGroupedGemmRunner`, `run_grouped`, fp16/bf16/fp8(E4M3 FNUZ)/bf8(E5M2
FNUZ) codecs, output-dtype-aware C buffer.
- `tile_engine/ops/gemm/grouped_gemm_full_benchmark.py` +
`run_one_grouped_gemm_kernel.py` — TE driver + worker for the parity
sweep.
- `bindings/ctypes/GROUPED_GEMM_BRIDGE.md` — design README.

## Coverage (= Old-TE grouped runnable set on develop)

| Layout \ Dtype | fp16 | bf16 | fp8 (E4M3) | bf8 (E5M2) |
|---|---|---|---|---|
| rcr / rrr / ccr / crr | ✓ | ✓ | ✓ | ✓ |

C is always row-major. `int8` (rejected by the TE grouped builder) and
`fp32`/`fp64` (no MFMA warp tiles) are excluded on both sides.

## Parity vs Old-TE (MI300X / gfx942)

Apples-to-apples (same warmup=50/repeat=100 both sides, A/B interleaved,
single GPU, both engines rebuilt fresh, stale-`.so` guard, matched
compile flags):

- **Correctness: 64/64 PASS.**
- **Performance: 64/64 within ±15%.**
- The 5 small-shape (1024³ fp8/bf8) rows that initially read >15% were
proven by `rocprof` to be a **measurement-harness artifact** (Old-TE's
JSON `latency(ms)` rounded to 2 decimals → 30–50% TFLOPS swing on ~0.02
ms kernels), **not** a kernel/codegen difference — bridge and Old-TE
launch byte-identical kernels (same grid/VGPR/SGPR, duration ≤3.22%);
full-precision re-measure collapses all 5 to <3%.

## Notes

- Targets `develop`. Depends on #8997 (fp16/bf16 bridge) and #8998
(fp8/bf8/int8 bridge) merging to `develop` first; until then this PR's
diff also shows their content, after which it reduces to the
grouped-only files.
- Supersedes #8075 (closed).
2026-07-16 02:55:42 +00:00

193 lines
7.6 KiB
C++

// Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
/**
* Minimal standalone grouped-GEMM driver (dispatcher way).
*
* Grouped GEMM cannot ride the standard dispatcher.run(A,B,C,problem) path:
* that backend hardcodes a single GemmHostArgs. Instead, this driver includes a
* single generated grouped kernel header (CK_TILE_SINGLE_KERNEL_INCLUDE) and
* calls SelectedKernel::launch(descs, stream) directly with a vector of
* descriptors -- the same 2-arg signature the dispatcher generates (workspace is
* allocated INSIDE launch()). It builds per-group tensors, runs, and verifies
* each group against ck_tile::reference_gemm.
*
* Build (single-kernel include style):
* hipcc -std=c++17 --offload-arch=gfx942 \
* -DCK_TILE_SINGLE_KERNEL_INCLUDE \
* -I <ck>/include -I <generated_dir> \
* -include <generated_dir>/<one>_grouped.hpp \
* 02_grouped_gemm_driver.cpp -o grouped_gemm_driver
*/
#include <hip/hip_runtime.h>
#include <algorithm>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include "ck_tile/core.hpp"
#include "ck_tile/host.hpp"
#include "ck_tile/ops/gemm.hpp"
// The generated grouped kernel header is injected on the command line with
// -include and -DCK_TILE_SINGLE_KERNEL_INCLUDE. It exports into the global
// namespace: SelectedKernel, ADataType, BDataType, CDataType, AccDataType,
// ALayout, BLayout, CLayout, and KERNEL_NAME.
template <typename Layout>
static constexpr inline auto is_row_major(Layout)
{
return ck_tile::bool_constant<
std::is_same_v<ck_tile::remove_cvref_t<Layout>, ck_tile::tensor_layout::gemm::RowMajor>>{};
}
static std::vector<int> parse_csv_ints(const std::string& s)
{
std::vector<int> out;
std::string cur;
for(char c : s)
{
if(c == ',')
{
if(!cur.empty())
{
out.push_back(std::stoi(cur));
cur.clear();
}
}
else
cur.push_back(c);
}
if(!cur.empty())
out.push_back(std::stoi(cur));
return out;
}
static std::string get_opt(int argc, char** argv, const std::string& key, const std::string& def)
{
for(int i = 1; i < argc - 1; ++i)
if(key == argv[i])
return argv[i + 1];
return def;
}
int main(int argc, char** argv)
{
const int group_count = std::stoi(get_opt(argc, argv, "--groups", "8"));
const int kbatch = std::stoi(get_opt(argc, argv, "--kbatch", "1"));
const int warmup = std::stoi(get_opt(argc, argv, "--warmup", "10"));
const int repeat = std::stoi(get_opt(argc, argv, "--repeat", "50"));
const bool validate = get_opt(argc, argv, "--validate", "1") != "0";
std::vector<int> Ms = parse_csv_ints(get_opt(argc, argv, "--Ms", ""));
std::vector<int> Ns = parse_csv_ints(get_opt(argc, argv, "--Ns", ""));
std::vector<int> Ks = parse_csv_ints(get_opt(argc, argv, "--Ks", ""));
const int dm = std::stoi(get_opt(argc, argv, "--m", "256"));
const int dn = std::stoi(get_opt(argc, argv, "--n", "256"));
const int dk = std::stoi(get_opt(argc, argv, "--k", "512"));
auto sz = static_cast<std::size_t>(group_count);
if(Ms.size() != sz || Ns.size() != sz || Ks.size() != sz)
{
Ms.assign(group_count, dm);
Ns.assign(group_count, dn);
Ks.assign(group_count, dk);
}
std::cout << "Kernel: " << KERNEL_NAME << "\n";
std::cout << "groups=" << group_count << " kbatch=" << kbatch << "\n";
std::vector<ck_tile::HostTensor<ADataType>> a_host, b_host;
std::vector<ck_tile::HostTensor<CDataType>> c_host;
std::vector<std::unique_ptr<ck_tile::DeviceMem>> a_dev, b_dev, c_dev;
std::vector<ck_tile::index_t> sA(group_count), sB(group_count), sC(group_count);
std::vector<ck_tile::GroupedGemmHostArgs<>> descs;
descs.reserve(group_count);
for(int i = 0; i < group_count; ++i)
{
const ck_tile::index_t M = Ms[i], N = Ns[i], K = Ks[i];
sA[i] = ck_tile::get_default_stride(M, K, 0, is_row_major(ALayout{}));
sB[i] = ck_tile::get_default_stride(K, N, 0, is_row_major(BLayout{}));
sC[i] = ck_tile::get_default_stride(M, N, 0, is_row_major(CLayout{}));
a_host.push_back(ck_tile::HostTensor<ADataType>(
ck_tile::host_tensor_descriptor(M, K, sA[i], is_row_major(ALayout{}))));
b_host.push_back(ck_tile::HostTensor<BDataType>(
ck_tile::host_tensor_descriptor(K, N, sB[i], is_row_major(BLayout{}))));
c_host.push_back(ck_tile::HostTensor<CDataType>(
ck_tile::host_tensor_descriptor(M, N, sC[i], is_row_major(CLayout{}))));
ck_tile::FillUniformDistribution<ADataType>{-1.f, 1.f}(a_host[i]);
ck_tile::FillUniformDistribution<BDataType>{-1.f, 1.f}(b_host[i]);
c_host[i].SetZero();
a_dev.push_back(std::make_unique<ck_tile::DeviceMem>(a_host[i]));
b_dev.push_back(std::make_unique<ck_tile::DeviceMem>(b_host[i]));
c_dev.push_back(std::make_unique<ck_tile::DeviceMem>(c_host[i]));
c_dev[i]->SetZero();
descs.push_back(ck_tile::GroupedGemmHostArgs<>{a_dev[i]->GetDeviceBuffer(),
b_dev[i]->GetDeviceBuffer(),
{},
c_dev[i]->GetDeviceBuffer(),
kbatch,
M,
N,
K,
sA[i],
sB[i],
{},
sC[i]});
}
const ck_tile::stream_config s{nullptr, true, /*log=*/0, warmup, repeat};
float ave_time = SelectedKernel::launch(descs, s);
std::size_t flop = 0, bytes = 0;
for(int i = 0; i < group_count; ++i)
{
flop += std::size_t(2) * Ms[i] * Ns[i] * Ks[i];
bytes += sizeof(ADataType) * Ms[i] * Ks[i] + sizeof(BDataType) * Ks[i] * Ns[i] +
sizeof(CDataType) * Ms[i] * Ns[i];
}
const float tflops = static_cast<float>(flop) / 1.E9 / ave_time;
const float gbps = static_cast<float>(bytes) / 1.E6 / ave_time;
std::cout << "Perf: " << std::setw(10) << ave_time << " ms, " << tflops << " TFlops, " << gbps
<< " GB/s\n";
for(int i = 0; i < group_count; ++i)
c_dev[i]->FromDevice(c_host[i].data());
bool pass = true;
if(validate)
{
for(int i = 0; i < group_count; ++i)
{
ck_tile::HostTensor<CDataType> ref(
ck_tile::host_tensor_descriptor(Ms[i], Ns[i], sC[i], is_row_major(CLayout{})));
ref.SetZero();
ck_tile::reference_gemm<ADataType, BDataType, AccDataType, CDataType>(
a_host[i], b_host[i], ref);
const float maxv = *std::max_element(ref.mData.begin(), ref.mData.end());
const auto rtol = ck_tile::get_relative_threshold<ADataType, CDataType, AccDataType>(
ck_tile::integer_divide_ceil(Ks[i], kbatch));
const auto atol = ck_tile::get_absolute_threshold<ADataType, CDataType, AccDataType>(
maxv / kbatch, ck_tile::integer_divide_ceil(Ks[i], kbatch));
bool ok =
ck_tile::check_err(c_host[i], ref, "group[" + std::to_string(i) + "]", rtol, atol);
pass &= ok;
}
std::cout << "Verification: " << (pass ? "PASS" : "FAIL") << "\n";
}
return pass ? 0 : 1;
}