Skip to content

Commit 8225e42

Browse files
ozturkosuclaude
andcommitted
[CK_TILE] Stream-K bridge: port #8123 driver/bench improvements
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>
1 parent ecd7bce commit 8225e42

4 files changed

Lines changed: 418 additions & 134 deletions

File tree

projects/composablekernel/dispatcher/bindings/ctypes/streamk_gemm_ctypes_lib.cpp

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
#include <hip/hip_runtime.h>
3434
#include <cstdint>
35+
#include <cstdlib>
3536
#include <cstring>
3637
#include <exception>
3738
#include <string>
@@ -47,6 +48,33 @@
4748

4849
static bool g_initialized = false;
4950

51+
// Read an integer benchmark knob from the environment, falling back to
52+
// `fallback` when unset or unparseable.
53+
static int env_int(const char* name, int fallback)
54+
{
55+
const char* v = std::getenv(name);
56+
if(v == nullptr || *v == '\0')
57+
return fallback;
58+
char* end = nullptr;
59+
const long out = std::strtol(v, &end, 10);
60+
if(end == v)
61+
return fallback;
62+
return static_cast<int>(out);
63+
}
64+
65+
// Read a boolean benchmark knob ("0"/"false"/"off", any case => false, else true).
66+
static bool env_bool(const char* name, bool fallback)
67+
{
68+
const char* v = std::getenv(name);
69+
if(v == nullptr || *v == '\0')
70+
return fallback;
71+
std::string s(v);
72+
for(char& c : s)
73+
if(c >= 'A' && c <= 'Z')
74+
c = static_cast<char>(c - 'A' + 'a');
75+
return !(s == "0" || s == "false" || s == "off");
76+
}
77+
5078
extern "C" {
5179

5280
/**
@@ -150,15 +178,29 @@ int dispatcher_run_gemm(
150178
/*stride_B=*/static_cast<ck_tile::index_t>(K),
151179
/*stride_C=*/static_cast<ck_tile::index_t>(N));
152180

181+
// Benchmark parameters. warmup/repeat default to old Tile Engine's values
182+
// (warmup=50, repeat=100); a generous warmup keeps the GPU clock ramped, and
183+
// 100 timed iterations give a stable median. These were the knobs behind the
184+
// regular bridge's spurious "perf gap" (#8123): the old default of warmup=3/
185+
// repeat=10 measured a cold, un-ramped clock. Each knob is env-overridable so
186+
// a caller can match another harness without recompiling.
187+
//
188+
// Divergence from the regular path (generated_tile_backend.hpp): flush_cache_
189+
// and rotating_count_ default OFF here. The Stream-K Atomic reduction
190+
// accumulates into C, and the generated launch's launch_kernel_time_mask
191+
// preprocess re-zeros only the original args.e_ptr -- rotating C across
192+
// multiple buffers would leave the rotated copies un-zeroed and corrupt the
193+
// accumulation. Leave rotating_count_=1 unless a caller knows the kernel
194+
// re-zeros every rotated buffer.
153195
ck_tile::stream_config stream_cfg;
154196
stream_cfg.stream_id_ = nullptr;
155197
stream_cfg.time_kernel_ = true;
156198
stream_cfg.log_level_ = 0;
157-
stream_cfg.cold_niters_ = 3;
158-
stream_cfg.nrepeat_ = 10;
199+
stream_cfg.cold_niters_ = env_int("CK_TILE_BENCH_WARMUP", 50);
200+
stream_cfg.nrepeat_ = env_int("CK_TILE_BENCH_REPEAT", 100);
159201
stream_cfg.is_gpu_timer_ = true;
160-
stream_cfg.flush_cache_ = false;
161-
stream_cfg.rotating_count_ = 1;
202+
stream_cfg.flush_cache_ = env_bool("CK_TILE_BENCH_FLUSH", false);
203+
stream_cfg.rotating_count_ = env_int("CK_TILE_BENCH_ROTATING", 1);
162204

163205
float exec_time = 0.0f;
164206
try

