[rocm-libraries] ROCm/rocm-libraries#7199 (commit 23f7320)

[CK_TILE] [QuantGEMM] Fix SplitK tail handling and other
 improvements (#7199)

This pull request introduces improved and more robust split-K support
for quantized GEMM. The main changes add runtime validation, utility
functions for split-K batch calculations, pointer offset handling for
split-K in grouped kernels, and enhanced support for various tensor
layouts. The changes also improve error handling and provide more
flexibility for runtime tail handling in split-K pipelines.

**Split-K Support and Validation Enhancements:**

* Added runtime validation to ensure `k_batch` is a positive integer and
that split-K configurations do not produce empty final batches or
mismatched pipeline tails, with detailed error messages and logging for
misconfiguration.
[[1]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871R1184-R1211)
[[2]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871L1161-R1250)
* Introduced utility functions `get_splitk_batch_k_read` and
`get_splitk_last_batch_k` to compute per-batch K read sizes and handle
split rounding, ensuring correct and consistent split-K batch
partitioning.
[[1]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871R206-R234)
[[2]](diffhunk://#diff-635b89bdffa96b2b42f1632520cde36701d7d631e864185591f6b32f7645cf47L104-R107)
[[3]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871L388-R417)
[[4]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871L1161-R1250)
* Changed the default value of `k_batch` in `QuantGemmHostArgs` to 1 (no
split-K) for safer default behavior.

**Pointer Offsets and Grouped Kernel Handling:**

* Updated `QuantGroupedGemmKernel` to apply split-K per-batch offsets to
all input pointers, mirroring the behavior of non-grouped kernels and
ensuring correctness for split-K launches.
* Modified AQ tensor view handling to correctly reflect the remaining
K-groups from the split-K batch's offset position, improving accuracy
for split-K in grouped kernels.

**Pipeline and Layout Flexibility:**

* Added support for runtime selection of split-K tail handling via a new
template parameter `RuntimeSplitKTail_`, with new helper methods to
dispatch GEMM pipelines accordingly.
[[1]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871R273)
[[2]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871R1496-R1567)
[[3]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871L1427)
[[4]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871L1447-R1629)
[[5]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871L1459-R1641)
* Improved handling for tensor layout cases, including preshuffled B and
both row-major and column-major AQ layouts, ensuring correct pointer
arithmetic and compatibility checks.
[[1]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871R438-R454)
[[2]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871L464-R516)
[[3]](diffhunk://#diff-d000149a681cd42bfb9947872c603e556cea26cbd7fd4f8f60afc6595d975871R1184-R1211)
This commit is contained in:
Sami Remes
2026-06-05 11:41:49 +00:00
committed by assistant-librarian[bot]
parent 7b9245f18c
commit ad4e2e7624
11 changed files with 909 additions and 89 deletions

View File

@@ -103,9 +103,12 @@ float gemm_calc_quant(const ck_tile::QuantGemmHostArgs& args, const ck_tile::str
}();
using BaseGemmPipeline = std::decay_t<decltype(base_gemm_pipeline)>;
const ck_tile::index_t K_split = ck_tile::integer_least_multiple(args.K, GemmConfig::K_Tile);
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
constexpr auto K1 = GemmShape::WarpTile::at(ck_tile::number<2>{});
const ck_tile::index_t K_split =
args.k_batch == 1 ? ck_tile::integer_least_multiple(args.K, GemmConfig::K_Tile)
: ck_tile::get_splitk_batch_k_read(args.K, args.k_batch, K1);
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
const ck_tile::TailNumber tail_num = BaseGemmPipeline::GetBlockLoopTailNum(num_loop);
const auto Run = [&](const auto has_hot_loop_, const auto tail_number_) {

View File

@@ -180,7 +180,8 @@ struct QuantGemmHostArgs : public QuantGemmProblem
const void* aq_ptr = nullptr;
const void* bq_ptr = nullptr;
void* c_ptr = nullptr;
index_t k_batch = 0;
// k_batch must be a positive integer; defaults to 1 (no split-K).
index_t k_batch = 1;
};
struct QuantGemmKernelArgs
@@ -203,10 +204,35 @@ struct QuantGemmKernelArgs
index_t k_batch;
};
CK_TILE_HOST_DEVICE auto
get_splitk_batch_k_read(index_t K, index_t k_batch, index_t k_unit) noexcept -> index_t
{
// k_batch and k_unit must be positive integers. Callers are expected to
// validate via IsSupportedArgument(); this fallback returns K so a
// misconfigured launch behaves as a no-split kernel.
if(k_batch <= 0 || k_unit <= 0)
{
return K;
}
const index_t k_t = k_batch * k_unit;
return (K + k_t - 1) / k_t * k_unit;
}
CK_TILE_HOST_DEVICE auto
get_splitk_last_batch_k(index_t K, index_t k_batch, index_t k_read) noexcept -> index_t
{
if(k_batch <= 0)
{
return K;
}
return K - k_read * (k_batch - 1);
}
template <typename TilePartitioner_,
typename GemmPipeline_,
typename EpiloguePipeline_,
QuantType QuantType_>
QuantType QuantType_,
bool RuntimeSplitKTail_ = false>
struct QuantGemmKernel
{
using TilePartitioner = remove_cvref_t<TilePartitioner_>;
@@ -244,7 +270,8 @@ struct QuantGemmKernel
static constexpr auto I3 = number<3>(); // BQ Tensor
static constexpr auto I4 = number<4>(); // C Tensor
static constexpr auto kQuantType = QuantType_;
static constexpr auto kQuantType = QuantType_;
static constexpr bool RuntimeSplitKTail = RuntimeSplitKTail_;
[[nodiscard]] CK_TILE_HOST static const std::string GetName()
{
@@ -386,11 +413,9 @@ struct QuantGemmKernel
{
constexpr auto K1 =
GemmPipeline::BlockGemmShape::WarpTile::at(I2); // smallest unit of K work per block
const index_t K_t = amd_wave_read_first_lane(
kargs.k_batch * K1); // amount of K elements consumed if every split-K batch
// performs exactly one "unit" (K1)
const index_t KRead = amd_wave_read_first_lane(
(kargs.K + K_t - 1) / K_t * K1); // total k elements to be read in this batch
const index_t KRead =
amd_wave_read_first_lane(get_splitk_batch_k_read(kargs.K, kargs.k_batch, K1));
// total k elements to be read in this batch
// offset not necessarily = KRead, because B can have packed elements (e.g. fp8i4)
constexpr index_t BPackedSize =
ck_tile::numeric_traits<remove_cvref_t<BDataType>>::PackedSize;
@@ -412,7 +437,21 @@ struct QuantGemmKernel
}
else if constexpr(std::is_same_v<tensor_layout::gemm::ColumnMajor, BLayout>)
{
b_k_split_offset = amd_wave_read_first_lane(b_k_offset_elements);
if constexpr(PreshuffleB)
{
// Preshuffled B is laid out as [N/N_Warp_Tile, K_outer, N_Warp_Tile, K_inner]
// (see shuffle_b<>), where each "N_outer" row spans N_Warp_Tile * full_K
// linear elements. MakeBBlockWindow already builds the descriptor with
// stride [N_Warp_Tile * kargs.K, 1], so to advance the K starting position
// by k_id * KRead within row 0 we need to advance the pointer by
// (k_id * KRead) * N_Warp_Tile -- not just (k_id * KRead).
constexpr index_t N_Warp_Tile = GemmPipeline::BlockGemmShape::WarpTile::at(I1);
b_k_split_offset = amd_wave_read_first_lane(b_k_offset_elements * N_Warp_Tile);
}
else
{
b_k_split_offset = amd_wave_read_first_lane(b_k_offset_elements);
}
}
if(k_id < static_cast<uint32_t>(kargs.k_batch - 1))
@@ -462,12 +501,20 @@ struct QuantGemmKernel
using BQuantGroupSize = remove_cvref_t<typename GemmPipeline::BQuantGroupSize>;
// Compute AQ K-group offset for this split-K batch.
// AQ tensor layout is RowMajor [M, QK_A] with stride [stride_AQ, 1].
// Advancing to column aq_group_offset means a pointer offset of aq_group_offset
// elements (column stride = 1).
const index_t k_offset_aq = amd_wave_read_first_lane(k_id * KRead);
aq_group_offset = amd_wave_read_first_lane(k_offset_aq / AQuantGroupSize::kK);
aq_k_split_offset = amd_wave_read_first_lane(aq_group_offset);
aq_group_offset = amd_wave_read_first_lane(k_offset_aq / AQuantGroupSize::kK);
if constexpr(std::is_same_v<AQLayout, tensor_layout::gemm::RowMajor>)
{
// RowMajor AQ is [M, QK_A] with stride [stride_AQ, 1].
// Advancing to K-group column g is a pointer offset of g.
aq_k_split_offset = amd_wave_read_first_lane(aq_group_offset);
}
else if constexpr(std::is_same_v<AQLayout, tensor_layout::gemm::ColumnMajor>)
{
// ColumnMajor AQ is [QK_A, M] with K-group row stride stride_AQ.
// Advancing to K-group row g is a pointer offset of g * stride_AQ.
aq_k_split_offset = amd_wave_read_first_lane(aq_group_offset * kargs.stride_AQ);
}
// Compute BQ K-group offset for this split-K batch.
// BQ tensor layout is ColumnMajor [N/kN, K/kK] with stride [K/kK, 1] for
@@ -1135,6 +1182,34 @@ struct QuantGemmKernel
CK_TILE_HOST static bool IsSupportedArgument(const QuantGemmKernelArgs& kargs)
{
// k_batch must be a positive integer.
if(kargs.k_batch <= 0)
{
if(ck_tile::EnvIsEnabled(CK_TILE_ENV(CK_TILE_LOGGING)))
{
CK_TILE_ERROR("k_batch must be a positive integer (got " +
std::to_string(kargs.k_batch) + ")!");
}
return false;
}
// The split-K K-unit (warp-tile K dimension) must be positive too;
// it is a compile-time constant taken from the pipeline shape.
static_assert(GemmPipeline::BlockGemmShape::WarpTile::at(I2) > 0,
"Pipeline warp-tile K dimension (k_unit) must be positive.");
// ABQuantGrouped does not currently support RowMajor BQ layout: the
// BQ tensor view, tile window, and split-K offset code are all
// written for ColumnMajor BQ. The deeper static_asserts in
// MakeBQBlockWindow enforce this at instantiation time; surface it
// here at the host-arg entry point too so the limitation is visible
// before the first device-side instantiation.
static_assert(!(kQuantType == QuantType::ABQuantGrouped &&
std::is_same_v<BQLayout, tensor_layout::gemm::RowMajor>),
"ABQuantGrouped does not currently support RowMajor BQ layout. "
"Use ColumnMajor BQ (or extend MakeBQBlockWindow and the split-K "
"BQ offset path to handle RowMajor BQ).");
// Split-K is supported for BQuantGrouped (without preshuffle) and
// ABQuantGrouped (without APreshuffleQuant) modes.
if(kargs.k_batch != 1)
@@ -1158,12 +1233,22 @@ struct QuantGemmKernel
}
else
{
constexpr auto K1 = GemmPipeline::BlockGemmShape::WarpTile::at(I2);
const index_t K_t = kargs.k_batch * K1;
const index_t KRead = (kargs.K + K_t - 1) / K_t * K1; // per-batch K read size
constexpr auto K1 = GemmPipeline::BlockGemmShape::WarpTile::at(I2);
const index_t KRead =
get_splitk_batch_k_read(kargs.K, kargs.k_batch, K1); // per-batch K read size
const index_t KLast = get_splitk_last_batch_k(kargs.K, kargs.k_batch, KRead);
constexpr index_t BPackedSize =
ck_tile::numeric_traits<remove_cvref_t<BDataType>>::PackedSize;
if(KLast <= 0)
{
if(ck_tile::EnvIsEnabled(CK_TILE_ENV(CK_TILE_LOGGING)))
{
CK_TILE_ERROR("Split-K configuration produces an empty final K batch!");
}
return false;
}
// Constraint 1: KRead must align with B packing requirements.
// For packed data types, multiple K elements are stored in each storage unit.
// Split-K advances the B pointer by (KRead / BPackedSize) storage units per batch.
@@ -1223,8 +1308,7 @@ struct QuantGemmKernel
// (i.e. per_batch_num_loop == 1) the prefetch would read the tile
// belonging to the next split-K batch, producing incorrect results.
{
const index_t per_batch_num_loop =
KRead / static_cast<index_t>(TilePartitioner::KPerBlock);
const index_t per_batch_num_loop = TilePartitioner::GetLoopNum(KRead);
if(per_batch_num_loop < 2)
{
if(ck_tile::EnvIsEnabled(CK_TILE_ENV(CK_TILE_LOGGING)))
@@ -1240,6 +1324,33 @@ struct QuantGemmKernel
return false;
}
}
// Host-side fixed tail selection is only valid when all split-K batches have
// the same hot-loop/tail classification. Earlier batches use KRead; the final
// batch may be shorter due to split rounding.
{
const index_t first_num_loop = TilePartitioner::GetLoopNum(KRead);
const index_t last_num_loop = TilePartitioner::GetLoopNum(KLast);
const bool first_hot_loop = GemmPipeline::BlockHasHotloop(first_num_loop);
const bool last_hot_loop = GemmPipeline::BlockHasHotloop(last_num_loop);
const auto first_tail = GemmPipeline::GetBlockLoopTailNum(first_num_loop);
const auto last_tail = GemmPipeline::GetBlockLoopTailNum(last_num_loop);
if constexpr(!RuntimeSplitKTail)
{
if(first_hot_loop != last_hot_loop || first_tail != last_tail)
{
if(ck_tile::EnvIsEnabled(CK_TILE_ENV(CK_TILE_LOGGING)))
{
CK_TILE_ERROR(
"Split-K batches require different hot-loop/tail handling. "
"Use a K/k_batch combination that gives matching pipeline "
"tails or enable runtime split-K tail dispatch.");
}
return false;
}
}
}
}
}
@@ -1383,6 +1494,78 @@ struct QuantGemmKernel
return true;
}
template <typename ADramBlockWindow, typename BDramBlockWindow, typename BQDramBlockWindow>
CK_TILE_DEVICE static auto CallBQuantGemmPipeline(const ADramBlockWindow& a_block_window,
const BDramBlockWindow& b_block_window,
const BQDramBlockWindow& bq_block_window,
const index_t num_loop,
void* smem_ptr,
const index_t n)
{
if constexpr(RuntimeSplitKTail)
{
static_assert(!PreshuffleB,
"RuntimeSplitKTail is not implemented for preshuffle-B BQuant "
"pipelines.");
const bool has_hot_loop = GemmPipeline::BlockHasHotloop(num_loop);
const TailNumber tail_num = GemmPipeline::GetBlockLoopTailNum(num_loop);
return GemmPipeline{}(a_block_window,
b_block_window,
bq_block_window,
num_loop,
has_hot_loop,
tail_num,
smem_ptr,
n);
}
else
{
return GemmPipeline{}(
a_block_window, b_block_window, bq_block_window, num_loop, smem_ptr, n);
}
}
template <typename ADramBlockWindow,
typename BDramBlockWindow,
typename AQDramBlockWindow,
typename BQDramBlockWindow>
CK_TILE_DEVICE static auto CallABQuantGemmPipeline(const ADramBlockWindow& a_block_window,
const BDramBlockWindow& b_block_window,
const AQDramBlockWindow& aq_block_window,
const BQDramBlockWindow& bq_block_window,
const index_t num_loop,
void* smem_ptr,
const index_t m,
const index_t n)
{
if constexpr(RuntimeSplitKTail)
{
const bool has_hot_loop = GemmPipeline::BlockHasHotloop(num_loop);
const TailNumber tail_num = GemmPipeline::GetBlockLoopTailNum(num_loop);
return GemmPipeline{}(a_block_window,
b_block_window,
aq_block_window,
bq_block_window,
num_loop,
has_hot_loop,
tail_num,
smem_ptr,
m,
n);
}
else
{
return GemmPipeline{}(a_block_window,
b_block_window,
aq_block_window,
bq_block_window,
num_loop,
smem_ptr,
m,
n);
}
}
/**
* @brief Runs single GEMM problem cooperatively by whole workgroup.
*
@@ -1425,7 +1608,6 @@ struct QuantGemmKernel
const index_t num_loop =
amd_wave_read_first_lane(TilePartitioner::GetLoopNum(splitk_batch_offset.splitted_k));
// Run GEMM cooperatively by whole workgroup.
const auto& c_block_tile = [&]() {
if constexpr(kQuantType == QuantType::AQuantGrouped)
@@ -1445,7 +1627,7 @@ struct QuantGemmKernel
{
n = kargs.N;
}
return GemmPipeline{}(
return CallBQuantGemmPipeline(
a_block_window, b_block_window, bq_block_window, num_loop, smem_ptr, n);
}
else if constexpr(kQuantType == QuantType::ABQuantGrouped)
@@ -1457,14 +1639,14 @@ struct QuantGemmKernel
// m = kargs.M;
n = kargs.N;
}
return GemmPipeline{}(a_block_window,
b_block_window,
aq_block_window,
bq_block_window,
num_loop,
smem_ptr,
m,
n);
return CallABQuantGemmPipeline(a_block_window,
b_block_window,
aq_block_window,
bq_block_window,
num_loop,
smem_ptr,
m,
n);
}
else if constexpr(kQuantType == QuantType::RowColQuant ||
kQuantType == QuantType::TensorQuant)
@@ -1557,7 +1739,6 @@ struct QuantGemmKernel
// allocate LDS
__shared__ char smem_ptr[GetSmemSize()];
assert(kargs.k_batch == 1);
RunGemm(
a_ptr, b_ptr, aq_ptr, bq_ptr, c_ptr, smem_ptr, kargs, splitk_batch_offset, i_m, i_n);
}

View File

@@ -314,12 +314,18 @@ struct QuantGroupedGemmKernel
const typename Base::SplitKBatchOffset splitk_batch_offset(kargs, block_idx_z);
// options
const ADataType* a_ptr = static_cast<const ADataType*>(kargs.a_ptr);
const BDataType* b_ptr = static_cast<const BDataType*>(kargs.b_ptr);
const AQDataType* aq_ptr = static_cast<const AQDataType*>(kargs.aq_ptr);
const BQDataType* bq_ptr = static_cast<const BQDataType*>(kargs.bq_ptr);
CDataType* c_ptr = static_cast<CDataType*>(kargs.c_ptr);
// Apply split-K per-batch offsets to every input pointer, mirroring the
// non-grouped QuantGemmKernel::Run_ path. All offsets are 0 when
// k_batch == 1, so this is a no-op for non-split-K launches.
const ADataType* a_ptr =
static_cast<const ADataType*>(kargs.a_ptr) + splitk_batch_offset.a_k_split_offset;
const BDataType* b_ptr =
static_cast<const BDataType*>(kargs.b_ptr) + splitk_batch_offset.b_k_split_offset;
const AQDataType* aq_ptr =
static_cast<const AQDataType*>(kargs.aq_ptr) + splitk_batch_offset.aq_k_split_offset;
const BQDataType* bq_ptr =
static_cast<const BQDataType*>(kargs.bq_ptr) + splitk_batch_offset.bq_k_split_offset;
CDataType* c_ptr = static_cast<CDataType*>(kargs.c_ptr);
// allocate LDS
__shared__ char smem_ptr[GetSmemSize()];
@@ -454,8 +460,11 @@ struct QuantGroupedGemmKernel
Base::MakeABlockWindow(a_ptr, kargs, splitk_batch_offset.splitted_k, block_idx_m);
const auto& b_block_window =
Base::MakeBBlockWindow(b_ptr, kargs, splitk_batch_offset.splitted_k, block_idx_n);
const auto& aq_block_window =
Base::MakeAQBlockWindow(aq_ptr, kargs, block_idx_m, block_idx_n);
// Pass aq_group_offset so the AQ tensor view dimension reflects the
// remaining K-groups from this split-K batch's offset position
// (mirrors QuantGemmKernel::RunGemm).
const auto& aq_block_window = Base::MakeAQBlockWindow(
aq_ptr, kargs, block_idx_m, block_idx_n, splitk_batch_offset.aq_group_offset);
const auto& bq_block_window = Base::MakeBQBlockWindow(
bq_ptr, kargs, splitk_batch_offset.bq_group_offset, block_idx_m, block_idx_n);

View File

@@ -637,8 +637,10 @@ struct WPABQuantBPipelineAgBgCrV2 : public WeightPreshufflePipelineAGmemBGmemCRe
const AQDramBlockWindowTmp& aq_dram_block_window_tmp,
const BQDramBlockWindowTmp& bq_dram_block_window_tmp,
index_t num_loop,
bool has_hot_loop,
TailNumber tail_number,
void* p_smem,
index_t m = 0,
index_t n = 0) const
{
const auto RunPipeline = [&](auto bool_val, auto tail_num_) {
@@ -650,11 +652,14 @@ struct WPABQuantBPipelineAgBgCrV2 : public WeightPreshufflePipelineAGmemBGmemCRe
b_flat_dram_block_window_tmp,
aq_dram_block_window_tmp,
bq_dram_block_window_tmp,
n, // dummy value, won't be used
// The preshuffle-B ABQuant pipeline currently ignores m and n; keep this
// runtime-tail wrapper aligned with the generic ABQuant pipeline signature.
m,
n,
num_loop,
p_smem);
};
return Base::TailHandler(RunPipeline, true, tail_number);
return Base::TailHandler(RunPipeline, has_hot_loop, tail_number);
}
};
} // namespace ck_tile

View File

@@ -92,6 +92,11 @@ if(GPU_TARGETS MATCHES "gfx94|gfx95|gfx12")
)
target_compile_options(test_tile_gemm_quant_abquant_splitk_prefill PRIVATE ${TEST_GEMM_COMPILE_OPTIONS})
add_gtest_executable(test_tile_gemm_quant_abquant_splitk_preshuffleB
test_gemm_quant_abquant_splitk_preshuffleB.cpp
)
target_compile_options(test_tile_gemm_quant_abquant_splitk_preshuffleB PRIVATE ${TEST_GEMM_COMPILE_OPTIONS})
add_gtest_executable(test_tile_gemm_quant_abquant_a4w4_base
test_gemm_quant_abquant_a4w4_base.cpp
)
@@ -278,6 +283,7 @@ if(GPU_TARGETS MATCHES "gfx94|gfx95|gfx12")
# ABQuant split-K tests
test_tile_gemm_quant_abquant_splitk_decode
test_tile_gemm_quant_abquant_splitk_prefill
test_tile_gemm_quant_abquant_splitk_preshuffleB
# BQuant tests
test_tile_gemm_quant_bquant_1d_128
test_tile_gemm_quant_bquant_1d_64

View File

@@ -110,3 +110,46 @@ TYPED_TEST(TestCkTileGemmABQuant, SplitK8_LargeK_LargeMN)
// K=4096, larger M and N
this->run_test_with_validation(48, 192, 4096, 8);
}
// Test one padded-K split shape whose earlier split-K batches and final batch would need
// different compile-time pipeline tail handling. Fixed host-side tail selection rejects it.
//
// K=3328, k_batch=9, GemmConfigPadding (K_Tile=256):
// K_Warp_Tile is arch-dependent (gfx94/95: 32 or 64, gfx12 WMMA: 16); for all of these
// ceil(3328 / (9 * K_Warp_Tile)) * K_Warp_Tile = 384, so KRead is always a multiple of
// BQuantGroupSize::kK = AQuantGroupSize::kK = 128 (Constraints 2/3 of IsSupportedArgument).
// KLast = 3328 - 8*384 = 256.
// num_loop_first = ceil(384/256) = 2 (hot_loop=false, tail=Even)
// num_loop_last = ceil(256/256) = 1 (hot_loop=false, tail=Odd)
// Mismatched tail, so the fixed host-side dispatch rejects; the runtime-tail dispatch path
// (RuntimeSplitKTail=true) accepts and dispatches per-batch.
using ABQuantSplitKRejectTypes = ::testing::Types<std::tuple<RowMajor,
ColumnMajor,
RowMajor,
RowMajor,
FP8,
FP8,
float,
Half,
ABQuantGrouped,
GemmConfigPadding,
GroupSize1x1x128,
GroupSize1x1x128,
ColumnMajor>>;
template <typename Tuple>
class TestCkTileGemmABQuantSplitKReject : public TestCkTileGemmABQuant<Tuple>
{
};
TYPED_TEST_SUITE(TestCkTileGemmABQuantSplitKReject, ABQuantSplitKRejectTypes);
TYPED_TEST(TestCkTileGemmABQuantSplitKReject, RejectsMismatchedTailSplitK)
{
EXPECT_THROW(this->run_test_with_validation(32, 128, 3328, 9), std::runtime_error);
}
TYPED_TEST(TestCkTileGemmABQuantSplitKReject, RuntimeTailAllowsMismatchedTailSplitK)
{
this->run_test_with_validation(32, 128, 3328, 9, 0, true /* allow_runtime_splitk_tail */);
}

