Skip to content

Commit 5b6162e

Browse files
committed
[CK_TILE] Stream-K bridge: merge deep-core #8094 codegen into the bridge
Port #8136's Tile-Engine->Dispatcher Stream-K bridge onto the rewritten deep-core #8094 engine (KernelKey reduction fields, KernelInstance workspace virtuals, StreamK backend, Dispatcher-owned reduction workspace, registry + validation driver). 3-way merge over the shared stream_k ancestor; only the streamk launch emitter in unified_gemm_codegen.py and 03_streamk_gemm_driver.cpp conflicted -- both resolved to the deep-core side: - codegen now emits the struct-scope Sk* kernel type + GetWorkSpaceSize + IsSupported, keeps the 2-arg internal-workspace launch the bridge ctypes lib calls, and adds the 3-arg dispatcher-owned-workspace launch. - driver takes deep-core's stoll parse + apple-to-apple timing + validate cold shot. Bridge ctypes lib still bypasses the registry and calls the 2-arg launch directly, so the bridge runs the exact deep-core kernels. Codegen smoke: atomic/linear/tree + regular gemm all generate cleanly (0 failed).
2 parents 384ff41 + 9d033fd commit 5b6162e

11 files changed

Lines changed: 1054 additions & 72 deletions

File tree

projects/composablekernel/dispatcher/codegen/unified_gemm_codegen.py

