Repository navigation
feat(ck-tile): microscaling (mx) GEMM TE to dispatcher bridge #9329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
630d76f
feat(ck-tile): mx_gemm GEMM TE to dispatcher bridge
ozturkosu 3119eee
test(ck-tile): add CPU-only unit tests for mx_gemm bridge
ozturkosu 4a5b463
harden(ck-tile): mx_gemm bridge divisibility guard, fp8 OCP precondit…
ozturkosu ec36f78
fix(ck-tile): address Copilot round-3 findings on mx_gemm bridge
ozturkosu 0488a9d
polish(ck-tile): mx_gemm bridge LOW nits from round-1 review
ozturkosu 5ecc48e
fix(ck-tile): match Old-TE compile flags in mx_gemm bridge _compile_k…
ozturkosu 09327f2
fix(ck-tile): probe-gate mx_gemm coerce-illegal-types flag for toolch…
ozturkosu c582f48
fix(ck-tile): mx_gemm bridge — require GPU arch via get_arch, do not …
ozturkosu f1944ad
fix(ck-tile): mx_gemm bridge — assert gfx950-only at compile time for…
ozturkosu d59b626
fix(ck-tile): mx_gemm bridge — add runtime gfx950 device guard for pr…
ozturkosu 53bfd2a
fix(ck-tile): mx_gemm bridge — address review (unsupported -2 code, a…
ozturkosu 0c40bda
perf(ck-tile): mx_gemm bridge — sweep buffer reuse, vectorized fp8 de…
ozturkosu 3cb9b92
fix(ck-tile): address Copilot review on mx_gemm bridge (arch/CMake/te…
ozturkosu 1a6765e
fix(ck-tile): mx_gemm CMake resolves GFX_ARCH generically (no hardcod…
ozturkosu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
304 changes: 304 additions & 0 deletions
304
projects/composablekernel/dispatcher/bindings/ctypes/mx_gemm_ctypes_lib.cpp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,304 @@ | ||
| // Copyright (c) Advanced Micro Devices, Inc., or its affiliates. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| /** | ||
| * MX-GEMM Dispatcher ctypes Library (TileEngine -> Dispatcher bridge). | ||
| * | ||
| * Provides a C API for Python ctypes integration. The kernel header is | ||
| * force-included at compile time: | ||
| * hipcc -include <kernel.hpp> -DCK_TILE_SINGLE_KERNEL_INCLUDE mx_gemm_ctypes_lib.cpp | ||
| * | ||
| * Force-include defines (at global scope): SelectedKernel (with static constexpr | ||
| * TileM/TileN/TileK, WarpPerBlock_{M,N,K}, WarpTile{M,N,K} and | ||
| * static float launch(const MxGemmHostArgs&, const ck_tile::stream_config&)), | ||
| * KERNEL_NAME, ADataType/BDataType/CDataType/AccDataType/ScaleType(=e8m0_t), | ||
| * using MxGemmHostArgs = ck_tile::MxGemmHostArgs<1,1,0>, ALayout/BLayout/CLayout, | ||
| * and #include "ck_tile/ops/gemm.hpp". | ||
| * | ||
| * Registry bypass: microscaling GEMM's launch takes ck_tile::MxGemmHostArgs | ||
| * (with per-32-K e8m0 block scales that must be pre-shuffled for gfx950), which | ||
| * the generic dispatcher backend cannot express. So this lib builds the HostArgs | ||
| * from plain C arrays and calls SelectedKernel::launch() directly -- the same | ||
| * direct-launch pattern used by the batched/multi-D bridges. | ||
| * | ||
| * Memory model: host-pointer in. The lib owns device allocation/copy/free via | ||
| * ck_tile::HostTensor + ck_tile::DeviceMem (RAII), exactly like the Old-TE | ||
| * profiler. A/B/C byte sizes come from HostTensor::get_element_space_size_in_bytes(), | ||
| * which divides the logical element count by numeric_traits<T>::PackedSize -- so | ||
| * fp8 (PackedSize==1) allocates M*K bytes while fp4 (pk_fp4_t PackedSize==2, two | ||
| * e2m1 elements per byte) allocates M*K/2 bytes. The incoming A/B host buffers | ||
| * are therefore expected PHYSICALLY packed ([M,K/PackedSize]/[N,K/PackedSize] | ||
| * bytes); strides passed to the kernel stay in LOGICAL element units. | ||
| * Layout is fixed by the compiled-in ALayout/BLayout/CLayout (rcr: A row-major | ||
| * [M,K], B col-major [K,N] == [N,K] storage, C row-major [M,N]). K % 32 == 0. | ||
| * v1 scope: k_batch == 1 (no split-K). | ||
| * | ||
| * The scale pre-shuffle mirrors mx_gemm_profiler.hpp exactly: pack params are | ||
| * derived from SelectedKernel tile dims at compile time (not hardcoded). | ||
| */ | ||
|
|
||
| #include <hip/hip_runtime.h> | ||
| #include <cstdint> | ||
| #include <cstdlib> | ||
| #include <cstring> | ||
| #include <exception> | ||
| #include <iostream> | ||
| #include <vector> | ||
|
ozturkosu marked this conversation as resolved.
Resolved
|
||
|
|
||
| // Kernel header force-included via -include. Brings in ck_tile core + the | ||
| // ck_tile::MxGemmHostArgs type, SelectedKernel/KERNEL_NAME, and (via | ||
| // ck_tile/ops/gemm.hpp) the mx pipeline. We additionally pull the host | ||
| // helpers explicitly for HostTensor / preShuffleScaleBuffer_gfx950. | ||
| #include "ck_tile/host.hpp" | ||
| // Old-TE common helpers: provides the free-function template is_row_major(Layout) | ||
| // -> ck_tile::bool_constant<...>, matching the mx_gemm profiler usage. | ||
| #include "common/utils.hpp" | ||
|
|
||
| #ifndef GFX_ARCH | ||
| #define GFX_ARCH "gfx950" | ||
| #endif | ||
|
ozturkosu marked this conversation as resolved.
Resolved
|
||
|
|
||
| static bool g_initialized = false; | ||
|
|
||
| namespace { | ||
|
|
||
| int env_int(const char* name, int fallback) | ||
| { | ||
| const char* v = std::getenv(name); | ||
| if(!v) | ||
| return fallback; | ||
| return std::atoi(v); | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| extern "C" { | ||
|
|
||
| int dispatcher_initialize() | ||
| { | ||
| g_initialized = true; | ||
| return 0; | ||
| } | ||
| int dispatcher_init() { return dispatcher_initialize(); } | ||
|
|
||
| const char* dispatcher_get_kernel_name() { return KERNEL_NAME; } | ||
| int dispatcher_get_kernel_count() { return 1; } | ||
|
|
||
| void dispatcher_cleanup() { g_initialized = false; } | ||
|
|
||
| /** | ||
| * Run microscaling GEMM: C[M,N] = (A[M,K] * scale_a) . (B[K,N] * scale_b). | ||
| * | ||
| * A, B, C are host pointers. scale_a/scale_b are raw e8m0 bytes, unpacked and | ||
| * unshuffled, shaped [M, K/32] and [N, K/32] row-major respectively. The lib | ||
| * builds HostTensors from them, runs the SAME preShuffleScaleBuffer_gfx950 path | ||
| * as the Old-TE profiler, uploads everything, and calls SelectedKernel::launch. | ||
| * | ||
| * Returns 0 ok, -1 HIP/bad-args, -2 kernel reports unsupported args. | ||
| */ | ||
| int dispatcher_run_mx_gemm(const void* A, | ||
| const void* B, | ||
| void* C, | ||
| const uint8_t* scale_a, | ||
| const uint8_t* scale_b, | ||
| int M, | ||
| int N, | ||
| int K, | ||
| int k_batch, | ||
| float* time_ms) | ||
| { | ||
| if(!g_initialized) | ||
| { | ||
| std::cerr << "dispatcher_run_mx_gemm: not initialized\n"; | ||
| return -1; | ||
| } | ||
| if(!A || !B || !C || !scale_a || !scale_b) | ||
| { | ||
| std::cerr << "dispatcher_run_mx_gemm: null pointer\n"; | ||
| return -1; | ||
| } | ||
| if(M <= 0 || N <= 0 || K <= 0) | ||
| { | ||
| std::cerr << "dispatcher_run_mx_gemm: M,N,K must be > 0\n"; | ||
| return -1; | ||
| } | ||
| if(K % 32 != 0) | ||
| { | ||
| std::cerr << "dispatcher_run_mx_gemm: MX GEMM requires K to be a multiple of 32, got K=" | ||
| << K << "\n"; | ||
| return -1; | ||
| } | ||
| if(k_batch != 1) | ||
| { | ||
| // v1 scope: split-K (k_batch > 1) is not yet supported through this bridge. | ||
| std::cerr << "dispatcher_run_mx_gemm: only k_batch==1 is supported in v1, got " << k_batch | ||
| << "\n"; | ||
| return -1; | ||
| } | ||
|
ozturkosu marked this conversation as resolved.
Resolved
|
||
|
|
||
| const ALayout layout_a = ALayout{}; | ||
| const BLayout layout_b = BLayout{}; | ||
| const CLayout layout_c = CLayout{}; | ||
|
|
||
| const ck_tile::index_t m = static_cast<ck_tile::index_t>(M); | ||
| const ck_tile::index_t n = static_cast<ck_tile::index_t>(N); | ||
| const ck_tile::index_t k = static_cast<ck_tile::index_t>(K); | ||
|
|
||
| // Packed default strides for the compiled-in layouts (rcr: A row-major, | ||
| // B col-major, C row-major), matching the Old-TE profiler. get_default_stride | ||
| // takes the row-major-ness as a compile-time bool_constant tag (4th arg). | ||
| const ck_tile::index_t stride_a = | ||
| static_cast<ck_tile::index_t>(ck_tile::get_default_stride(m, k, 0, is_row_major(layout_a))); | ||
| const ck_tile::index_t stride_b = | ||
| static_cast<ck_tile::index_t>(ck_tile::get_default_stride(k, n, 0, is_row_major(layout_b))); | ||
| const ck_tile::index_t stride_c = | ||
| static_cast<ck_tile::index_t>(ck_tile::get_default_stride(m, n, 0, is_row_major(layout_c))); | ||
|
|
||
| const ck_tile::index_t scale_k_size = k / 32; | ||
|
|
||
| // ---- Scale pre-shuffle pack params, derived from SelectedKernel (compile | ||
| // time), exactly mirroring mx_gemm_profiler.hpp. ---- | ||
| constexpr ck_tile::index_t m_per_xdl = SelectedKernel::WarpTileM; | ||
| constexpr ck_tile::index_t n_per_xdl = SelectedKernel::WarpTileN; | ||
| constexpr ck_tile::index_t k_per_xdl = SelectedKernel::WarpTileK; | ||
| constexpr ck_tile::index_t m_iter_per_warp = | ||
| SelectedKernel::TileM / (SelectedKernel::WarpPerBlock_M * m_per_xdl); | ||
| constexpr ck_tile::index_t n_iter_per_warp = | ||
| SelectedKernel::TileN / (SelectedKernel::WarpPerBlock_N * n_per_xdl); | ||
| constexpr ck_tile::index_t k_iter_per_warp = SelectedKernel::TileK / k_per_xdl; | ||
|
|
||
| constexpr ck_tile::index_t m_xdl_pack = | ||
| (m_iter_per_warp >= 2 && m_iter_per_warp % 2 == 0) ? 2 : 1; | ||
| constexpr ck_tile::index_t n_xdl_pack = | ||
| (n_iter_per_warp >= 2 && n_iter_per_warp % 2 == 0) ? 2 : 1; | ||
| constexpr ck_tile::index_t k_xdl_pack = | ||
| (k_iter_per_warp >= 2 && k_iter_per_warp % 2 == 0) ? 2 : 1; | ||
|
|
||
| constexpr ck_tile::index_t xdl_mn_thread = SelectedKernel::WarpTileM; | ||
| constexpr ck_tile::index_t xdl_k_thread = 64 / xdl_mn_thread; | ||
|
|
||
| // ---- Build unshuffled scale HostTensors from the incoming raw e8m0 bytes. | ||
| // scale_a: [M, K/32] row-major; scale_b: [N, K/32] row-major. ---- | ||
| ck_tile::HostTensor<ScaleType> scale_a_host( | ||
| {static_cast<std::size_t>(m), static_cast<std::size_t>(scale_k_size)}, | ||
| {static_cast<std::size_t>(scale_k_size), static_cast<std::size_t>(1)}); | ||
| ck_tile::HostTensor<ScaleType> scale_b_host( | ||
| {static_cast<std::size_t>(n), static_cast<std::size_t>(scale_k_size)}, | ||
| {static_cast<std::size_t>(scale_k_size), static_cast<std::size_t>(1)}); | ||
|
|
||
| const std::size_t scale_a_count = static_cast<std::size_t>(m) * scale_k_size; | ||
| const std::size_t scale_b_count = static_cast<std::size_t>(n) * scale_k_size; | ||
| // e8m0_t wraps a single uint8 (raw biased exponent); construct 1:1 from the | ||
| // incoming raw byte via the explicit e8m0_t(raw_type) constructor. | ||
| for(std::size_t i = 0; i < scale_a_count; ++i) | ||
| scale_a_host.mData[i] = ScaleType(static_cast<typename ScaleType::raw_type>(scale_a[i])); | ||
| for(std::size_t i = 0; i < scale_b_count; ++i) | ||
| scale_b_host.mData[i] = ScaleType(static_cast<typename ScaleType::raw_type>(scale_b[i])); | ||
|
|
||
| // ---- Shuffled scale buffers (same lengths as the profiler). ---- | ||
| ck_tile::HostTensor<ScaleType> scale_a_shuffled( | ||
| {static_cast<std::size_t>(m / m_xdl_pack * 2), | ||
| static_cast<std::size_t>(scale_k_size / k_xdl_pack * 2)}, | ||
| {static_cast<std::size_t>(scale_k_size / k_xdl_pack * 2), static_cast<std::size_t>(1)}); | ||
| ck_tile::HostTensor<ScaleType> scale_b_shuffled( | ||
| {static_cast<std::size_t>(n / n_xdl_pack * 2), | ||
| static_cast<std::size_t>(scale_k_size / k_xdl_pack * 2)}, | ||
| {static_cast<std::size_t>(scale_k_size / k_xdl_pack * 2), static_cast<std::size_t>(1)}); | ||
|
|
||
| ck_tile::preShuffleScaleBuffer_gfx950<m_xdl_pack, k_xdl_pack, xdl_mn_thread, xdl_k_thread>( | ||
| scale_a_host.mData.data(), scale_a_shuffled.mData.data(), m, scale_k_size, true); | ||
| ck_tile::preShuffleScaleBuffer_gfx950<n_xdl_pack, k_xdl_pack, xdl_mn_thread, xdl_k_thread>( | ||
|
ozturkosu marked this conversation as resolved.
Resolved
|
||
| scale_b_host.mData.data(), scale_b_shuffled.mData.data(), n, scale_k_size, true); | ||
|
ozturkosu marked this conversation as resolved.
Resolved
|
||
|
|
||
| // ---- Build A/B/C HostTensors with the SAME descriptors the Old-TE profiler | ||
| // uses, then allocate/copy through ck_tile::DeviceMem. This makes the byte | ||
| // accounting packing-correct for BOTH fp8 (PackedSize==1) and fp4 | ||
| // (pk_fp4_t::PackedSize==2, i.e. two logical e2m1 elements per byte): | ||
| // HostTensor<T>::get_element_space_size_in_bytes() == | ||
| // sizeof(T) * (logical_elems / numeric_traits<T>::PackedSize). | ||
| // The incoming A/B host buffers are already PHYSICALLY packed | ||
| // ([M, K/PackedSize] and [N, K/PackedSize] bytes for fp4; [M,K]/[N,K] for | ||
| // fp8), so we byte-copy them into mData (which is sized in physical objects). | ||
| // Strides remain in LOGICAL element units (get_default_stride above uses | ||
| // logical M,N,K), which is what the kernel expects. | ||
| ck_tile::HostTensor<ADataType> a_m_k( | ||
| ck_tile::host_tensor_descriptor(m, k, stride_a, is_row_major(layout_a))); | ||
| ck_tile::HostTensor<BDataType> b_k_n( | ||
| ck_tile::host_tensor_descriptor(k, n, stride_b, is_row_major(layout_b))); | ||
| ck_tile::HostTensor<CDataType> c_m_n( | ||
| ck_tile::host_tensor_descriptor(m, n, stride_c, is_row_major(layout_c))); | ||
|
|
||
| const std::size_t a_bytes = a_m_k.get_element_space_size_in_bytes(); | ||
| const std::size_t b_bytes = b_k_n.get_element_space_size_in_bytes(); | ||
| const std::size_t c_bytes = c_m_n.get_element_space_size_in_bytes(); | ||
|
|
||
| std::memcpy(a_m_k.mData.data(), A, a_bytes); | ||
| std::memcpy(b_k_n.mData.data(), B, b_bytes); | ||
|
|
||
| float exec_time = 0.0f; | ||
| try | ||
| { | ||
| // ---- Device buffers, sized via HostTensor (packing-correct). ---- | ||
| ck_tile::DeviceMem a_dev_buf(a_m_k.get_element_space_size_in_bytes()); | ||
| ck_tile::DeviceMem b_dev_buf(b_k_n.get_element_space_size_in_bytes()); | ||
| ck_tile::DeviceMem c_dev_buf(c_m_n.get_element_space_size_in_bytes()); | ||
| ck_tile::DeviceMem scale_a_dev_buf(scale_a_shuffled.get_element_space_size_in_bytes()); | ||
| ck_tile::DeviceMem scale_b_dev_buf(scale_b_shuffled.get_element_space_size_in_bytes()); | ||
|
|
||
| a_dev_buf.ToDevice(a_m_k.data()); | ||
| b_dev_buf.ToDevice(b_k_n.data()); | ||
| c_dev_buf.SetZero(); | ||
| scale_a_dev_buf.ToDevice(scale_a_shuffled.data()); | ||
| scale_b_dev_buf.ToDevice(scale_b_shuffled.data()); | ||
|
|
||
| // ---- Build MxGemmHostArgs in the exact profiler argument order: | ||
| // a_ptr, scale_a, b_ptr, scale_b, ds{}, c_ptr, split_k, m, n, k, | ||
| // {stride_a}, {stride_b}, ds_strides{}, stride_c. ---- | ||
| MxGemmHostArgs gemm_args({a_dev_buf.GetDeviceBuffer()}, | ||
| {scale_a_dev_buf.GetDeviceBuffer()}, | ||
| {b_dev_buf.GetDeviceBuffer()}, | ||
| {scale_b_dev_buf.GetDeviceBuffer()}, | ||
| {}, | ||
| c_dev_buf.GetDeviceBuffer(), | ||
| static_cast<ck_tile::index_t>(k_batch), | ||
| m, | ||
| n, | ||
| k, | ||
| {stride_a}, | ||
| {stride_b}, | ||
| {}, | ||
| stride_c); | ||
|
|
||
| const bool do_time = (time_ms != nullptr); | ||
| const int warmup = do_time ? env_int("CK_TILE_BENCH_WARMUP", 20) : 0; | ||
| const int repeat = do_time ? env_int("CK_TILE_BENCH_REPEAT", 50) : 1; | ||
| // stream_config field order: stream_id, time_kernel, log_level, cold_niters | ||
| // (warmup), nrepeat, is_gpu_timer, flush_cache, rotating_count. | ||
| ck_tile::stream_config stream_cfg{nullptr, do_time, 0, warmup, repeat, false, false, 1}; | ||
|
|
||
| exec_time = SelectedKernel::launch(gemm_args, stream_cfg); | ||
| if(exec_time < 0.0f) | ||
| { | ||
| std::cerr << "dispatcher_run_mx_gemm: kernel reports unsupported args\n"; | ||
| return -2; | ||
| } | ||
|
|
||
| c_dev_buf.FromDevice(c_m_n.data()); | ||
| } | ||
| catch(const std::exception& e) | ||
| { | ||
| std::cerr << "dispatcher_run_mx_gemm: launch threw: " << e.what() << "\n"; | ||
| return -1; | ||
| } | ||
|
|
||
| // C is always a non-packed output type (fp16/bf16), so c_bytes byte-copy back | ||
| // to the caller buffer is a 1:1 element copy. | ||
| std::memcpy(C, c_m_n.mData.data(), c_bytes); | ||
|
|
||
| if(time_ms) | ||
| *time_ms = exec_time; | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| } // extern "C" | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.