View File

@@ -0,0 +1,94 @@
// Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include "test_gemm_quant_common.hpp"
using GroupSize1x1x128 = ck_tile::QuantGroupShape<ck_tile::sequence<1, 1, 128>>;
using GroupSize1x128x128 = ck_tile::QuantGroupShape<ck_tile::sequence<1, 128, 128>>;
// ABQuant split-K with B-preshuffle pipeline (WPABQuantBPipelineAgBgCrV2).
// Exercises both the regular (uniform-split) and the runtime-tail (uneven-split)
// dispatch paths, mirroring the non-preshuffle reject/accept tests in
// test_gemm_quant_abquant_splitk_decode.cpp.
//
// Tuple format: <ALayout, BLayout, CLayout, AQLayout, ADataType, BDataType, QDataType, CDataType,
// QuantType, GemmConfig, AQuantGroupSize, BQuantGroupSize, BQLayout>
// =====================================================================================
// Uniform-split tests: every split-K batch has the same per-batch num_loop and tail
// classification, so the regular (non-runtime-tail) dispatch path applies.
// =====================================================================================
// clang-format off
using ABQuantSplitKPreshuffleBUniformTypes = ::testing::Types<
std::tuple<RowMajor, ColumnMajor, RowMajor, RowMajor, FP8, FP8, float, Half, ABQuantGrouped, GemmConfigPreshuffleBPrefill, GroupSize1x1x128, GroupSize1x128x128, ColumnMajor>,
std::tuple<RowMajor, ColumnMajor, RowMajor, RowMajor, BF8, BF8, float, Half, ABQuantGrouped, GemmConfigPreshuffleBPrefill, GroupSize1x1x128, GroupSize1x128x128, ColumnMajor>
>;
// clang-format on
TYPED_TEST_SUITE(TestCkTileGemmABQuant, ABQuantSplitKPreshuffleBUniformTypes);
// GemmConfigPreshuffleBPrefill: M_Tile=128, N_Tile=128, K_Tile=128, K_Warp_Tile=64.
// For uniform splits we want every batch to have the same num_loop classification:
// pick K such that KRead == KLast and num_loop_per_batch >= 2.
TYPED_TEST(TestCkTileGemmABQuant, PreshuffleB_SplitK2_K1024)
{
// K=1024, k_batch=2 -> KRead=KLast=512, num_loop=4 per batch (Even tail).
this->run_test_with_validation(128, 128, 1024, 2);
}
TYPED_TEST(TestCkTileGemmABQuant, PreshuffleB_SplitK4_K2048)
{
// K=2048, k_batch=4 -> KRead=KLast=512, num_loop=4 per batch (Even tail).
this->run_test_with_validation(128, 128, 2048, 4);
}
TYPED_TEST(TestCkTileGemmABQuant, PreshuffleB_SplitK2_LargeK_LargeN)
{
// K=2048, larger N (multiple of N_Tile=128).
this->run_test_with_validation(128, 256, 2048, 2);
}
// =====================================================================================
// Runtime-tail tests: K and k_batch chosen so the first split-K batch and the final
// (shorter) batch land in different (hot-loop, tail) classifications. The default
// host-side fixed tail dispatch must reject this; the runtime-tail dispatch path
// (RuntimeSplitKTail=true) must accept it. Uses the padded preshuffle config so
// uneven K passes the kPadK=false divisibility check (mirrors the non-preshuffle
// decode test against GemmConfigPadding).
// =====================================================================================
// clang-format off
using ABQuantSplitKPreshuffleBRuntimeTailTypes = ::testing::Types<
std::tuple<RowMajor, ColumnMajor, RowMajor, RowMajor, FP8, FP8, float, Half, ABQuantGrouped, GemmConfigPreshuffleBPrefillPadded, GroupSize1x1x128, GroupSize1x128x128, ColumnMajor>
>;
// clang-format on
template <typename Tuple>
class TestCkTileGemmABQuantSplitKPreshuffleBReject : public TestCkTileGemmABQuant<Tuple>
{
};
TYPED_TEST_SUITE(TestCkTileGemmABQuantSplitKPreshuffleBReject,
ABQuantSplitKPreshuffleBRuntimeTailTypes);
// K=3328, k_batch=9 with K_Tile=128:
// K_Warp_Tile is arch-dependent (gfx94/95: 64 for 8-bit, gfx12 WMMA: 16); for both
// ceil(3328 / (9 * K_Warp_Tile)) * K_Warp_Tile = 384, so KRead is always a multiple of
// BQuantGroupSize::kK = AQuantGroupSize::kK = 128 (Constraints 2/3 of IsSupportedArgument).
// KLast = 3328 - 8*384 = 256.
// num_loop_first = ceil(384/128) = 3 (hot_loop=true, tail=Odd)
// num_loop_last = ceil(256/128) = 2 (hot_loop=false, tail=Even)
// Both hot_loop and tail differ, so the fixed host-side dispatch rejects; the runtime-tail
// dispatch path (RuntimeSplitKTail=true) accepts and dispatches per-batch.
TYPED_TEST(TestCkTileGemmABQuantSplitKPreshuffleBReject, RejectsMismatchedTailSplitK)
{
EXPECT_THROW(this->run_test_with_validation(128, 128, 3328, 9), std::runtime_error);
}
TYPED_TEST(TestCkTileGemmABQuantSplitKPreshuffleBReject, RuntimeTailAllowsMismatchedTailSplitK)
{
this->run_test_with_validation(128, 128, 3328, 9, 0, true /* allow_runtime_splitk_tail */);
}