Lines changed: 127 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -774,30 +774,43 @@ def _launch_function_streamk(self, config: KernelConfig) -> str:
774774
"tree": "Tree",
775775
}[config.reduction_strategy]
776776
return f"""
777-
static float launch(const ck_tile::StreamKHostArgs& args, const stream_config& stream) {{
778-
constexpr auto scheduler = {self.tm.SCHEDULER_TO_CK[config.trait.scheduler]};
779-
constexpr auto ReductionStrategy = ck_tile::StreamKReductionStrategy::{reduction_ck};
780-
781-
using GemmUniversalTraits = TileGemmUniversalTraits<kPadM, kPadN, kPadK, DoubleSmemBuffer,
782-
ALayout, BLayout, CLayout, TransposeC,
783-
UseStructuredSparsity, UsePersistentKernel,
784-
NumWaveGroups, Preshuffle>;
785-
786-
using UniversalGemmProblem = UniversalGemmPipelineProblem<
787-
ADataType, BDataType, AccDataType, TileShape, GemmUniversalTraits, scheduler>;
788-
789-
using GemmPipeline = {self.tm.PIPELINE_TO_CK[config.trait.pipeline]}<UniversalGemmProblem>;
790-
{self._epilogue_code(config)}
777+
// ---- Stream-K kernel type, hoisted to struct scope so the workspace API
778+
// ---- (GetWorkSpaceSize + external-workspace launch) can reuse the same type. ----
779+
static constexpr auto SkScheduler = {self.tm.SCHEDULER_TO_CK[config.trait.scheduler]};
780+
static constexpr auto SkReductionStrategy = ck_tile::StreamKReductionStrategy::{reduction_ck};
781+
static constexpr int SkBlockPerCu = {config.k_block_per_cu};
782+
783+
using SkGemmUniversalTraits = TileGemmUniversalTraits<kPadM, kPadN, kPadK, DoubleSmemBuffer,
784+
ALayout, BLayout, CLayout, TransposeC,
785+
UseStructuredSparsity, UsePersistentKernel,
786+
NumWaveGroups, Preshuffle>;
787+
using SkUniversalGemmProblem = UniversalGemmPipelineProblem<
788+
ADataType, BDataType, AccDataType, TileShape, SkGemmUniversalTraits, SkScheduler>;
789+
using SkGemmPipeline = {self.tm.PIPELINE_TO_CK[config.trait.pipeline]}<SkUniversalGemmProblem>;
790+
{self._epilogue_code(config)}
791+
using SkStreamKTilePartitioner =
792+
ck_tile::StreamKTilePartitioner<TileShape, SkReductionStrategy, UsePersistentKernel>;
793+
using StreamKGemmKernel =
794+
ck_tile::StreamKKernel<SkStreamKTilePartitioner, SkGemmPipeline, GemmEpilogue>;
795+
796+
// Device workspace (bytes) this kernel needs for `args`. 0 for Atomic;
797+
// >0 for Linear/Tree. The Dispatcher uses this to size the buffer it owns.
798+
static std::size_t GetWorkSpaceSize(const ck_tile::StreamKHostArgs& args) {{
799+
auto kargs = StreamKGemmKernel::MakeKernelArgs(args);
800+
return StreamKGemmKernel::GetWorkSpaceSize(kargs);
801+
}}
791802
792-
using StreamKTilePartitioner =
793-
ck_tile::StreamKTilePartitioner<TileShape, ReductionStrategy, UsePersistentKernel>;
794-
using StreamKGemmKernel =
795-
ck_tile::StreamKKernel<StreamKTilePartitioner, GemmPipeline, GemmEpilogue>;
803+
// Whether the kernel can actually partition this problem (enough tiles across
804+
// CUs). Lets the dispatcher's supports() reject too-small problems and fall
805+
// back to a non-Stream-K kernel instead of throwing at launch.
806+
static bool IsSupported(const ck_tile::StreamKHostArgs& args) {{
807+
return StreamKGemmKernel::IsSupportedArgument(StreamKGemmKernel::MakeKernelArgs(args));
808+
}}
796809
810+
// Internal-workspace launch: allocates a fresh DeviceMem on every call.
811+
// Kept unchanged for the bridge ctypes lib and the standalone 03 driver.
812+
static float launch(const ck_tile::StreamKHostArgs& args, const stream_config& stream) {{
797813
auto kargs = StreamKGemmKernel::MakeKernelArgs(args);
798-
799-
// Allocate the reduction workspace INTERNALLY (dispatcher idiom), not via
800-
// an external pointer the way Tile Engine does.
801814
const auto ws_size = StreamKGemmKernel::GetWorkSpaceSize(kargs);
802815
ck_tile::DeviceMem workspace_dev(ws_size);
803816
workspace_dev.SetZero();
@@ -809,42 +822,66 @@ def _launch_function_streamk(self, config: KernelConfig) -> str:
809822
810823
const dim3 grids = StreamKGemmKernel::GridSize(kargs.tile_partitioner);
811824
const dim3 blocks = StreamKGemmKernel::BlockSize();
812-
constexpr int kBlockPerCu = {config.k_block_per_cu};
813825
814-
// Atomic reduction accumulates into C, so reset it before every run.
815-
// Zero only the used MxN region honoring the leading dimension
816-
// (stride_E): a flat M*N memset would skip elements when C has a padded
817-
// stride and corrupt the atomic accumulation. hipMemset2DAsync handles
818-
// the pitched 2D extent and its status is checked, not discarded.
826+
// Atomic reduction accumulates into C, so reset buffers before each run.
819827
auto reset_data_buffers = [&]() {{
820-
if constexpr (ReductionStrategy == ck_tile::StreamKReductionStrategy::Atomic) {{
821-
constexpr size_t c_elem = sizeof(CDataType);
822-
const size_t c_pitch = static_cast<size_t>(args.stride_E) * c_elem;
823-
hipError_t memset_status;
824-
if constexpr (std::is_same_v<CLayout, ck_tile::tensor_layout::gemm::RowMajor>) {{
825-
// Row-major C: M rows of N elements, row pitch = stride_E.
826-
memset_status = hipMemset2DAsync(args.e_ptr, c_pitch, 0,
827-
static_cast<size_t>(args.N) * c_elem,
828-
static_cast<size_t>(args.M), stream.stream_id_);
829-
}} else {{
830-
// Column-major C: N columns of M elements, column pitch = stride_E.
831-
memset_status = hipMemset2DAsync(args.e_ptr, c_pitch, 0,
832-
static_cast<size_t>(args.M) * c_elem,
833-
static_cast<size_t>(args.N), stream.stream_id_);
834-
}}
835-
if(memset_status != hipSuccess) {{
836-
throw std::runtime_error(
837-
std::string("hipMemset2DAsync failed to zero C: ") +
838-
hipGetErrorString(memset_status));
839-
}}
828+
if constexpr (SkReductionStrategy == ck_tile::StreamKReductionStrategy::Atomic) {{
829+
// Stride-aware: CLayout is row-major with stride_E elems/row, so a
830+
// padded C is zeroed correctly (not just the contiguous M*N case).
831+
(void)hipMemset2DAsync(args.e_ptr,
832+
args.stride_E * sizeof(CDataType),
833+
0,
834+
args.N * sizeof(CDataType),
835+
args.M,
836+
stream.stream_id_);
840837
}} else {{
841838
workspace_dev.SetZero();
842839
}}
843840
}};
844841
std::function<void()> preprocess = reset_data_buffers;
845842
846843
float ave_time = launch_kernel_time_mask(stream, preprocess,
847-
make_kernel<kBlockPerCu>(StreamKGemmKernel{{}}, grids, blocks, 0, kargs));
844+
make_kernel<SkBlockPerCu>(StreamKGemmKernel{{}}, grids, blocks, 0, kargs));
845+
return ave_time;
846+
}}
847+
848+
// External-workspace launch (PR-D): the Dispatcher owns and reuses the
849+
// reduction buffer and passes it in. `workspace` may be null for Atomic
850+
// (size 0). The per-iteration reset stays here because it needs CDataType
851+
// and the reduction strategy, which the dtype-erased Dispatcher lacks.
852+
static float launch(const ck_tile::StreamKHostArgs& args, const stream_config& stream,
853+
void* workspace) {{
854+
auto kargs = StreamKGemmKernel::MakeKernelArgs(args);
855+
const auto ws_size = StreamKGemmKernel::GetWorkSpaceSize(kargs);
856+
if (workspace != nullptr) {{
857+
StreamKGemmKernel::SetWorkSpacePointer(kargs, workspace);
858+
}}
859+
860+
if (!StreamKGemmKernel::IsSupportedArgument(kargs)) {{
861+
throw std::runtime_error("Arguments not supported for stream-k kernel!");
862+
}}
863+
864+
const dim3 grids = StreamKGemmKernel::GridSize(kargs.tile_partitioner);
865+
const dim3 blocks = StreamKGemmKernel::BlockSize();
866+
867+
auto reset_data_buffers = [&]() {{
868+
if constexpr (SkReductionStrategy == ck_tile::StreamKReductionStrategy::Atomic) {{
869+
// Stride-aware: CLayout is row-major with stride_E elems/row, so a
870+
// padded C is zeroed correctly (not just the contiguous M*N case).
871+
(void)hipMemset2DAsync(args.e_ptr,
872+
args.stride_E * sizeof(CDataType),
873+
0,
874+
args.N * sizeof(CDataType),
875+
args.M,
876+
stream.stream_id_);
877+
}} else {{
878+
(void)hipMemsetAsync(workspace, 0, ws_size, stream.stream_id_);
879+
}}
880+
}};
881+
std::function<void()> preprocess = reset_data_buffers;
882+
883+
float ave_time = launch_kernel_time_mask(stream, preprocess,
884+
make_kernel<SkBlockPerCu>(StreamKGemmKernel{{}}, grids, blocks, 0, kargs));
848885
return ave_time;
849886
}}"""
850887