projects/composablekernel/tile_engine/ops/gemm/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,40 @@ What that means for this PR:
138138
- Grouped GEMM and stream-K go through **separate bridge efforts** (stream-K in
139139
#8136, grouped GEMM on its own branch), not this PR.
140140

141+
### Stream-K bridge
142+
143+
Stream-K reuses the same bridge mechanics through its **own** driver/worker pair
144+
(it keeps a distinct ctypes lib that bypasses the registry; see
145+
`dispatcher/bindings/ctypes/streamk_gemm_ctypes_lib.cpp`):
146+
147+
| Script | Role |
148+
|---|---|
149+
| `streamk_gemm_full_benchmark.py` | Stream-K driver — same 3-phase, multi-GPU, `--verify`/`--devices` flags as the regular driver; threads `variant="stream_k"`. Defaults to `gemm_streamk/configs/default_config.json`. |
150+
| `run_one_streamk_gemm_kernel.py` | Disposable per-kernel worker (same fp16 host path as the regular worker; the ABI matches). |
151+
152+
```bash
153+
# Default Stream-K config, all visible GPUs, with correctness checking:
154+
python streamk_gemm_full_benchmark.py --verify
155+
156+
# Explicit config, 4 GPUs, custom output:
157+
python streamk_gemm_full_benchmark.py gemm_streamk/configs/default_config.json \
158+
--devices 4 --csv streamk_gemm_results.csv
159+
```
160+
161+
Notes specific to Stream-K:
162+
- Kernel names carry a `_streamk` suffix (`GemmKernelConfig(variant="stream_k").name`).
163+
- Supported surface here is **fp16 / rcr**, same as the regular bridge.
164+
- The Atomic reduction does multiple fp16 atomic-adds per K-split, so it is noisier
165+
than regular GEMM; `--verify` still uses the default `2e-2` gate (observed
166+
`max_rel ≤ 2.5e-3`, well within it).
167+
- Benchmark knobs in `streamk_gemm_ctypes_lib.cpp` default to warmup=50/repeat=100
168+
(env-overridable via `CK_TILE_BENCH_WARMUP`/`REPEAT`/`FLUSH`/`ROTATING`), matching
169+
the regular path — **except** `rotating_count` defaults to 1, because the Atomic
170+
preprocess only re-zeros the original C buffer and rotating C would corrupt the
171+
accumulation.
172+
- Tiny problems (e.g. `257³`) have too few tiles to partition across CUs; the kernel
173+
reports them unsupported (status `-2`) and the bridge surfaces that gracefully.
174+
141175
### Removal note
142176

143177
The legacy regular-GEMM standalone build path has been **removed**, and the

projects/composablekernel/tile_engine/ops/gemm/run_one_streamk_gemm_kernel.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,16 @@
1515
Single: {"so_path": "...", "problem": {"M":.., "N":.., "K":..}, "kernel_name": "..."}
1616
Batch: {"items": [{"so_path": "...", "problem": {...}, "kernel_name": "..."}, ...]}
1717
18+
Optional top-level keys ``verify`` (bool) and ``verify_tol`` (float) enable an
19+
fp32 numpy reference check; when set, each OK result also carries ``verified``
20+
and ``max_rel``. Stream-K's Atomic reduction does multiple fp16 atomic-adds (one
21+
per K-split partial), so it is inherently noisier than a single fp32->fp16 store;
22+
the default gate tolerance (2e-2) is loose enough to pass while still catching
23+
gross errors.
24+
1825
Output JSON format (one line per kernel):
1926
{"idx": 0, "ok": true, "ms": 0.123, "tflops": 456.7, "non_zero": 1, "kernel": "..."}
27+
{"idx": 0, "ok": true, ..., "verified": true, "max_rel": 1.1e-3} # with --verify
2028
{"idx": 1, "ok": false, "error": "...", "kernel": "..."}
2129
"""
2230

@@ -35,8 +43,14 @@
3543
import numpy as np # noqa: E402
3644

3745

38-
def _run_one(idx, so_path, prob_dict, kernel_name):
39-
"""Run a single kernel and emit its result as one JSON line."""
46+
def _run_one(idx, so_path, prob_dict, kernel_name, verify=False, verify_tol=2e-2):
47+
"""Run a single kernel and emit its result as one JSON line.
48+
49+
When ``verify`` is set, the kernel output is checked against an fp32 numpy
50+
reference (``A @ B``) using the global relative metric
51+
``max|out - ref| / max|ref|``; the emitted ``verified`` field then reflects
52+
correctness, not just liveness (``non_zero``).
53+
"""
4054
try:
4155
problem = GemmProblem.from_dict(prob_dict)
4256

@@ -54,19 +68,22 @@ def _run_one(idx, so_path, prob_dict, kernel_name):
5468
if result.output is not None
5569
else 0
5670
)
57-
print(
58-
json.dumps(
59-
{
60-
"idx": idx,
61-
"ok": True,
62-
"ms": result.time_ms,
63-
"tflops": result.tflops,
64-
"non_zero": non_zero,
65-
"kernel": kernel_name,
66-
}
67-
),
68-
flush=True,
69-
)
71+
out = {
72+
"idx": idx,
73+
"ok": True,
74+
"ms": result.time_ms,
75+
"tflops": result.tflops,
76+
"non_zero": non_zero,
77+
"kernel": kernel_name,
78+
}
79+
if verify:
80+
ref = A.astype(np.float32) @ B.astype(np.float32)
81+
got = result.output.astype(np.float32)
82+
denom = float(np.max(np.abs(ref))) or 1.0
83+
max_rel = float(np.max(np.abs(got - ref)) / denom)
84+
out["max_rel"] = max_rel
85+
out["verified"] = bool(max_rel <= verify_tol)
86+
print(json.dumps(out), flush=True)
7087
else:
7188
print(
7289
json.dumps(
@@ -100,13 +117,28 @@ def main():
100117
)
101118
sys.exit(1)
102119

120+
verify = bool(d.get("verify", False))
121+
verify_tol = float(d.get("verify_tol", 2e-2))
122+
103123
if "items" in d:
104124
for i, item in enumerate(d["items"]):
105125
_run_one(
106-
i, item["so_path"], item["problem"], item.get("kernel_name", "unknown")
126+
i,
127+
item["so_path"],
128+
item["problem"],
129+
item.get("kernel_name", "unknown"),
130+
verify=verify,
131+
verify_tol=verify_tol,
107132
)
108133
else:
109-
_run_one(0, d["so_path"], d["problem"], d.get("kernel_name", "unknown"))
134+
_run_one(
135+
0,
136+
d["so_path"],
137+
d["problem"],
138+
d.get("kernel_name", "unknown"),
139+
verify=verify,
140+
verify_tol=verify_tol,
141+
)
110142

111143

112144
if __name__ == "__main__":

0 commit comments

Comments
 (0)