View File

@@ -53,6 +53,31 @@ struct SafeTupleElement<TTuple,
template <typename TTuple, size_t Index, typename DefaultType>
using SafeTupleElement_t = typename SafeTupleElement<TTuple, Index, DefaultType>::type;
namespace test_gemm_quant_base_detail {
// TODO: replace with C++20 requires later.
// C++17 detection idiom: true when
// T::run_quant_gemm_impl<Shape, Partitioner, Traits>(QuantGemmHostArgs, stream_config, bool)
// is a well-formed expression.
template <typename T, typename Shape, typename Partitioner, typename Traits, typename = void>
struct has_run_quant_gemm_impl_splitk : std::false_type
{
};
template <typename T, typename Shape, typename Partitioner, typename Traits>
struct has_run_quant_gemm_impl_splitk<
T,
Shape,
Partitioner,
Traits,
std::void_t<
decltype(std::declval<T*>()->template run_quant_gemm_impl<Shape, Partitioner, Traits>(
std::declval<const ck_tile::QuantGemmHostArgs&>(),
std::declval<const ck_tile::stream_config&>(),
std::declval<bool>()))>> : std::true_type
{
};
} // namespace test_gemm_quant_base_detail
// Base class for common quant gemm functionality
template <typename Tuple, typename Derived>
class TestCkTileGemmQuantBase : public ::testing::Test
@@ -114,7 +139,9 @@ class TestCkTileGemmQuantBase : public ::testing::Test
void TearDown() override { static_cast<Derived*>(this)->TearDownQuantTypeSpecific(); }
// Common test execution logic
void invoke_quant_gemm(const ck_tile::QuantGemmHostArgs& args, const ck_tile::stream_config& s)
void invoke_quant_gemm(const ck_tile::QuantGemmHostArgs& args,
const ck_tile::stream_config& s,
bool allow_runtime_splitk_tail = false)
{
// WP pipeline requires per-thread tile size aligned to Problem::VectorLoadSize.
// static_assert((WG::kM * WG::kK * sizeof(ADataType) * MIterPerWarp / WaveSize) %
@@ -149,9 +176,24 @@ class TestCkTileGemmQuantBase : public ::testing::Test
VectorSize>;
// Let the derived class create the appropriate pipeline and epilogue
static_cast<Derived*>(this)
->template run_quant_gemm_impl<CodegenGemmShape, TilePartitioner, CodegenGemmTraits>(
args, s);
auto* derived = static_cast<Derived*>(this);
if constexpr(test_gemm_quant_base_detail::has_run_quant_gemm_impl_splitk<
Derived,
CodegenGemmShape,
TilePartitioner,
CodegenGemmTraits>::value)
{
derived->template run_quant_gemm_impl<CodegenGemmShape,
TilePartitioner,
CodegenGemmTraits>(
args, s, allow_runtime_splitk_tail);
}
else
{
derived->template run_quant_gemm_impl<CodegenGemmShape,
TilePartitioner,
CodegenGemmTraits>(args, s);
}
}
void RunTest(ck_tile::index_t M, ck_tile::index_t N, ck_tile::index_t K)