@@ -899,12 +936,49 @@ def generate(
899936
output_dtype = self.tm.get_output_dtype(self.datatype)
900937
rel_path = kernel_path.relative_to(output_dir)
901938

939+
# Stream-K kernels need the Stream-K backend (StreamKHostArgs launch) and
940+
# the SK key fields, so the registry can tell atomic/linear/tree apart and
941+
# the right launch path compiles. All other variants use the regular backend.
942+
is_streamk = config.variant == GemmVariant.STREAM_K
943+
backend_inc = (
944+
"generated_tile_backend_streamk.hpp"
945+
if is_streamk
946+
else "generated_kernel_backend.hpp"
947+
)
948+
949+
sk_fields = ""
950+
if is_streamk:
951+
rs = {"atomic": "Atomic", "linear": "Linear", "tree": "Tree"}[
952+
config.reduction_strategy
953+
]
954+
ws = str(config.reduction_strategy != "atomic").lower()
955+
sk_fields = f"""
956+
key.algorithm.pad_m = {str(config.trait.pad_m).lower()};
957+
key.algorithm.pad_n = {str(config.trait.pad_n).lower()};
958+
key.algorithm.pad_k = {str(config.trait.pad_k).lower()};
959+
key.algorithm.streamk = true;
960+
key.algorithm.reduction_strategy = ::ck_tile::dispatcher::ReductionStrategy::{rs};
961+
key.algorithm.workspace = {ws};"""
962+
963+
if is_streamk:
964+
ret_stmt = (
965+
"return backends::create_generated_streamk_kernel<KernelStruct, "
966+
"KernelStruct::ADataType, KernelStruct::BDataType, "
967+
"KernelStruct::CDataType, KernelStruct::AccDataType>"
968+
f'(key, "{kernel_name}");'
969+
)
970+
else:
971+
ret_stmt = (
972+
"return std::make_shared<backends::GeneratedKernelInstance<KernelStruct>>"
973+
f'(key, "{kernel_name}");'
974+
)
975+
902976
return f"""// SPDX-License-Identifier: MIT
903977
// Auto-generated dispatcher wrapper
904978
#pragma once
905979
906980
#include "ck_tile/dispatcher.hpp"
907-
#include "ck_tile/dispatcher/backends/generated_kernel_backend.hpp"
981+
#include "ck_tile/dispatcher/backends/{backend_inc}"
908982
#include "{rel_path}"
909983
910984
namespace ck_tile {{
@@ -955,11 +1029,11 @@ def generate(
9551029
key.algorithm.persistent = {str(config.trait.persistent).lower()};
9561030
key.algorithm.preshuffle = {str(config.preshuffle).lower()};
9571031
key.algorithm.transpose_c = false;
958-
key.algorithm.num_wave_groups = {config.num_wave_groups};
959-
1032+
key.algorithm.num_wave_groups = {config.num_wave_groups};{sk_fields}
1033+
9601034
key.gfx_arch = gfx_arch;
961-
962-
return std::make_shared<backends::GeneratedKernelInstance<KernelStruct>>(key, "{kernel_name}");
1035+
1036+
{ret_stmt}
9631037
}}
9641038
9651039
}}}}}}

projects/composablekernel/dispatcher/examples/gemm/cpp/03_streamk_gemm_driver.cpp

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,36 @@ static std::string get_opt(int argc, char** argv, const std::string& key, const
5555

5656
int main(int argc, char** argv)
5757
{
58-
// Parse with std::stoll (not std::stoi) before narrowing to index_t: stoi
59-
// throws std::out_of_range past INT_MAX, which would reject large GEMM sizes.
60-
const ck_tile::index_t M =
61-
static_cast<ck_tile::index_t>(std::stoll(get_opt(argc, argv, "--m", "3840")));
62-
const ck_tile::index_t N =
63-
static_cast<ck_tile::index_t>(std::stoll(get_opt(argc, argv, "--n", "4096")));
64-
const ck_tile::index_t K =
65-
static_cast<ck_tile::index_t>(std::stoll(get_opt(argc, argv, "--k", "2048")));
66-
const int warmup = std::stoi(get_opt(argc, argv, "--warmup", "10"));
67-
const int repeat = std::stoi(get_opt(argc, argv, "--repeat", "50"));
58+
const ck_tile::index_t M = std::stoll(get_opt(argc, argv, "--m", "3840"));
59+
const ck_tile::index_t N = std::stoll(get_opt(argc, argv, "--n", "4096"));
60+
const ck_tile::index_t K = std::stoll(get_opt(argc, argv, "--k", "2048"));
61+
int warmup = std::stoi(get_opt(argc, argv, "--warmup", "50"));
62+
int repeat = std::stoi(get_opt(argc, argv, "--repeat", "100"));
6863
const bool validate = get_opt(argc, argv, "--validate", "1") != "0";
6964

65+
// Apple-to-apple with tile_engine: time the kernel with the SAME methodology the
66+
// tile_engine benchmark uses (gemm_streamk_profiler.hpp) -- gpu timer and a
67+
// cold-cache measurement that flushes the cache and rotates input buffers each
68+
// iteration. tile_engine defaults: timer=true, flush_cache=true, rotating_count=1000.
69+
// Without these the driver measured a warm-cache best case and over-reported TFlops,
70+
// which is the entire source of the dispatcher-vs-TE "performance gap".
71+
const bool gpu_timer = get_opt(argc, argv, "--timer", "1") != "0";
72+
bool flush_cache = get_opt(argc, argv, "--flush_cache", "1") != "0";
73+
int rotating_count = std::stoi(get_opt(argc, argv, "--rotating_count", "1000"));
74+
75+
// Verification reads C back and compares against the reference for the known A/B.
76+
// Rotating buffers and multi-repeat rotate/accumulate the output, so the C left on
77+
// the device would not correspond to the reference inputs. tile_engine handles this
78+
// with repeat_once_if_verify(); we mirror it -- a validating run times a single cold
79+
// shot. Run a separate --validate 0 pass to collect apple-to-apple perf numbers.
80+
if(validate)
81+
{
82+
warmup = 0;
83+
repeat = 1;
84+
flush_cache = false;
85+
rotating_count = 1;
86+
}
87+
7088
std::cout << "Kernel: " << KERNEL_NAME << "\n";
7189
std::cout << "M=" << M << " N=" << N << " K=" << K << "\n";
7290

@@ -100,7 +118,8 @@ int main(int argc, char** argv)
100118
sB,
101119
sC};
102120

103-
const ck_tile::stream_config s{nullptr, true, /*log=*/0, warmup, repeat};
121+
const ck_tile::stream_config s{
122+
nullptr, true, /*log=*/0, warmup, repeat, gpu_timer, flush_cache, rotating_count};
104123
float ave_time = SelectedKernel::launch(args, s);
105124

106125
const std::size_t flop = std::size_t(2) * M * N * K;
@@ -121,9 +140,34 @@ int main(int argc, char** argv)
121140
ref.SetZero();
122141
ck_tile::reference_gemm<ADataType, BDataType, AccDataType, CDataType>(a_host, b_host, ref);
123142
const float maxv = *std::max_element(ref.mData.begin(), ref.mData.end());
124-
const auto rtol = ck_tile::get_relative_threshold<ADataType, CDataType, AccDataType>(K);
125-
const auto atol =
126-
ck_tile::get_absolute_threshold<ADataType, CDataType, AccDataType>(maxv, K);
143+
144+
// Stream-K splits K across CUs and reduces partials. Atomic reduction
145+
// accumulates those partials directly into low-precision C, so the
146+
// verification tolerance must account for the split-K accumulation
147+
// error -- exactly as the tile_engine verifier does in
148+
// tile_engine/include/utility/validation.hpp::calculate_rtol_atol.
149+
// kbatch is the number of workgroups reducing into a single output
150+
// tile, taken from the kernel's own tile partitioner so the driver and
151+
// tile_engine agree on the split factor.
152+
auto kargs = SelectedKernel::StreamKGemmKernel::MakeKernelArgs(args);
153+
const ck_tile::index_t kbatch =
154+
std::max<ck_tile::index_t>(1, kargs.tile_partitioner.estimate_num_wgs_per_tile());
155+
156+
using ComputeType =
157+
std::conditional_t<sizeof(ADataType) < sizeof(BDataType), ADataType, BDataType>;
158+
const ck_tile::index_t k_per_split = ck_tile::integer_divide_ceil(K, kbatch);
159+
// single-pass (per-split) tolerance
160+
const auto rtol_base =
161+
ck_tile::get_relative_threshold<ComputeType, CDataType, AccDataType>(k_per_split);
162+
const auto atol_base = ck_tile::get_absolute_threshold<ComputeType, CDataType, AccDataType>(
163+
maxv / kbatch, k_per_split);
164+
// error contributed by reducing kbatch partials in low-precision C
165+
const auto rtol_split_k =
166+
ck_tile::get_relative_threshold<CDataType, CDataType, CDataType>(kbatch);
167+
const auto atol_split_k =
168+
ck_tile::get_absolute_threshold<CDataType, CDataType, CDataType>(maxv, kbatch);
169+
const auto rtol = std::max(rtol_base, rtol_split_k);
170+
const auto atol = std::max(atol_base, atol_split_k);
127171
pass = ck_tile::check_err(c_host, ref, "streamk", rtol, atol);
128172
std::cout << "Verification: " << (pass ? "PASS" : "FAIL") << "\n";
129173
}

0 commit comments

Comments
 (0)