View File

@@ -3,6 +3,8 @@
#pragma once
#include <type_traits>
#include "test_gemm_quant_base.hpp"
#include "ck_tile/host/permute_pk_int4.hpp"
#include "ck_tile/host/tensor_shuffle_utils.hpp"
@@ -145,6 +147,12 @@ struct GemmConfigPreshuffleBPrefillTransposeC : public GemmConfigPreshuffleBPref
static constexpr bool TransposeC = true;
};
struct GemmConfigPreshuffleBPrefillPadded : public GemmConfigPreshuffleBPrefill
{
static constexpr bool kPadN = true;
static constexpr bool kPadK = true;
};
struct GemmConfigPreshuffleQuantPrefill : public GemmConfigPrefill
{
static constexpr bool BPreshuffleQuant = true;
@@ -375,9 +383,12 @@ class TestCkTileGemmAQuant : public TestCkTileGemmQuantBase<Tuple, TestCkTileGem
using BaseGemmPipeline = ck_tile::BaseGemmPipelineAgBgCrCompV3<GemmPipelineProblem>;
const ck_tile::index_t K_split = (args.K + Base::K_Tile - 1) / Base::K_Tile * Base::K_Tile;
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
constexpr auto K1 = CodegenGemmShape::WarpTile::at(ck_tile::number<2>{});
const ck_tile::index_t K_split =
args.k_batch == 1 ? (args.K + Base::K_Tile - 1) / Base::K_Tile * Base::K_Tile
: ck_tile::get_splitk_batch_k_read(args.K, args.k_batch, K1);
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
const ck_tile::TailNumber tail_num = BaseGemmPipeline::GetBlockLoopTailNum(num_loop);
const auto Run = [&](const auto has_hot_loop_, const auto tail_number_) {
@@ -594,16 +605,19 @@ class TestCkTileGemmAQuantMem
void run_quant_gemm_impl(const ck_tile::QuantGemmHostArgs& args,
const ck_tile::stream_config& s)
{
using GemmPipelineProblem = ck_tile::GemmPipelineProblemBase<ADataType,
BDataType,
AccDataType,
CodegenGemmShape,
CodegenGemmTraits,
ComputeDataType>;
using BaseGemmPipeline = ck_tile::BaseGemmPipelineAgBgCrMem<GemmPipelineProblem>;
const ck_tile::index_t K_split = (args.K + Base::K_Tile - 1) / Base::K_Tile * Base::K_Tile;
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
using GemmPipelineProblem = ck_tile::GemmPipelineProblemBase<ADataType,
BDataType,
AccDataType,
CodegenGemmShape,
CodegenGemmTraits,
ComputeDataType>;
using BaseGemmPipeline = ck_tile::BaseGemmPipelineAgBgCrMem<GemmPipelineProblem>;
constexpr auto K1 = CodegenGemmShape::WarpTile::at(ck_tile::number<2>{});
const ck_tile::index_t K_split =
args.k_batch == 1 ? (args.K + Base::K_Tile - 1) / Base::K_Tile * Base::K_Tile
: ck_tile::get_splitk_batch_k_read(args.K, args.k_batch, K1);
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
const ck_tile::TailNumber tail_num = BaseGemmPipeline::GetBlockLoopTailNum(num_loop);
const auto Run = [&](const auto has_hot_loop_, const auto tail_number_) {
constexpr bool has_hot_loop_v = has_hot_loop_.value;
@@ -888,9 +902,12 @@ class TestCkTileGemmBQuant : public TestCkTileGemmQuantBase<Tuple, TestCkTileGem
ck_tile::BaseGemmPipelineAgBgCrCompV3<GemmPipelineProblem>,
ck_tile::BaseWeightPreshufflePipelineAGmemBGmemCRegV2<GemmPipelineProblem>>;
const ck_tile::index_t K_split = (args.K + Base::K_Tile - 1) / Base::K_Tile * Base::K_Tile;
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
constexpr auto K1 = CodegenGemmShape::WarpTile::at(ck_tile::number<2>{});
const ck_tile::index_t K_split =
args.k_batch == 1 ? (args.K + Base::K_Tile - 1) / Base::K_Tile * Base::K_Tile
: ck_tile::get_splitk_batch_k_read(args.K, args.k_batch, K1);
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
const ck_tile::TailNumber tail_num = BaseGemmPipeline::GetBlockLoopTailNum(num_loop);
const auto Run = [&](const auto has_hot_loop_, const auto tail_number_) {
@@ -1022,8 +1039,9 @@ class TestCkTileGemmABQuant : public TestCkTileGemmQuantBase<Tuple, TestCkTileGe
void run_test_with_validation(ck_tile::index_t M,
ck_tile::index_t N,
ck_tile::index_t K,
ck_tile::index_t k_batch = 1,
ck_tile::index_t stride_B_pad = 0)
ck_tile::index_t k_batch = 1,
ck_tile::index_t stride_B_pad = 0,
bool allow_runtime_splitk_tail = false)
{
const ck_tile::index_t stride_A =
ck_tile::get_default_stride(M, K, 0, this->is_row_major(ALayout{}));
@@ -1167,7 +1185,7 @@ class TestCkTileGemmABQuant : public TestCkTileGemmQuantBase<Tuple, TestCkTileGe
// Run the kernel
ck_tile::stream_config stream_config{};
this->invoke_quant_gemm(args, stream_config);
this->invoke_quant_gemm(args, stream_config, allow_runtime_splitk_tail);
// Validation using reference implementation
ck_tile::HostTensor<CDataType> c_m_n_host_ref(
@@ -1216,11 +1234,16 @@ class TestCkTileGemmABQuant : public TestCkTileGemmQuantBase<Tuple, TestCkTileGe
}
}
private:
// ABQuant-specific pipeline implementation
public:
// ABQuant-specific pipeline implementation. Public so the
// has_run_quant_gemm_impl_splitk SFINAE trait in
// test_gemm_quant_base.hpp can detect this 3-arg overload from outside
// the class (the trait lives in a different namespace and is not a
// friend of this fixture).
template <typename CodegenGemmShape, typename TilePartitioner, typename CodegenGemmTraits>
void run_quant_gemm_impl(const ck_tile::QuantGemmHostArgs& args,
const ck_tile::stream_config& s)
const ck_tile::stream_config& s,
bool allow_runtime_splitk_tail)
{
static_assert(std::is_same_v<CLayout, ck_tile::tensor_layout::gemm::RowMajor>);
@@ -1258,8 +1281,10 @@ class TestCkTileGemmABQuant : public TestCkTileGemmQuantBase<Tuple, TestCkTileGe
}();
using BaseGemmPipeline = std::decay_t<decltype(base_gemm_pipeline)>;
constexpr auto K1 = CodegenGemmShape::WarpTile::at(ck_tile::number<2>{});
const ck_tile::index_t K_split =
ck_tile::integer_least_multiple(args.K, GemmConfig::K_Tile);
args.k_batch == 1 ? ck_tile::integer_least_multiple(args.K, GemmConfig::K_Tile)
: ck_tile::get_splitk_batch_k_read(args.K, args.k_batch, K1);
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
const ck_tile::TailNumber tail_num = BaseGemmPipeline::GetBlockLoopTailNum(num_loop);
@@ -1330,23 +1355,37 @@ class TestCkTileGemmABQuant : public TestCkTileGemmQuantBase<Tuple, TestCkTileGe
Base::K_Warp_Tile,
transpose_c>>>;
using Kernel = ck_tile::QuantGemmKernel<TilePartitioner,
GemmPipeline,
GemmEpilogue,
ck_tile::QuantType::ABQuantGrouped>;
// TODO: Replace with templated lambda when C++20 is available
auto LaunchKernel = [&](auto RuntimeSplitKTailTag) {
constexpr bool RuntimeSplitKTail = decltype(RuntimeSplitKTailTag)::value;
using Kernel = ck_tile::QuantGemmKernel<TilePartitioner,
GemmPipeline,
GemmEpilogue,
ck_tile::QuantType::ABQuantGrouped,
RuntimeSplitKTail>;
auto kargs = Kernel::MakeKernelArgs(args);
const dim3 grids = Kernel::GridSize(args.M, args.N, args.k_batch);
const dim3 blocks = Kernel::BlockSize();
auto kargs = Kernel::MakeKernelArgs(args);
const dim3 grids = Kernel::GridSize(args.M, args.N, args.k_batch);
const dim3 blocks = Kernel::BlockSize();
if(!Kernel::IsSupportedArgument(kargs))
if(!Kernel::IsSupportedArgument(kargs))
{
throw std::runtime_error("Arguments not supported for ABQuant kernel");
}
using k_attr_t = ck_tile::kernel_attr<eight_waves>;
ck_tile::launch_kernel(s,
ck_tile::make_kernel<GemmConfigBase::kBlockPerCu, k_attr_t>(
Kernel{}, grids, blocks, 0, kargs));
};
if(allow_runtime_splitk_tail)
{
throw std::runtime_error("Arguments not supported for ABQuant kernel");
LaunchKernel(std::true_type{});
}
else
{
LaunchKernel(std::false_type{});
}
using k_attr_t = ck_tile::kernel_attr<eight_waves>;
ck_tile::launch_kernel(s,
ck_tile::make_kernel<GemmConfigBase::kBlockPerCu, k_attr_t>(
Kernel{}, grids, blocks, 0, kargs));
};
return BaseGemmPipeline::TailHandler(Run, has_hot_loop, tail_num);
@@ -1510,9 +1549,12 @@ class TestCkTileGemmRowColQuant
using BaseGemmPipeline = ck_tile::BaseGemmPipelineAgBgCrCompV3<GemmPipelineProblem>;
const ck_tile::index_t K_split = (args.K + Base::K_Tile - 1) / Base::K_Tile * Base::K_Tile;
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
constexpr auto K1 = CodegenGemmShape::WarpTile::at(ck_tile::number<2>{});
const ck_tile::index_t K_split =
args.k_batch == 1 ? (args.K + Base::K_Tile - 1) / Base::K_Tile * Base::K_Tile
: ck_tile::get_splitk_batch_k_read(args.K, args.k_batch, K1);
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
const ck_tile::TailNumber tail_num = BaseGemmPipeline::GetBlockLoopTailNum(num_loop);
const auto Run = [&](const auto has_hot_loop_, const auto tail_number_) {
@@ -1725,9 +1767,12 @@ class TestCkTileGemmTensorQuant
using BaseGemmPipeline = ck_tile::BaseGemmPipelineAgBgCrCompV3<GemmPipelineProblem>;
const ck_tile::index_t K_split = (args.K + Base::K_Tile - 1) / Base::K_Tile * Base::K_Tile;
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
constexpr auto K1 = CodegenGemmShape::WarpTile::at(ck_tile::number<2>{});
const ck_tile::index_t K_split =
args.k_batch == 1 ? (args.K + Base::K_Tile - 1) / Base::K_Tile * Base::K_Tile
: ck_tile::get_splitk_batch_k_read(args.K, args.k_batch, K1);
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
const ck_tile::TailNumber tail_num = BaseGemmPipeline::GetBlockLoopTailNum(num_loop);
const auto Run = [&](const auto has_hot_loop_, const auto tail_number_) {

View File

@@ -23,6 +23,16 @@ if(GPU_TARGETS MATCHES "gfx94|gfx95|gfx12")
add_gtest_executable(test_ck_tile_grouped_gemm_quant_bquant_preshuffleb test_grouped_gemm_quant_bquant_preshuffleb.cpp)
target_compile_options(test_ck_tile_grouped_gemm_quant_bquant_preshuffleb PRIVATE ${EXAMPLE_GEMM_COMPILE_OPTIONS})
# Smoke test for grouped ABQuant + split-K. Verifies that
# QuantGroupedGemmKernel<..., ABQuantGrouped, ...> instantiates, that
# IsSupportedArgument behaves correctly for valid / invalid k_batch, and
# runs one minimal end-to-end correctness check (single group, k_batch=2)
# to cover the grouped-specific per-batch pointer offsetting and AQ
# group offset wiring (broader code paths are tested under
# gemm_block_scale/test_gemm_quant_abquant_splitk_*).
add_gtest_executable(test_ck_tile_grouped_gemm_quant_abquant_splitk_smoke test_grouped_gemm_quant_abquant_splitk_smoke.cpp)
target_compile_options(test_ck_tile_grouped_gemm_quant_abquant_splitk_smoke PRIVATE ${EXAMPLE_GEMM_COMPILE_OPTIONS})
# Collect all test targets for umbrella label
set(CK_TILE_GROUPED_GEMM_QUANT_TEST_TARGETS
test_ck_tile_grouped_gemm_quant_rowcol
@@ -30,6 +40,7 @@ if(GPU_TARGETS MATCHES "gfx94|gfx95|gfx12")
test_ck_tile_grouped_gemm_quant_aquant
test_ck_tile_grouped_gemm_quant_bquant
test_ck_tile_grouped_gemm_quant_bquant_preshuffleb
test_ck_tile_grouped_gemm_quant_abquant_splitk_smoke
)
# Label all ck_tile grouped_gemm_quant tests with CK_TILE_GROUPED_GEMM_QUANT_TESTS for selective execution

View File

@@ -0,0 +1,381 @@
// Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
// Minimal smoke test for QuantGroupedGemmKernel<..., ABQuantGrouped, ...>
// with split-K. The main ABQuant + split-K code paths (uniform splits,
// runtime-tail dispatch, BPreshuffle, etc.) are exercised in
// test/ck_tile/gemm_block_scale/test_gemm_quant_abquant_splitk_*.cpp using
// the non-grouped kernel; the grouped kernel reuses the same Base::RunGemm.
// What we cover here is grouped-specific:
//
// 1. Compile-time instantiation of the grouped kernel for ABQuantGrouped.
// If any inner static_assert (e.g. RowMajor BQ + ABQuant) fires this
// file won't compile.
// 2. Host-side IsSupportedArgument acceptance for valid k_batch and
// rejection for k_batch <= 0.
// 3. A single end-to-end correctness run with k_batch == 2 on a single
// group. This exercises QuantGroupedGemmKernel::Run's per-batch
// pointer offsetting (a/b/aq/bq) and the aq_group_offset wired into
// MakeAQBlockWindow -- both of which were latent bugs before this
// change because every existing grouped test launched with k_batch=1.
#include "ck_tile/host.hpp"
#include "ck_tile/ops/epilogue.hpp"
#include "ck_tile/ops/gemm.hpp"
#include "ck_tile/ops/gemm_quant.hpp"
#include <gtest/gtest.h>
namespace {
using ALayout = ck_tile::tensor_layout::gemm::RowMajor;
using BLayout = ck_tile::tensor_layout::gemm::ColumnMajor;
using CLayout = ck_tile::tensor_layout::gemm::RowMajor;
using AQLayout = ck_tile::tensor_layout::gemm::RowMajor;
using BQLayout = ck_tile::tensor_layout::gemm::ColumnMajor;
using ADataType = ck_tile::fp8_t;
using BDataType = ck_tile::fp8_t;
using AccDataType = float;
using CDataType = ck_tile::half_t;
using QDataType = float;
using ComputeDataType = ADataType;
using AQuantGroupSize = ck_tile::QuantGroupShape<ck_tile::sequence<1, 1, 128>>;
using BQuantGroupSize = ck_tile::QuantGroupShape<ck_tile::sequence<1, 128, 128>>;
constexpr ck_tile::index_t M_Tile = 128;
constexpr ck_tile::index_t N_Tile = 128;
constexpr ck_tile::index_t K_Tile = 128;
constexpr ck_tile::index_t M_Warp = 1;
constexpr ck_tile::index_t N_Warp = 4;
constexpr ck_tile::index_t K_Warp = 1;
constexpr ck_tile::index_t M_Warp_Tile = 16;
constexpr ck_tile::index_t N_Warp_Tile = 16;
#if CK_TILE_USE_WMMA
constexpr ck_tile::index_t K_Warp_Tile = 16;
#else
constexpr ck_tile::index_t K_Warp_Tile = 64;
#endif
constexpr bool kPadM = false;
constexpr bool kPadN = false;
constexpr bool kPadK = false;
constexpr bool TransposeC = true;
constexpr ck_tile::QuantType QuantMode = ck_tile::QuantType::ABQuantGrouped;
using GemmShape = ck_tile::TileGemmShape<ck_tile::sequence<M_Tile, N_Tile, K_Tile>,
ck_tile::sequence<M_Warp, N_Warp, K_Warp>,
ck_tile::sequence<M_Warp_Tile, N_Warp_Tile, K_Warp_Tile>>;
using TilePartitioner = ck_tile::GemmTile1DPartitioner<GemmShape>;
using GemmTraits = ck_tile::TileGemmQuantTraits<kPadM,
kPadN,
kPadK,
/*APreshuffleQuant=*/false,
/*BPreshuffleQuant=*/false,
/*PreshuffleB=*/false,
ALayout,
BLayout,
CLayout,
QuantMode,
AQLayout,
BQLayout,
TransposeC,
/*DoubleSmemBuffer=*/false>;
// PipelineProblem template used to drive BaseGemmPipeline / TailHandler
// dispatch. The HasHotLoop / TailNumber template parameters are fixed at
// compile time per-instantiation; TailHandler picks the right one at
// runtime based on (K, k_batch).
template <bool HasHotLoop, ck_tile::TailNumber TailNum>
using AbquantPipelineProblem =
ck_tile::GemmABQuantPipelineProblem<ADataType,
QDataType, // AQDataType
BDataType,
QDataType, // BQDataType
AccDataType,
GemmShape,
GemmTraits,
AQuantGroupSize,
BQuantGroupSize,
TransposeC,
ComputeDataType,
ck_tile::GemmPipelineScheduler::Intrawave,
HasHotLoop,
TailNum>;
using BasePipelineProblem = ck_tile::GemmPipelineProblemBase<ADataType,
BDataType,
AccDataType,
GemmShape,
GemmTraits,
ComputeDataType>;
using BaseGemmPipeline = ck_tile::BaseGemmPipelineAgBgCrCompV3<BasePipelineProblem>;
// Compile-time instantiation check: a representative set of types must
// build cleanly (this is the minimal "smoke" portion of the test).
using SmokePipeline = ck_tile::ABQuantGemmPipelineAgBgCrCompV3<
AbquantPipelineProblem<true, ck_tile::TailNumber::Full>>;
using SmokeEpilogue =
ck_tile::CShuffleEpilogue<ck_tile::CShuffleEpilogueProblem<ADataType,
BDataType,
ck_tile::tuple<>,
AccDataType,
CDataType,
ck_tile::tuple<>,
CLayout,
ck_tile::element_wise::PassThrough,
TilePartitioner::MPerBlock,
TilePartitioner::NPerBlock,
M_Warp,
N_Warp,
M_Warp_Tile,
N_Warp_Tile,
K_Warp_Tile,
TransposeC>>;
using SmokeGroupedKernel =
ck_tile::QuantGroupedGemmKernel<TilePartitioner, SmokePipeline, SmokeEpilogue, QuantMode>;
using SmokeBaseKernel = typename SmokeGroupedKernel::Base;
static_assert(sizeof(SmokeGroupedKernel) > 0, "QuantGroupedGemmKernel must instantiate");
static_assert(sizeof(SmokeBaseKernel) > 0, "QuantGemmKernel base must instantiate");
ck_tile::QuantGemmKernelArgs MakeKargsForValidation(ck_tile::index_t k_batch)
{
constexpr ck_tile::index_t M = 128;
constexpr ck_tile::index_t N = 128;
constexpr ck_tile::index_t K = 1024;
ck_tile::QuantGemmKernelArgs kargs{};
kargs.a_ptr = nullptr;
kargs.b_ptr = nullptr;
kargs.aq_ptr = nullptr;
kargs.bq_ptr = nullptr;
kargs.c_ptr = nullptr;
kargs.M = M;
kargs.N = N;
kargs.K = K;
kargs.QK_A = ck_tile::integer_divide_ceil(K, AQuantGroupSize::kK);
kargs.QK_B = ck_tile::integer_divide_ceil(K, BQuantGroupSize::kK);
kargs.stride_A = K;
kargs.stride_B = K;
kargs.stride_C = N;
kargs.stride_AQ = kargs.QK_A;
kargs.stride_BQ = N;
kargs.k_batch = k_batch;
return kargs;
}
// End-to-end runner. Builds host tensors, fills them with deterministic
// random data, runs the grouped kernel for a single group, and validates
// against the host reference. Kept intentionally narrow: one group, fixed
// layouts/types, no preshuffle. The wider parameter space is covered by
// the non-grouped ABQuant tests.
bool RunSingleGroupABQuantSplitK(ck_tile::index_t M,
ck_tile::index_t N,
ck_tile::index_t K,
ck_tile::index_t k_batch)
{
auto is_row_major = [](auto layout) {
return ck_tile::bool_constant<std::is_same_v<ck_tile::remove_cvref_t<decltype(layout)>,
ck_tile::tensor_layout::gemm::RowMajor>>{};
};
const ck_tile::index_t stride_A = ck_tile::get_default_stride(M, K, 0, is_row_major(ALayout{}));
const ck_tile::index_t stride_B = ck_tile::get_default_stride(K, N, 0, is_row_major(BLayout{}));
const ck_tile::index_t stride_C = ck_tile::get_default_stride(M, N, 0, is_row_major(CLayout{}));
const ck_tile::index_t AQK = ck_tile::integer_divide_ceil(K, AQuantGroupSize::kK);
const ck_tile::index_t BQN = ck_tile::integer_divide_ceil(N, BQuantGroupSize::kN);
const ck_tile::index_t BQK = ck_tile::integer_divide_ceil(K, BQuantGroupSize::kK);
const ck_tile::index_t stride_AQ =
ck_tile::get_default_stride(M, AQK, 0, is_row_major(AQLayout{}));
const ck_tile::index_t stride_BQ =
ck_tile::get_default_stride(BQK, BQN, 0, is_row_major(BQLayout{}));
ck_tile::HostTensor<ADataType> a_m_k(
ck_tile::host_tensor_descriptor(M, K, stride_A, is_row_major(ALayout{})));
ck_tile::HostTensor<BDataType> b_k_n(
ck_tile::host_tensor_descriptor(K, N, stride_B, is_row_major(BLayout{})));
ck_tile::HostTensor<QDataType> aq_m_aqk(
ck_tile::host_tensor_descriptor(M, AQK, stride_AQ, is_row_major(AQLayout{})));
ck_tile::HostTensor<QDataType> bq_bqk_bqn(
ck_tile::host_tensor_descriptor(BQK, BQN, stride_BQ, is_row_major(BQLayout{})));
ck_tile::FillUniformDistribution<ADataType>{-2.0f, 3.0f}(a_m_k);
ck_tile::FillUniformDistribution<BDataType>{-5.0f, 5.0f}(b_k_n);
ck_tile::FillUniformDistribution<QDataType>{-2.0f, 2.0f}(aq_m_aqk);
ck_tile::FillUniformDistribution<QDataType>{-2.0f, 2.0f}(bq_bqk_bqn);
ck_tile::DeviceMem a_m_k_dev_buf(a_m_k.get_element_space_size() * sizeof(ADataType));
ck_tile::DeviceMem b_k_n_dev_buf(b_k_n.get_element_space_size() * sizeof(BDataType));
ck_tile::DeviceMem aq_m_aqk_dev_buf(aq_m_aqk.get_element_space_size() * sizeof(QDataType));
ck_tile::DeviceMem bq_bqk_bqn_dev_buf(bq_bqk_bqn.get_element_space_size() * sizeof(QDataType));
ck_tile::DeviceMem c_m_n_dev_buf(M * N * sizeof(CDataType));
a_m_k_dev_buf.ToDevice(a_m_k.data());
b_k_n_dev_buf.ToDevice(b_k_n.data());
aq_m_aqk_dev_buf.ToDevice(aq_m_aqk.data());
bq_bqk_bqn_dev_buf.ToDevice(bq_bqk_bqn.data());
if(k_batch > 1)
{
c_m_n_dev_buf.SetZero();
}
std::vector<ck_tile::QuantGroupedGemmHostArgs> gemm_descs;
gemm_descs.emplace_back(a_m_k_dev_buf.GetDeviceBuffer(),
b_k_n_dev_buf.GetDeviceBuffer(),
c_m_n_dev_buf.GetDeviceBuffer(),
aq_m_aqk_dev_buf.GetDeviceBuffer(),
bq_bqk_bqn_dev_buf.GetDeviceBuffer(),
k_batch,
M,
N,
K,
AQK,
BQK,
stride_A,
stride_B,
stride_C,
stride_AQ,
stride_BQ);
// Workspace holds the per-group QuantGemmTransKernelArg vector, copied
// to device before the launch.
ck_tile::DeviceMem gemm_workspace(gemm_descs.size() * sizeof(ck_tile::QuantGemmTransKernelArg));
void* kargs_ptr = gemm_workspace.GetDeviceBuffer();
// Drive TailHandler dispatch with split-K-aware K_split (mirrors
// run_quant_gemm_impl in the non-grouped fixture).
constexpr auto K1 = GemmShape::WarpTile::at(ck_tile::number<2>{});
const ck_tile::index_t K_split = (k_batch == 1)
? ck_tile::integer_least_multiple(K, K_Tile)
: ck_tile::get_splitk_batch_k_read(K, k_batch, K1);
const ck_tile::index_t num_loop = TilePartitioner::GetLoopNum(K_split);
const bool has_hot_loop = BaseGemmPipeline::BlockHasHotloop(num_loop);
const ck_tile::TailNumber tail_num = BaseGemmPipeline::GetBlockLoopTailNum(num_loop);
ck_tile::stream_config s{};
auto Run = [&](const auto has_hot_loop_, const auto tail_number_) {
constexpr bool has_hot_loop_v = has_hot_loop_.value;
constexpr auto tail_number_v = tail_number_.value;
using PipelineProblem = AbquantPipelineProblem<has_hot_loop_v, tail_number_v>;
using GemmPipeline = ck_tile::ABQuantGemmPipelineAgBgCrCompV3<PipelineProblem>;
using GemmEpilogue = ck_tile::CShuffleEpilogue<
ck_tile::CShuffleEpilogueProblem<ADataType,
BDataType,
ck_tile::tuple<>,
AccDataType,
CDataType,
ck_tile::tuple<>,
CLayout,
ck_tile::element_wise::PassThrough,
TilePartitioner::MPerBlock,
TilePartitioner::NPerBlock,
M_Warp,
N_Warp,
M_Warp_Tile,
N_Warp_Tile,
K_Warp_Tile,
TransposeC>>;
using Kernel =
ck_tile::QuantGroupedGemmKernel<TilePartitioner, GemmPipeline, GemmEpilogue, QuantMode>;
auto kargs = Kernel::MakeKargs(gemm_descs);
if(!Kernel::IsSupportedArgument(kargs))
{
throw std::runtime_error("Grouped ABQuant SplitK args not supported");
}
const dim3 grids = Kernel::GridSize(gemm_descs);
const dim3 blocks = Kernel::BlockSize();
HIP_CHECK_ERROR(hipMemcpyWithStream(kargs_ptr,
kargs.data(),
kargs.size() * sizeof(ck_tile::QuantGemmTransKernelArg),
hipMemcpyHostToDevice,
s.stream_id_));
ck_tile::launch_kernel(
s,
ck_tile::make_kernel<1>(Kernel{},
grids,
blocks,
0,
ck_tile::cast_pointer_to_constant_address_space(kargs_ptr),
static_cast<ck_tile::index_t>(gemm_descs.size())));
};
BaseGemmPipeline::TailHandler(Run, has_hot_loop, tail_num);
ck_tile::HostTensor<CDataType> c_m_n_host_ref(
ck_tile::host_tensor_descriptor(M, N, stride_C, is_row_major(CLayout{})));
c_m_n_host_ref.SetZero();
ck_tile::reference_gemm_abquant<ADataType,
QDataType,
BDataType,
QDataType,
AccDataType,
CDataType,
AQuantGroupSize,
BQuantGroupSize>(
a_m_k, aq_m_aqk, b_k_n, bq_bqk_bqn, c_m_n_host_ref);
ck_tile::HostTensor<CDataType> c_m_n_dev_result(
ck_tile::host_tensor_descriptor(M, N, stride_C, is_row_major(CLayout{})));
c_m_n_dev_buf.FromDevice(c_m_n_dev_result.mData.data());
const float max_accumulated_value =
*std::max_element(c_m_n_host_ref.mData.begin(), c_m_n_host_ref.mData.end());
const auto rtol =
std::max(ck_tile::get_relative_threshold<ADataType, CDataType, AccDataType>(
ck_tile::integer_divide_ceil(K, k_batch)),
ck_tile::get_relative_threshold<CDataType, CDataType, CDataType>(k_batch));
const auto atol =
std::max(ck_tile::get_absolute_threshold<ADataType, CDataType, AccDataType>(
max_accumulated_value / k_batch, ck_tile::integer_divide_ceil(K, k_batch)),
ck_tile::get_absolute_threshold<CDataType, CDataType, CDataType>(
max_accumulated_value, k_batch));
return ck_tile::check_err(
c_m_n_dev_result, c_m_n_host_ref, "Grouped ABQuant SplitK mismatch", rtol, atol);
}
} // namespace
TEST(GroupedABQuantSplitKSmoke, AcceptsKBatch1)
{
EXPECT_TRUE(SmokeBaseKernel::IsSupportedArgument(MakeKargsForValidation(/*k_batch=*/1)));
}
TEST(GroupedABQuantSplitKSmoke, AcceptsKBatch2)
{
EXPECT_TRUE(SmokeBaseKernel::IsSupportedArgument(MakeKargsForValidation(/*k_batch=*/2)));
}
TEST(GroupedABQuantSplitKSmoke, RejectsKBatchZero)
{
EXPECT_FALSE(SmokeBaseKernel::IsSupportedArgument(MakeKargsForValidation(/*k_batch=*/0)));
}
TEST(GroupedABQuantSplitKSmoke, RejectsKBatchNegative)
{
EXPECT_FALSE(SmokeBaseKernel::IsSupportedArgument(MakeKargsForValidation(/*k_batch=*/-1)));
}
// End-to-end correctness for the grouped ABQuant + split-K path on a
// single group with k_batch=2. Catches regressions in:
// - QuantGroupedGemmKernel::Run per-batch a/b/aq/bq pointer offsetting,
// - aq_group_offset wiring through MakeAQBlockWindow.
// Both were latent (existing grouped tests only used k_batch=1).
TEST(GroupedABQuantSplitKSmoke, EndToEnd_SingleGroup_KBatch2)
{
EXPECT_TRUE(RunSingleGroupABQuantSplitK(/*M=*/128,
/*N=*/128,
/*K=*/512,
/*k_batch=*/2));
}