Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

1994 lines
82 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*******************************************************************************
*
* MIT License
*
* Copyright 2025-2026 AMD ROCm(TM) Software
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*******************************************************************************/
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "common.hpp"
using Catch::Approx;
namespace {
void set_problem_dtypes(origami::problem_t& problem, origami::data_type_t mi_dtype) {
problem.mi_dtype = mi_dtype;
if (mi_dtype == origami::data_type_t::XFloat32) {
problem.a_dtype = origami::data_type_t::Float;
problem.b_dtype = origami::data_type_t::Float;
problem.c_dtype = origami::data_type_t::Float;
problem.d_dtype = origami::data_type_t::Float;
return;
}
problem.a_dtype = mi_dtype;
problem.b_dtype = mi_dtype;
problem.c_dtype = mi_dtype;
problem.d_dtype = mi_dtype;
}
double lookup_cms_main_loop_efficiency(origami::data_type_t mi_dtype,
origami::transpose_t trans_a,
origami::transpose_t trans_b,
size_t mt_m,
size_t mt_n,
size_t mt_k) {
auto& db = origami::heuristics_database_t::get_instance();
auto hardware = make_hardware(950);
auto problem = make_problem(1024, 1024, 1024, trans_a, trans_b);
auto config = make_config(mt_m, mt_n, mt_k, 16, 16, 16, true);
set_problem_dtypes(problem, mi_dtype);
return db.lookup(problem, hardware, config).main_loop_efficiency;
}
} // namespace
// Test functions for gemm.hpp/cpp
TEST_CASE("GEMM: compute_num_matrix_instructions", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - 128x128x64 with 16x16x16") {
auto hardware = make_hardware(gpu_arch);
origami::dim3_t mt{128, 128, 64};
origami::dim3_t mi{16, 16, 16};
auto num_instructions = origami::gemm::compute_number_matrix_instructions(mt, mi);
REQUIRE(num_instructions == 256); // 8 * 8 * 4
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - 16x16x64 with 16x16x32") {
auto hardware = make_hardware(gpu_arch);
origami::dim3_t mt{16, 16, 64};
origami::dim3_t mi{16, 16, 32};
auto num_instructions = origami::gemm::compute_number_matrix_instructions(mt, mi);
REQUIRE(num_instructions == 2); // 1 * 1 * 2
}
}
}
TEST_CASE("GEMM: compute_mt_compute_latency", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - transA=T transB=N") {
auto hardware = make_hardware(gpu_arch);
auto problem =
make_problem(4096, 4096, 1024, origami::transpose_t::T, origami::transpose_t::N);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
auto latency = origami::gemm::compute_mt_compute_latency(problem, hardware, config);
REQUIRE(latency == 4096);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - transA=N transB=T") {
auto hardware = make_hardware(gpu_arch);
auto problem =
make_problem(4096, 4096, 1024, origami::transpose_t::N, origami::transpose_t::T);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
auto latency = origami::gemm::compute_mt_compute_latency(problem, hardware, config);
REQUIRE(latency == 4096);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - transA=N transB=N") {
auto hardware = make_hardware(gpu_arch);
auto problem =
make_problem(4096, 4096, 1024, origami::transpose_t::N, origami::transpose_t::N);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
auto latency = origami::gemm::compute_mt_compute_latency(problem, hardware, config);
REQUIRE(latency == 4096);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - transA=T transB=T") {
auto hardware = make_hardware(gpu_arch);
auto problem =
make_problem(4096, 4096, 1024, origami::transpose_t::T, origami::transpose_t::T);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
auto latency = origami::gemm::compute_mt_compute_latency(problem, hardware, config);
REQUIRE(latency == 4096);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - different MT_K") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(128, 128, 32, 32, 32, 8, false, 1);
auto latency = origami::gemm::compute_mt_compute_latency(problem, hardware, config);
REQUIRE(latency == 2048);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - larger tile") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(224, 224, 64, 32, 32, 8, false, 1);
auto latency = origami::gemm::compute_mt_compute_latency(problem, hardware, config);
REQUIRE(latency > 12543);
}
}
}
TEST_CASE("GEMM: compute_memory_latency", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - verify smaller tiles have lower latency") {
auto hardware = make_hardware(gpu_arch);
auto problem =
make_problem(4096, 4096, 1024, origami::transpose_t::T, origami::transpose_t::N, 1);
auto config_small = make_config(128, 128, 64, 32, 32, 8, false, 8);
auto config_large = make_config(256, 256, 128, 32, 32, 8, false, 8);
auto mem_latency_small = origami::gemm::compute_memory_latency(
problem, hardware, config_small, origami::gemm::context_t(problem, hardware, config_small));
auto mem_latency_large = origami::gemm::compute_memory_latency(
problem, hardware, config_large, origami::gemm::context_t(problem, hardware, config_large));
REQUIRE(mem_latency_small < mem_latency_large);
}
}
}
TEST_CASE("GEMM: compute_tile_latency", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - verify larger tiles have higher latency") {
auto hardware = make_hardware(gpu_arch);
auto problem =
make_problem(4096, 4096, 1024, origami::transpose_t::T, origami::transpose_t::N, 2);
auto config_small = make_config(128, 128, 64, 32, 32, 8, false, 6);
auto config_large = make_config(256, 256, 128, 32, 32, 8, false, 6);
auto tile_latency_small = origami::gemm::compute_tile_latency(
problem, hardware, config_small, origami::gemm::context_t(problem, hardware, config_small));
auto tile_latency_large = origami::gemm::compute_tile_latency(
problem, hardware, config_large, origami::gemm::context_t(problem, hardware, config_large));
REQUIRE(tile_latency_large > tile_latency_small);
}
}
}
TEST_CASE("GEMM: compute_timestep_latency", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - timestep latency equals tile latency") {
auto hardware = make_hardware(gpu_arch);
auto problem =
make_problem(4096, 4096, 1024, origami::transpose_t::T, origami::transpose_t::N, 2);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 8);
auto tile_latency = origami::gemm::compute_tile_latency(
problem, hardware, config, origami::gemm::context_t(problem, hardware, config));
auto timestep_latency = origami::gemm::compute_timestep_latency(
problem, hardware, config, origami::gemm::context_t(problem, hardware, config));
REQUIRE(timestep_latency == Approx(tile_latency));
}
}
}
TEST_CASE("GEMM: compute_total_latency", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - smaller tiles have lower total latency") {
auto hardware = make_hardware(gpu_arch);
auto problem_small =
make_problem(4096, 4096, 1024, origami::transpose_t::T, origami::transpose_t::N, 2);
auto problem_large =
make_problem(8192, 8192, 2048, origami::transpose_t::T, origami::transpose_t::N, 2);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
auto latency_small =
origami::gemm::compute_total_latency(problem_small, hardware, config);
auto latency_large =
origami::gemm::compute_total_latency(problem_large, hardware, config);
REQUIRE(latency_small < latency_large);
}
}
}
TEST_CASE("GEMM: check_lds_capacity", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - 256x256x64 tile fits in LDS") {
auto hardware = make_hardware(gpu_arch);
origami::dim3_t mt{256, 256, 64};
auto fits = origami::gemm::check_lds_capacity(
hardware, mt, origami::data_type_t::BFloat16, origami::data_type_t::BFloat16);
REQUIRE(fits == true);
}
}
}
TEST_CASE("GEMM: estimate_l2_hit", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - L2 hit rate in valid range") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(256, 256, 64, 32, 32, 8, false, 1);
for (int wgm = 1; wgm < 1025; wgm++) {
config.workgroup_mapping = wgm;
auto l2_hit = origami::gemm::estimate_l2_hit(
problem, hardware, config, origami::gemm::context_t(problem, hardware, config));
REQUIRE(l2_hit > 0.0);
REQUIRE(l2_hit < 1.0);
}
}
}
}
TEST_CASE("GEMM: estimate_mall_hit", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - Mall hit rate is positive") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(256, 256, 64, 32, 32, 8, false, 1);
for (int wgm = 1; wgm < 1025; wgm++) {
config.workgroup_mapping = wgm;
auto mall_hit = origami::gemm::estimate_mall_hit(
problem, hardware, config, origami::gemm::context_t(problem, hardware, config));
REQUIRE(mall_hit > 0.0);
}
}
}
}
// Unit tests
TEST_CASE("GEMM: calculate_work_utilization unit test", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - calculate_work_utilization unit test") {
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(256, 256, 64, 32, 32, 8, false, 1);
// Test 1: Test with perfect tile alignment (should return 1.0)
auto result = origami::gemm::calculate_work_utilization(problem, config);
REQUIRE(result == 1.0);
// Test 2: Test with non-aligned dimensions
auto problem_non_aligned = make_problem(4351, 3839, 959);
auto result_non_aligned = origami::gemm::calculate_work_utilization(problem_non_aligned, config);
REQUIRE(result_non_aligned == Approx(0.998).epsilon(1e-3));
// Test 3: Test with zero dimensions (should return 1.0)
auto problem_zero_dimensions = make_problem(0, 3839, 959);
auto result_zero_dimensions =
origami::gemm::calculate_work_utilization(problem_zero_dimensions, config);
REQUIRE(result_zero_dimensions == 1.0);
// Test 4: Test with very small problems
auto problem_very_small = make_problem(10, 20, 15);
auto result_very_small = origami::gemm::calculate_work_utilization(problem_very_small, config);
REQUIRE(result_very_small == Approx(0.0007152).epsilon(1e-4));
// Test 5: Test with very large problems
auto problem_very_large = make_problem(409601, 409601, 4095);
auto result_very_large = origami::gemm::calculate_work_utilization(problem_very_large, config);
REQUIRE(result_very_large == Approx(0.998).epsilon(1e-3));
// Test 6: Test with skinny matrices
auto problem_skinny = make_problem(128, 81920, 1024); // Small M, Big N
auto result_skinny = origami::gemm::calculate_work_utilization(problem_skinny, config);
REQUIRE(result_skinny == 0.5);
problem_skinny = make_problem(81920, 128, 1024); // Small N, Big M
result_skinny = origami::gemm::calculate_work_utilization(problem_skinny, config);
REQUIRE(result_skinny == 0.5);
}
}
}
TEST_CASE("GEMM: calculate_output_utilization unit test", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - calculate_output_utilization unit test") {
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(256, 256, 64, 32, 32, 8, false, 1);
// Test 1: Test with perfect tile alignment (should return 1.0)
auto result = origami::gemm::calculate_output_utilization(problem, config, 1UL);
REQUIRE(result == 1.0);
// Test 2: Test with non-aligned dimensions
auto problem_non_aligned = make_problem(4351, 3839, 959);
auto result_non_aligned =
origami::gemm::calculate_output_utilization(problem_non_aligned, config, 1UL);
REQUIRE(result_non_aligned == Approx(0.999).epsilon(1e-3));
// Test 3: Test with vector_elems > 1
auto result_with_vector_elems = origami::gemm::calculate_output_utilization(problem, config, 23UL);
REQUIRE(result_with_vector_elems == Approx(1.010).epsilon(1e-3));
// Test 4: Test with zero dimensions (should return 1.0)
auto problem_zero_dimensions = make_problem(0, 3839, 959);
auto result_zero_dimensions =
origami::gemm::calculate_output_utilization(problem_zero_dimensions, config, 1UL);
REQUIRE(result_zero_dimensions == 1.0);
// Test 5: Test with different problem sizes
auto problem_very_small = make_problem(10, 20, 15);
auto result_very_small =
origami::gemm::calculate_output_utilization(problem_very_small, config, 1UL);
REQUIRE(result_very_small == Approx(0.003051).epsilon(1e-3));
auto problem_very_large = make_problem(409601, 409601, 4095);
auto result_very_large =
origami::gemm::calculate_output_utilization(problem_very_large, config, 1UL);
REQUIRE(result_very_large == Approx(0.998).epsilon(1e-3));
auto problem_skinny = make_problem(64, 81920, 1024); // Small M, Big N
auto result_skinny = origami::gemm::calculate_output_utilization(problem_skinny, config, 1UL);
REQUIRE(result_skinny == 0.25);
problem_skinny = make_problem(81920, 64, 1024); // Small N, Big M
result_skinny = origami::gemm::calculate_output_utilization(problem_skinny, config, 1UL);
REQUIRE(result_skinny == 0.25);
}
}
}
TEST_CASE("GEMM: compute_launch_parameters unit test", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - compute_launch_parameters unit test") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(256, 256, 64, 32, 32, 8, false, 1);
// Returns: (reduction_t, num_wgs, num_active_cus, num_timesteps, split_factor)
auto [reduction, num_wgs, num_active_cus, num_timesteps, split_factor] =
origami::gemm::compute_launch_parameters(
problem, hardware, config, origami::grid_selection_t::k_split_aware);
REQUIRE(num_wgs > 0);
REQUIRE(num_active_cus > 0);
REQUIRE(num_timesteps >= 1);
REQUIRE(split_factor >= 1);
// Test with different grid selection algorithms
auto result_analytical = origami::gemm::compute_launch_parameters(
problem, hardware, config, origami::grid_selection_t::analytical);
REQUIRE(std::get<1>(result_analytical) > 0);
// The CU budget now comes from problem.num_cus: capping it below the
// physical CU count must bound the number of active CUs accordingly.
auto capped_problem = problem;
capped_problem.num_cus = 150;
auto result_max_cus = origami::gemm::compute_launch_parameters(
capped_problem, hardware, config, origami::grid_selection_t::k_split_aware);
REQUIRE(std::get<2>(result_max_cus) <= 150);
}
}
}
TEST_CASE("GEMM: count_unique_range unit test", "[gemm]") {
// Test 1: Single tile — range covers exactly one WG
{
origami::dim4_t grid{1, 4, 8, 1}; // k=1, m=4, n=8, b=1
auto u = origami::gemm::count_unique_range(grid, 8, 0, 1);
REQUIRE(u.m == 1);
REQUIRE(u.n == 1);
REQUIRE(u.k == 1);
REQUIRE(u.b == 1);
}
// Test 2: Full grid coverage — all tiles
{
origami::dim4_t grid{1, 4, 8, 1};
auto u = origami::gemm::count_unique_range(grid, 8, 0, 32);
REQUIRE(u.m == 4);
REQUIRE(u.n == 8);
REQUIRE(u.k == 1);
REQUIRE(u.b == 1);
}
// Test 3: Within one slab (wgm=4), partial M
{
origami::dim4_t grid{1, 8, 16, 1}; // k=1, m=8, n=16, b=1
auto u = origami::gemm::count_unique_range(grid, 4, 0, 3);
REQUIRE(u.m == 1);
REQUIRE(u.n == 3);
REQUIRE(u.k == 1);
}
// Test 4: Within one slab, spans multiple rows
{
origami::dim4_t grid{1, 8, 16, 1};
// wgm=4: slab has 8*4=32 tiles. IDs 0..7 = row0 cols 0-3, row1 cols 0-3
auto u = origami::gemm::count_unique_range(grid, 4, 0, 8);
REQUIRE(u.m == 2);
REQUIRE(u.n == 4); // full slab width
}
// Test 5: Across two slabs
{
origami::dim4_t grid{1, 4, 8, 1};
// wgm=4: slab0 has 4*4=16 tiles (cols 0-3), slab1 has 16 tiles (cols 4-7)
auto u = origami::gemm::count_unique_range(grid, 4, 14, 4);
REQUIRE(u.m >= 1);
REQUIRE(u.n >= 2); // spans across slab boundary
}
// Test 6: K-split dimension
{
origami::dim4_t grid{4, 2, 2, 1}; // k=4 splits, m=2, n=2, b=1
// First 4 IDs = same MN tile, different K splits
auto u = origami::gemm::count_unique_range(grid, 2, 0, 4);
REQUIRE(u.m == 1);
REQUIRE(u.n == 1);
REQUIRE(u.k == 4);
}
// Test 7: K-split spanning multiple MN tiles -> all K covered
{
origami::dim4_t grid{4, 2, 2, 1};
auto u = origami::gemm::count_unique_range(grid, 2, 0, 5);
REQUIRE(u.k == 4); // crossed MN boundary, all K touched
}
// Test 8: Batch dimension
{
origami::dim4_t grid{1, 4, 4, 3}; // k=1, m=4, n=4, b=3
// 16 tiles per batch, 48 total. Range covering 2 batches:
auto u = origami::gemm::count_unique_range(grid, 4, 0, 32);
REQUIRE(u.b == 2);
REQUIRE(u.m == 4); // multiple batches -> full M
REQUIRE(u.n == 4); // multiple batches -> full N
}
// Test 9: wgm=1 (column-first, each slab is 1 column)
{
origami::dim4_t grid{1, 4, 8, 1};
auto u = origami::gemm::count_unique_range(grid, 1, 0, 4);
REQUIRE(u.m == 4); // 4 rows in one column
REQUIRE(u.n == 1); // single column slab
}
}
TEST_CASE("GEMM: wgm_to_grid unit test", "[gemm]") {
// Test 1: Simple 2x4 grid, wgm=4 (single slab covers all N)
{
origami::dim4_t grid{1, 2, 4, 1};
origami::workgroup_mapping_t wgm{0, 0, 4};
// slab_width=4, tiles_per_slab=2*4=8. All 8 tiles in one slab.
// offset 0: m=0,n=0 offset 1: m=0,n=1 offset 2: m=0,n=2 offset 3: m=0,n=3
// offset 4: m=1,n=0 offset 5: m=1,n=1 offset 6: m=1,n=2 offset 7: m=1,n=3
auto t0 = origami::gemm::wgm_to_grid(grid, wgm, 0);
REQUIRE(t0.m == 0);
REQUIRE(t0.n == 0);
REQUIRE(t0.k == 0);
REQUIRE(t0.b == 0);
auto t3 = origami::gemm::wgm_to_grid(grid, wgm, 3);
REQUIRE(t3.m == 0);
REQUIRE(t3.n == 3);
auto t4 = origami::gemm::wgm_to_grid(grid, wgm, 4);
REQUIRE(t4.m == 1);
REQUIRE(t4.n == 0);
auto t7 = origami::gemm::wgm_to_grid(grid, wgm, 7);
REQUIRE(t7.m == 1);
REQUIRE(t7.n == 3);
}
// Test 2: Two slabs — 4x6 grid, wgm=3
{
origami::dim4_t grid{1, 4, 6, 1};
origami::workgroup_mapping_t wgm{0, 0, 3};
// slab0 (cols 0-2): 4*3=12 tiles, slab1 (cols 3-5): 12 tiles
auto t0 = origami::gemm::wgm_to_grid(grid, wgm, 0);
REQUIRE(t0.m == 0);
REQUIRE(t0.n == 0);
auto t2 = origami::gemm::wgm_to_grid(grid, wgm, 2);
REQUIRE(t2.m == 0);
REQUIRE(t2.n == 2);
auto t3 = origami::gemm::wgm_to_grid(grid, wgm, 3);
REQUIRE(t3.m == 1);
REQUIRE(t3.n == 0);
auto t11 = origami::gemm::wgm_to_grid(grid, wgm, 11);
REQUIRE(t11.m == 3);
REQUIRE(t11.n == 2);
// slab1 starts at id=12
auto t12 = origami::gemm::wgm_to_grid(grid, wgm, 12);
REQUIRE(t12.m == 0);
REQUIRE(t12.n == 3);
auto t23 = origami::gemm::wgm_to_grid(grid, wgm, 23);
REQUIRE(t23.m == 3);
REQUIRE(t23.n == 5);
}
// Test 3: K-splits — each MN tile has k splits
{
origami::dim4_t grid{4, 2, 3, 1}; // k=4, m=2, n=3, b=1
origami::workgroup_mapping_t wgm{0, 0, 3};
// First MN tile (m=0,n=0) has ids 0..3 (k=0..3)
auto t0 = origami::gemm::wgm_to_grid(grid, wgm, 0);
REQUIRE(t0.m == 0);
REQUIRE(t0.n == 0);
REQUIRE(t0.k == 0);
auto t3 = origami::gemm::wgm_to_grid(grid, wgm, 3);
REQUIRE(t3.m == 0);
REQUIRE(t3.n == 0);
REQUIRE(t3.k == 3);
// Second MN tile (m=0,n=1) starts at id=4
auto t4 = origami::gemm::wgm_to_grid(grid, wgm, 4);
REQUIRE(t4.m == 0);
REQUIRE(t4.n == 1);
REQUIRE(t4.k == 0);
}
// Test 4: Batch dimension
{
origami::dim4_t grid{1, 2, 2, 3}; // k=1, m=2, n=2, b=3
origami::workgroup_mapping_t wgm{0, 0, 2};
// 4 tiles per batch. batch 0: ids 0-3, batch 1: ids 4-7, batch 2: ids 8-11
auto t0 = origami::gemm::wgm_to_grid(grid, wgm, 0);
REQUIRE(t0.b == 0);
REQUIRE(t0.m == 0);
REQUIRE(t0.n == 0);
auto t4 = origami::gemm::wgm_to_grid(grid, wgm, 4);
REQUIRE(t4.b == 1);
REQUIRE(t4.m == 0);
REQUIRE(t4.n == 0);
auto t11 = origami::gemm::wgm_to_grid(grid, wgm, 11);
REQUIRE(t11.b == 2);
REQUIRE(t11.m == 1);
REQUIRE(t11.n == 1);
}
// Test 5: WGMXCC interleaving
{
origami::dim4_t grid{1, 4, 4, 1}; // 16 tiles total
origami::workgroup_mapping_t wgm_xcc{0, 8, 4}; // wgmxcc=8
origami::workgroup_mapping_t wgm_no{0, 0, 4}; // no xcc
// With WGMXCC, consecutive IDs should map to different XCD groups.
// ID 0 and ID 1 should land in different XCD regions.
auto t0_xcc = origami::gemm::wgm_to_grid(grid, wgm_xcc, 0);
auto t1_xcc = origami::gemm::wgm_to_grid(grid, wgm_xcc, 1);
auto t0_no = origami::gemm::wgm_to_grid(grid, wgm_no, 0);
// Without WGMXCC, id 0 and 1 are adjacent; with WGMXCC they're spread apart
REQUIRE(t0_xcc.m == t0_no.m);
REQUIRE(t0_xcc.n == t0_no.n);
// id=1 with xcc should NOT be the same as id=1 without xcc (it gets remapped)
auto t1_no = origami::gemm::wgm_to_grid(grid, wgm_no, 1);
bool same = (t1_xcc.m == t1_no.m && t1_xcc.n == t1_no.n);
REQUIRE_FALSE(same);
}
// Test 6: wgm=1 — each slab is one column, M varies fastest
{
origami::dim4_t grid{1, 4, 3, 1};
origami::workgroup_mapping_t wgm{0, 0, 1};
// slab_width=1, tiles_per_slab=4. Col 0: ids 0-3, col 1: ids 4-7, col 2: ids 8-11
auto t0 = origami::gemm::wgm_to_grid(grid, wgm, 0);
REQUIRE(t0.m == 0);
REQUIRE(t0.n == 0);
auto t3 = origami::gemm::wgm_to_grid(grid, wgm, 3);
REQUIRE(t3.m == 3);
REQUIRE(t3.n == 0);
auto t4 = origami::gemm::wgm_to_grid(grid, wgm, 4);
REQUIRE(t4.m == 0);
REQUIRE(t4.n == 1);
}
// Test 7: Remainder slab — grid.n not divisible by wgm
{
origami::dim4_t grid{1, 3, 5, 1}; // 5 cols, wgm=2 -> 2 full slabs + remainder of 1
origami::workgroup_mapping_t wgm{0, 0, 2};
// slab0: cols 0,1 (6 tiles), slab1: cols 2,3 (6 tiles), remainder: col 4 (3 tiles)
auto t12 = origami::gemm::wgm_to_grid(grid, wgm, 12);
REQUIRE(t12.m == 0);
REQUIRE(t12.n == 4);
auto t14 = origami::gemm::wgm_to_grid(grid, wgm, 14);
REQUIRE(t14.m == 2);
REQUIRE(t14.n == 4);
}
// Test 8: Brute-force 'bijection' test — every id maps to a unique (m,n,k,b) and back
{
origami::dim4_t grid{2, 3, 4, 2};
origami::workgroup_mapping_t wgm{0, 0, 2};
size_t total = grid.total();
std::set<std::tuple<size_t, size_t, size_t, size_t>> seen;
for (size_t id = 0; id < total; ++id) {
auto t = origami::gemm::wgm_to_grid(grid, wgm, id);
REQUIRE(t.m < grid.m);
REQUIRE(t.n < grid.n);
REQUIRE(t.k < grid.k);
REQUIRE(t.b < grid.b);
auto key = std::make_tuple(t.m, t.n, t.k, t.b);
REQUIRE(seen.count(key) == 0);
seen.insert(key);
}
REQUIRE(seen.size() == total);
}
}
TEST_CASE("GEMM: compute_mall_tiles unit test", "[gemm]") {
// Test 1: Basic case — all CUs fit in one slab
{
auto [m, n] = origami::gemm::compute_mall_tiles(16, 32, 256, 6);
REQUIRE(m >= 1);
REQUIRE(m <= 16);
REQUIRE(n >= 1);
REQUIRE(n <= 32);
}
// Test 2: wgm_value == 1 → slab width 1, all CUs in M dimension
{
auto [m, n] = origami::gemm::compute_mall_tiles(128, 128, 256, 1);
REQUIRE(m <= 128);
REQUIRE(n >= 1);
}
// Test 3: Large wgm → slab covers entire N
{
auto [m, n] = origami::gemm::compute_mall_tiles(16, 8, 64, 16);
REQUIRE(n <= 8);
REQUIRE(m <= 16);
}
// Test 4: active_cus > grid tiles → wrap around
{
auto [m, n] = origami::gemm::compute_mall_tiles(4, 4, 256, 4);
REQUIRE(m == 4);
REQUIRE(n == 4);
}
// Test 5: Single tile grid
{
auto [m, n] = origami::gemm::compute_mall_tiles(1, 1, 256, 6);
REQUIRE(m == 1);
REQUIRE(n == 1);
}
}
TEST_CASE("GEMM: compute_l2_tiles unit test", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - compute_l2_tiles") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(256, 256, 64, 32, 32, 8, false, 1);
size_t grid_m = (4096 + 255) / 256;
size_t grid_n = (4096 + 255) / 256;
// Test 1: Basic — result should be in valid range
auto [m, n] = origami::gemm::compute_l2_tiles(problem, hardware, config, grid_m, grid_n, 256, 1, 6);
REQUIRE(m >= 1);
REQUIRE(m <= grid_m);
REQUIRE(n >= 1);
REQUIRE(n <= grid_n);
// Test 2: Larger WGM → wider slab → more N reuse
auto [m1, n1] =
origami::gemm::compute_l2_tiles(problem, hardware, config, grid_m, grid_n, 256, 1, 1);
auto [m2, n2] =
origami::gemm::compute_l2_tiles(problem, hardware, config, grid_m, grid_n, 256, 1, 8);
// Wider slab should not have fewer N columns
REQUIRE(n2 >= n1);
// Test 3: With splitting factor > 1 → fewer effective CUs per XCD → smaller tile
auto [m3, n3] =
origami::gemm::compute_l2_tiles(problem, hardware, config, grid_m, grid_n, 256, 4, 6);
REQUIRE(m3 <= m); // Splitting should not increase tile size
// Test 4: Single tile grid
auto [m4, n4] = origami::gemm::compute_l2_tiles(problem, hardware, config, 1, 1, 256, 1, 6);
REQUIRE(m4 == 1);
REQUIRE(n4 == 1);
}
}
}
TEST_CASE("GEMM: compute_mem_bw_from_occupancy unit test", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - compute_mem_bw_from_occupancy unit test") {
auto hardware = make_hardware(gpu_arch);
// Test 1: Test with various num_active_cus values
auto result_various_num_active_cus =
origami::gemm::compute_mem_bw_from_occupancy(hardware, hardware.N_CU);
REQUIRE(result_various_num_active_cus == 1.0);
// Test 2: Test with different mem_bw_per_wg_coefficients
hardware.mem_bw_per_wg_coefficients = std::make_tuple(0, 0.008, 0);
auto result_different_mem_bw_per_wg_coefficients =
origami::gemm::compute_mem_bw_from_occupancy(hardware, hardware.N_CU);
REQUIRE(result_different_mem_bw_per_wg_coefficients == 1.0);
hardware.mem_bw_per_wg_coefficients = std::make_tuple(0, 0.17, 0);
result_different_mem_bw_per_wg_coefficients =
origami::gemm::compute_mem_bw_from_occupancy(hardware, hardware.N_CU);
REQUIRE(result_different_mem_bw_per_wg_coefficients == 1.0);
hardware.mem_bw_per_wg_coefficients = std::make_tuple(0, 0.22, 0);
result_different_mem_bw_per_wg_coefficients =
origami::gemm::compute_mem_bw_from_occupancy(hardware, hardware.N_CU);
REQUIRE(result_different_mem_bw_per_wg_coefficients == 1.0);
// Test 3: Test with values less than 1
hardware.mem_bw_per_wg_coefficients = std::make_tuple(0.000001, 0.001, 0);
auto result_value_less_than_one =
origami::gemm::compute_mem_bw_from_occupancy(hardware, hardware.N_CU);
if (gpu_arch == 942)
REQUIRE(result_value_less_than_one == Approx(0.3964).epsilon(1e-3));
else if (gpu_arch == 950)
REQUIRE(result_value_less_than_one == Approx(0.32153).epsilon(1e-3));
hardware.mem_bw_per_wg_coefficients = std::make_tuple(0.000002, 0.002, 0);
result_value_less_than_one = origami::gemm::compute_mem_bw_from_occupancy(hardware, hardware.N_CU);
if (gpu_arch == 942)
REQUIRE(result_value_less_than_one == Approx(0.7928).epsilon(1e-3));
else if (gpu_arch == 950)
REQUIRE(result_value_less_than_one == Approx(0.64307).epsilon(1e-3));
// Reset the value of mem_bw_per_wg_coefficients back
if (gpu_arch == 942)
hardware.mem_bw_per_wg_coefficients = std::make_tuple(0, 0.015, 0);
else if (gpu_arch == 950)
hardware.mem_bw_per_wg_coefficients = std::make_tuple(0, 0.008, 0);
// Test 4: Verify calculation correctness (TODO)
}
}
}
TEST_CASE("GEMM: compute_l2_hit_rate_global unit test", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - compute_l2_hit_rate_global unit test") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(2047, 2047, 4096);
auto config = make_config(256, 256, 64, 32, 32, 8, false, 1);
// Test 1: Test with various problem sizes
auto result_various_problem_sizes = origami::gemm::compute_l2_hit_rate_global(
problem, hardware, config, hardware.L2_capacity * 1024);
REQUIRE(result_various_problem_sizes == 0.875);
auto problem_small = make_problem(331, 4077, 547);
result_various_problem_sizes = origami::gemm::compute_l2_hit_rate_global(
problem_small, hardware, config, hardware.L2_capacity * 1024);
REQUIRE(result_various_problem_sizes == 0.71875);
auto problem_large = make_problem(8193, 4077, 7453);
result_various_problem_sizes = origami::gemm::compute_l2_hit_rate_global(
problem_large, hardware, config, hardware.L2_capacity * 1024);
REQUIRE(result_various_problem_sizes == Approx(0.953).epsilon(1e-3));
// Test 2: Test with different splitting factors (TODO)(is this a valid test case as this
// function does not use splitting factors) Test 3: Test edge cases
REQUIRE_THROWS_WITH(origami::gemm::compute_l2_hit_rate_global(problem, hardware, config, 0UL),
"L2 Capacity is zero");
auto problem_zero = make_problem(0, 0, 7453);
REQUIRE_THROWS_WITH(origami::gemm::compute_l2_hit_rate_global(
problem_zero, hardware, config, hardware.L2_capacity * 1024),
"estimate_l2_hit grid dimensions can not be zero");
}
}
}
TEST_CASE("GEMM: round_elements_to_128B unit test", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - round_elements_to_128B unit test") {
// Test 1: Test with various element sizes
auto result_various_element_sizes =
origami::gemm::round_elements_to_128B(196, 32); // element size in bits - 32
REQUIRE(result_various_element_sizes == 224);
result_various_element_sizes =
origami::gemm::round_elements_to_128B(225, 16); // element size in bits - 16
REQUIRE(result_various_element_sizes == 256);
result_various_element_sizes =
origami::gemm::round_elements_to_128B(90, 8); // element size in bits - 8
REQUIRE(result_various_element_sizes == 128);
// Test 2: Test alignment to 128-byte boundary (TODO) (Was already covered in the above
// example, could be skipped) Test 3: Test edge cases
auto result_edge_cases = origami::gemm::round_elements_to_128B(0, 32);
REQUIRE(result_edge_cases == 0);
result_edge_cases = origami::gemm::round_elements_to_128B(256, 0);
REQUIRE(result_edge_cases == 256);
}
}
}
TEST_CASE("GEMM: compute_cvt_overhead unit test", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - compute_cvt_overhead unit test") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(2047, 2047, 4096);
auto config = make_config(256, 256, 64, 32, 32, 8, false, 1);
// Test 1: Test with Float data type
origami::problem_t problem_Float = {
.size = {4097, 8001, 4096},
.batch = 1,
.a_transpose = origami::transpose_t::N,
.b_transpose = origami::transpose_t::T,
.a_dtype = origami::data_type_t::Float,
.b_dtype = origami::data_type_t::Float,
.mi_dtype = origami::data_type_t::Float,
.a_mx_block_size = 0,
.b_mx_block_size = 0,
};
auto result_test_with_Float = origami::gemm::compute_cvt_overhead(problem_Float, hardware, config);
REQUIRE(result_test_with_Float == 0.0);
// Test 2: Test with XFloat32 as mi_dtype and BFloat16 as a_dtype and b_dtype
origami::problem_t problem_XFloat32 = {
.size = {8097, 8001, 4096},
.batch = 1,
.a_transpose = origami::transpose_t::N,
.b_transpose = origami::transpose_t::T,
.a_dtype = origami::data_type_t::BFloat16,
.b_dtype = origami::data_type_t::BFloat16,
.mi_dtype = origami::data_type_t::XFloat32,
.a_mx_block_size = 0,
.b_mx_block_size = 0,
};
auto result_test_with_XFloat32 =
origami::gemm::compute_cvt_overhead(problem_XFloat32, hardware, config);
REQUIRE(result_test_with_XFloat32 == 0.0);
// Test 3: Test conversion overhead calculation (TODO) (Need more clarification)
// Test 4: Test with different tile sizes
auto result_with_different_tile_sizes =
origami::gemm::compute_cvt_overhead(problem, hardware, config);
REQUIRE(result_with_different_tile_sizes == 0.0);
config = make_config(128, 128, 64, 32, 32, 8, false, 1);
result_with_different_tile_sizes = origami::gemm::compute_cvt_overhead(problem, hardware, config);
REQUIRE(result_with_different_tile_sizes == 0.0);
config = make_config(64, 64, 256, 32, 32, 8, false, 1);
result_with_different_tile_sizes = origami::gemm::compute_cvt_overhead(problem, hardware, config);
REQUIRE(result_with_different_tile_sizes == 0.0);
}
}
}
TEST_CASE("GEMM: arithmetic_intensity and emulated_tf32_arithmetic_intensity unit case test",
"[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION(
"gfx" << gpu_arch
<< " - arithmetic_intensity and emulated_tf32_arithmetic_intensity unit case test") {
// Test 1: Test calculation correctness
auto result = origami::gemm::arithmetic_intensity(2047, 2047, 4096, 2);
REQUIRE(result == Approx(818.879).epsilon(1e-3));
result = origami::gemm::emulated_tf32_arithmetic_intensity(2047, 2047, 4096, 2);
REQUIRE(result == Approx(2456.639).epsilon(1e-3));
// Test 2: Test with various m, n, k, bytes_per_element
auto result_various_problem_size = origami::gemm::arithmetic_intensity(127, 4097, 8193, 2);
REQUIRE(result_various_problem_size == Approx(121.356).epsilon(1e-3));
result_various_problem_size = origami::gemm::arithmetic_intensity(4097, 4097, 257, 4);
REQUIRE(result_various_problem_size == Approx(114.175).epsilon(1e-3));
result_various_problem_size = origami::gemm::arithmetic_intensity(237, 4097, 8183, 4);
REQUIRE(result_various_problem_size == Approx(109.034).epsilon(1e-3));
result_various_problem_size = origami::gemm::emulated_tf32_arithmetic_intensity(127, 4097, 8193, 2);
REQUIRE(result_various_problem_size == Approx(364.0709).epsilon(1e-3));
result_various_problem_size = origami::gemm::emulated_tf32_arithmetic_intensity(4097, 4097, 257, 4);
REQUIRE(result_various_problem_size == Approx(342.527).epsilon(1e-3));
result_various_problem_size = origami::gemm::emulated_tf32_arithmetic_intensity(237, 4097, 8183, 4);
REQUIRE(result_various_problem_size == Approx(327.104).epsilon(1e-3));
// Test 3: Test edge cases (zero values)
auto result_zero_value = origami::gemm::arithmetic_intensity(0, 0, 0, 4);
REQUIRE(result_zero_value == 0.0);
result_zero_value = origami::gemm::emulated_tf32_arithmetic_intensity(0, 0, 0, 2);
REQUIRE(result_zero_value == 0.0);
}
}
}
TEST_CASE("GEMM: check_lds_capacity unit test", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - check_lds_capacity unit test") {
auto hardware = make_hardware(gpu_arch);
if (gpu_arch == 942) {
// Test 1: Test with tiles that exceed LDS capacity
auto result_tiles_exceed_LDS_capacity =
origami::gemm::check_lds_capacity(hardware,
{256, 256, 256},
origami::data_type_t::BFloat16,
origami::data_type_t::BFloat16);
REQUIRE(result_tiles_exceed_LDS_capacity == false);
result_tiles_exceed_LDS_capacity =
origami::gemm::check_lds_capacity(hardware,
{128, 128, 256},
origami::data_type_t::BFloat16,
origami::data_type_t::BFloat16);
REQUIRE(result_tiles_exceed_LDS_capacity == false);
result_tiles_exceed_LDS_capacity =
origami::gemm::check_lds_capacity(hardware,
{64, 128, 256},
origami::data_type_t::BFloat16,
origami::data_type_t::BFloat16);
REQUIRE(result_tiles_exceed_LDS_capacity == false);
// Test 2: Test with different data type combinations
auto result_different_data_type = origami::gemm::check_lds_capacity(
hardware, {64, 64, 256}, origami::data_type_t::Half, origami::data_type_t::Half);
REQUIRE(result_different_data_type == true);
result_different_data_type = origami::gemm::check_lds_capacity(
hardware, {256, 256, 512}, origami::data_type_t::Double, origami::data_type_t::Double);
REQUIRE(result_different_data_type == false);
result_different_data_type = origami::gemm::check_lds_capacity(
hardware, {128, 128, 64}, origami::data_type_t::Int8, origami::data_type_t::Int8);
REQUIRE(result_different_data_type == true);
result_different_data_type = origami::gemm::check_lds_capacity(
hardware, {128, 128, 32}, origami::data_type_t::Int64, origami::data_type_t::Int64);
REQUIRE(result_different_data_type == true);
// Test 3: Test with Float (element_size = 16) (TODO: Need more clarification)
// Test 4: Test edge cases (exactly at capacity, just over)
auto result_exactly_at_capacity =
origami::gemm::check_lds_capacity(hardware,
{256, 256, 64},
origami::data_type_t::BFloat16,
origami::data_type_t::Float8); // exactly at capacity
REQUIRE(result_exactly_at_capacity == true);
result_exactly_at_capacity =
origami::gemm::check_lds_capacity(hardware,
{192, 96, 128},
origami::data_type_t::BFloat16,
origami::data_type_t::BFloat16); // just over
REQUIRE(result_exactly_at_capacity == false);
} else if (gpu_arch == 950) {
// Test 1: Test with tiles that exceed LDS capacity
auto result_tiles_exceed_LDS_capacity =
origami::gemm::check_lds_capacity(hardware,
{512, 512, 256},
origami::data_type_t::BFloat16,
origami::data_type_t::BFloat16);
REQUIRE(result_tiles_exceed_LDS_capacity == false);
result_tiles_exceed_LDS_capacity =
origami::gemm::check_lds_capacity(hardware,
{256, 256, 512},
origami::data_type_t::BFloat16,
origami::data_type_t::BFloat16);
REQUIRE(result_tiles_exceed_LDS_capacity == false);
result_tiles_exceed_LDS_capacity =
origami::gemm::check_lds_capacity(hardware,
{512, 128, 256},
origami::data_type_t::BFloat16,
origami::data_type_t::BFloat16);
REQUIRE(result_tiles_exceed_LDS_capacity == false);
// Test 2: Test with different data type combinations
auto result_different_data_type = origami::gemm::check_lds_capacity(
hardware, {64, 64, 256}, origami::data_type_t::Half, origami::data_type_t::Half);
REQUIRE(result_different_data_type == true);
result_different_data_type = origami::gemm::check_lds_capacity(
hardware, {256, 256, 512}, origami::data_type_t::Double, origami::data_type_t::Double);
REQUIRE(result_different_data_type == false);
result_different_data_type = origami::gemm::check_lds_capacity(
hardware, {256, 256, 64}, origami::data_type_t::Int8, origami::data_type_t::Int8);
REQUIRE(result_different_data_type == true);
result_different_data_type = origami::gemm::check_lds_capacity(
hardware, {256, 256, 32}, origami::data_type_t::Int64, origami::data_type_t::Int64);
REQUIRE(result_different_data_type == true);
// Test 3: Test with Float (element_size = 16) (TODO: Need more clarification)
// Test 4: Test edge cases (exactly at capacity, just over)
auto result_exactly_at_capacity =
origami::gemm::check_lds_capacity(hardware,
{512, 128, 256},
origami::data_type_t::Float8,
origami::data_type_t::Float8); // exactly at capacity
REQUIRE(result_exactly_at_capacity == true);
result_exactly_at_capacity =
origami::gemm::check_lds_capacity(hardware,
{512, 192, 256},
origami::data_type_t::BFloat16,
origami::data_type_t::BFloat16); // just over
REQUIRE(result_exactly_at_capacity == false);
}
}
}
}
TEST_CASE("GEMM: estimate_l2_hit and estimate_mall_hit unit test", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch
<< " - estimate_l2_hit and estimate_mall_hit with context_t") {
auto hardware = make_hardware(gpu_arch);
auto check_rates = [&](origami::problem_t& p, origami::config_t& c) {
origami::gemm::context_t ctx(p, hardware, c);
auto l2 = origami::gemm::estimate_l2_hit(p, hardware, c, ctx);
auto mall = origami::gemm::estimate_mall_hit(p, hardware, c, ctx);
REQUIRE(l2 >= 0.0);
REQUIRE(l2 <= 1.0);
REQUIRE(mall >= 0.0);
REQUIRE(mall <= 1.0);
};
// Various problem/config combinations
auto p1 = make_problem(2047, 2047, 4096);
auto c1 = make_config(256, 256, 64, 32, 32, 8, false, 1);
check_rates(p1, c1);
auto p2 = make_problem(8193, 2047, 4096);
auto c2 = make_config(128, 128, 128, 32, 32, 8, 1);
check_rates(p2, c2);
auto p3 = make_problem(8193, 4093, 1024);
auto c3 = make_config(64, 128, 128, 32, 32, 8, 1);
check_rates(p3, c3);
// Edge cases
auto p_small = make_problem(10, 11, 253);
auto c_edge = make_config(256, 256, 64, 32, 32, 8, 1);
check_rates(p_small, c_edge);
auto p_large = make_problem(81930, 40930, 10240);
check_rates(p_large, c_edge);
}
}
}
TEST_CASE("GEMM: count_unique_tiles unit test", "[gemm]") {
const size_t N_CU = 256;
const size_t num_xcd = 8;
SECTION("grid_b=64, grid_k=1, wgmxcc=1 — round-robin strided across batches") {
// Grid: 2×2×1×64, wgm=1, wgmxcc=1 (no wgmxcc)
// stride=8, mnk=4, each strided tile lands in a different batch
auto u = origami::gemm::count_unique_tiles({1, 2, 2, 64}, {0, 1, 1}, N_CU, num_xcd, 0, 0);
CHECK(u.k == 1);
CHECK(u.m == 1);
CHECK(u.n == 1);
CHECK(u.b == 32);
}
SECTION("grid_b=64, grid_k=1, wgmxcc=8 — contiguous block spans all mn per batch") {
// Grid: 2×2×1×64, wgm=1, wgmxcc=8
// Each XCD gets 32 contiguous tiles -> 4 mn × 8 batches
auto u = origami::gemm::count_unique_tiles({1, 2, 2, 64}, {0, 8, 1}, N_CU, num_xcd, 0, 0);
CHECK(u.k == 1);
CHECK(u.m == 2);
CHECK(u.n == 2);
CHECK(u.b == 8);
}
SECTION("grid_k=64, grid_b=1, wgmxcc=1 — round-robin strided across k-splits") {
// Grid: 2×2×64×1, wgm=1, wgmxcc=1
// stride=8 cycles through k: unique_k = 64/gcd(8,64) = 8
auto u = origami::gemm::count_unique_tiles({64, 2, 2, 1}, {0, 1, 1}, N_CU, num_xcd, 0, 0);
CHECK(u.k == 8);
CHECK(u.m == 2);
CHECK(u.n == 2);
CHECK(u.b == 1);
}
SECTION("grid_k=64, grid_b=1, wgmxcc=8 — contiguous block within one mn tile") {
// Grid: 2×2×64×1, wgm=1, wgmxcc=8
// Each XCD gets 32 contiguous tiles -> 32 k-splits in mn_id=0
auto u = origami::gemm::count_unique_tiles({64, 2, 2, 1}, {0, 8, 1}, N_CU, num_xcd, 0, 0);
CHECK(u.k == 32);
CHECK(u.m == 1);
CHECK(u.n == 1);
CHECK(u.b == 1);
}
SECTION("16×16 grid, wgmxcc=8, wgm=4 — contiguous block in first WGM slab") {
// Grid: 16×16×1×1, wgm=4, wgmxcc=8
// 32 contiguous tiles → 8 m-rows × 4 n-columns (one slab)
auto u = origami::gemm::count_unique_tiles({1, 16, 16, 1}, {0, 8, 4}, N_CU, num_xcd, 0, 0);
CHECK(u.k == 1);
CHECK(u.m == 8);
CHECK(u.n == 4);
CHECK(u.b == 1);
}
SECTION("8×8 grid with k=4, wgmxcc=8, wgm=2 — mixed k and mn") {
// Grid: 8×8×4×1, wgm=2, wgmxcc=8
// 32 contiguous tiles → 4 k-splits × 4 m-rows × 2 n-columns
auto u = origami::gemm::count_unique_tiles({4, 8, 8, 1}, {0, 8, 2}, N_CU, num_xcd, 0, 0);
CHECK(u.k == 4);
CHECK(u.m == 4);
CHECK(u.n == 2);
CHECK(u.b == 1);
}
SECTION("All XCDs report the same unique counts") {
// With wgmxcc=8, all XCDs should see the same tile structure
for (size_t xcd = 0; xcd < num_xcd; ++xcd) {
auto u = origami::gemm::count_unique_tiles({1, 16, 16, 1}, {0, 8, 4}, N_CU, num_xcd, xcd, 0);
CHECK(u.k == 1);
CHECK(u.m == 8);
CHECK(u.n == 4);
CHECK(u.b == 1);
}
}
SECTION("Zero/degenerate inputs return zeros") {
CHECK(origami::gemm::count_unique_tiles({1, 4, 4, 1}, {0, 8, 4}, 0, 8, 0, 0).m == 0);
CHECK(origami::gemm::count_unique_tiles({1, 4, 4, 1}, {0, 8, 4}, 256, 0, 0, 0).m == 0);
CHECK(origami::gemm::count_unique_tiles({1, 0, 4, 1}, {0, 8, 4}, 256, 8, 0, 0).m == 0);
CHECK(origami::gemm::count_unique_tiles({0, 4, 4, 1}, {0, 8, 4}, 256, 8, 0, 0).k == 0);
}
SECTION("Timestep beyond available tiles returns zeros") {
// 16 tiles total, 256 CUs -> 1 timestep. Timestep 1 should be empty.
origami::dim4_t grid{1, 4, 4, 1};
auto u = origami::gemm::count_unique_tiles(grid, {0, 1, 4}, N_CU, num_xcd, 0, 1);
CHECK(u.m == 0);
CHECK(u.n == 0);
CHECK(u.k == 0);
CHECK(u.b == 0);
}
SECTION("Single tile grid — all XCDs see at most 1 tile") {
origami::dim4_t grid{1, 1, 1, 1};
auto u0 = origami::gemm::count_unique_tiles(grid, {0, 1, 1}, N_CU, num_xcd, 0, 0);
CHECK(u0.m == 1);
CHECK(u0.n == 1);
CHECK(u0.k == 1);
CHECK(u0.b == 1);
// XCD 1 should get nothing (only 1 tile, XCD 0 gets it)
auto u1 = origami::gemm::count_unique_tiles(grid, {0, 1, 1}, N_CU, num_xcd, 1, 0);
CHECK(u1.m == 0);
}
SECTION("Round-robin: large grid, all MN tiles covered per XCD") {
// 32x32 grid = 1024 tiles, 256 CUs, 8 XCDs -> 32 tiles/XCD.
// stride=8 across 1024 MN tiles -> each XCD sees many M and N values.
origami::dim4_t grid{1, 32, 32, 1};
auto u = origami::gemm::count_unique_tiles(grid, {0, 1, 4}, N_CU, num_xcd, 0, 0);
CHECK(u.k == 1);
CHECK(u.m >= 1);
CHECK(u.m <= 32);
CHECK(u.n >= 1);
CHECK(u.n <= 32);
CHECK(u.b == 1);
}
SECTION("Round-robin: stride aligns with grid.k") {
// grid.k=8, stride=8 -> gcd=8, unique_k = 8/8 = 1.
// Each XCD sees a single K-split but many MN tiles.
origami::dim4_t grid{8, 4, 4, 1};
auto u = origami::gemm::count_unique_tiles(grid, {0, 1, 4}, N_CU, num_xcd, 0, 0);
CHECK(u.k == 1);
CHECK(u.m >= 1);
CHECK(u.n >= 1);
}
SECTION("Round-robin: stride coprime with grid.k") {
// grid.k=3, stride=8 -> gcd(8,3)=1, unique_k = 3/1 = 3 (all K-splits).
origami::dim4_t grid{3, 4, 4, 1};
auto u = origami::gemm::count_unique_tiles(grid, {0, 1, 4}, N_CU, num_xcd, 0, 0);
CHECK(u.k == 3);
}
SECTION("WGMXCC: last XCD gets correct tiles") {
// 256 tiles, 8 XCDs -> 32 per XCD. Last XCD starts at 7*32=224.
origami::dim4_t grid{1, 16, 16, 1};
auto u_first = origami::gemm::count_unique_tiles(grid, {0, 8, 4}, N_CU, num_xcd, 0, 0);
auto u_last = origami::gemm::count_unique_tiles(grid, {0, 8, 4}, N_CU, num_xcd, 7, 0);
// Both should get same structure with symmetric grid
CHECK(u_first.k == u_last.k);
CHECK(u_first.b == u_last.b);
// M/N may differ since they're at different positions, but should be valid
CHECK(u_last.m >= 1);
CHECK(u_last.m <= 16);
CHECK(u_last.n >= 1);
CHECK(u_last.n <= 16);
}
SECTION("Unique counts never exceed grid dimensions") {
// Fuzz-like: various grid shapes should never produce out-of-range results
std::vector<origami::dim4_t> grids = {{1, 3, 7, 1},
{4, 5, 5, 2},
{1, 1, 64, 1},
{1, 64, 1, 1},
{8, 4, 4, 4},
{1, 16, 16, 1},
{2, 8, 8, 3}};
std::vector<origami::workgroup_mapping_t> wgms = {
{0, 8, 1}, {0, 8, 4}, {0, 8, 8}, {0, 1, 1}, {0, 0, 6}};
for (auto& g : grids) {
for (auto& w : wgms) {
for (size_t xcd = 0; xcd < num_xcd; ++xcd) {
auto u = origami::gemm::count_unique_tiles(g, w, N_CU, num_xcd, xcd, 0);
CHECK(u.m <= g.m);
CHECK(u.n <= g.n);
CHECK(u.k <= g.k);
CHECK(u.b <= g.b);
}
}
}
}
}
TEST_CASE("Heuristics: Default parameters", "[heuristics]") {
origami::heuristic_params_t defaults;
// Check default weight values
REQUIRE(defaults.weight_mem_l2 == 1.0);
REQUIRE(defaults.weight_mem_mall == 1.0);
REQUIRE(defaults.weight_mem_dram == 1.0);
REQUIRE(defaults.weight_compute == 1.0);
REQUIRE(defaults.weight_memory == 1.0);
REQUIRE(defaults.weight_wg_setup == 1.0);
REQUIRE(defaults.weight_prologue == 1.5);
REQUIRE(defaults.weight_epilogue == 2.0);
REQUIRE(defaults.weight_loop_overhead == 500.0);
REQUIRE(defaults.weight_tile_total == 1.0);
// Check default empirical constants
// REQUIRE(defaults.l2_min_hit_rate_default == 0.5);
REQUIRE(defaults.main_memory_load_latency == 200.0);
REQUIRE(defaults.occupancy_decay_base == 0.95);
REQUIRE(defaults.mall_depth_sq == 2.0);
REQUIRE(defaults.mall_cold_floor == 0.85);
REQUIRE(defaults.l2_depth_sq == 4.0);
REQUIRE(defaults.l2_cold_floor == 0.75);
REQUIRE(defaults.l2_pollution_penalty == 0.7);
REQUIRE(defaults.l2_amp_ceiling_batched == 0.9);
REQUIRE(defaults.l2_amp_ceiling_k_split == 0.4);
REQUIRE(defaults.epilogue_cycles_per_acc_read == 8.0);
REQUIRE(defaults.epilogue_acc_read_parallelism == 0.9);
REQUIRE(defaults.epilogue_cycles_per_bounds_check == 6.0);
REQUIRE(defaults.epilogue_scalar_store_penalty == 1.1);
REQUIRE(defaults.epilogue_threads_per_wave == 64);
REQUIRE(defaults.epilogue_bytes_per_vectorized_store == 16);
REQUIRE(defaults.epilogue_cache_line_bytes == 128);
REQUIRE(defaults.epilogue_workspace_bytes_per_elem == 4);
REQUIRE(defaults.epilogue_salu_overhead == 35.0);
REQUIRE(defaults.epilogue_l_barrier == 100.0);
REQUIRE(defaults.epilogue_l_smem == 900.0);
REQUIRE(defaults.epilogue_k_padding_penalty == 50000.0);
REQUIRE(defaults.postgsu_compute_bytes == 4);
REQUIRE(defaults.postgsu_kernel_launch_overhead == 12000.0);
REQUIRE(defaults.postgsu_threads_per_wg == 256);
REQUIRE(defaults.postgsu_wavefront_size == 64);
// Check default main loop efficiency
REQUIRE(defaults.main_loop_efficiency == 1.0);
}
TEST_CASE("Heuristics: Parameter merging", "[heuristics]") {
origami::heuristic_params_t base;
origami::heuristic_params_t override;
// Set some non-default values in override
override.weight_compute = 2.0;
override.weight_memory = 3.0;
override.main_memory_load_latency = 300.0;
override.main_loop_efficiency = 0.8;
// Merge override into base
base.merge_with(override);
// Check that overridden values changed
REQUIRE(base.weight_compute == 2.0);
REQUIRE(base.weight_memory == 3.0);
REQUIRE(base.main_memory_load_latency == 300.0);
REQUIRE(base.main_loop_efficiency == 0.8);
// Check that non-overridden values remain default
REQUIRE(base.weight_mem_l2 == origami::heuristic_defaults_t::WEIGHT_MEM_L2);
REQUIRE(base.weight_prologue == origami::heuristic_defaults_t::WEIGHT_PROLOGUE);
REQUIRE(base.weight_epilogue == origami::heuristic_defaults_t::WEIGHT_EPILOGUE);
}
TEST_CASE("Heuristics: Key matching - exact match", "[heuristics]") {
auto hardware = make_hardware(950);
auto problem = make_problem(1024, 1024, 1024);
auto config = make_config(256, 256, 64, 16, 16, 16);
problem.mi_dtype = origami::data_type_t::BFloat16;
problem.a_transpose = origami::transpose_t::T;
problem.b_transpose = origami::transpose_t::N;
origami::heuristic_key_t key;
key.arch = origami::hardware_t::architecture_t::gfx950;
key.mi_dtype = origami::data_type_t::BFloat16;
key.a_transpose = origami::transpose_t::T;
key.b_transpose = origami::transpose_t::N;
key.mt_m = 256;
key.mt_n = 256;
key.mt_k = 64;
REQUIRE(key.matches(problem, hardware, config) == true);
}
TEST_CASE("Heuristics: Key matching - wildcard", "[heuristics]") {
auto hardware = make_hardware(950);
auto problem = make_problem(1024, 1024, 1024);
auto config = make_config(256, 256, 64, 16, 16, 16);
problem.mi_dtype = origami::data_type_t::BFloat16;
problem.a_transpose = origami::transpose_t::N;
problem.b_transpose = origami::transpose_t::T;
origami::heuristic_key_t key;
key.arch = origami::hardware_t::architecture_t::gfx950;
key.mi_dtype = origami::data_type_t::BFloat16;
// a_transpose, b_transpose, and tile sizes are wildcards (not set)
REQUIRE(key.matches(problem, hardware, config) == true);
}
TEST_CASE("Heuristics: Key matching - mismatch", "[heuristics]") {
auto hardware = make_hardware(950);
auto problem = make_problem(1024, 1024, 1024);
auto config = make_config(256, 256, 64, 16, 16, 16);
problem.mi_dtype = origami::data_type_t::BFloat16;
problem.a_transpose = origami::transpose_t::N;
problem.b_transpose = origami::transpose_t::T;
origami::heuristic_key_t key;
key.arch = origami::hardware_t::architecture_t::gfx950;
key.mi_dtype = origami::data_type_t::BFloat16;
key.a_transpose = origami::transpose_t::T; // Mismatch!
key.b_transpose = origami::transpose_t::T;
REQUIRE(key.matches(problem, hardware, config) == false);
}
TEST_CASE("Heuristics: Key matching - problem size ranges", "[heuristics]") {
auto hardware = make_hardware(950);
auto problem = make_problem(1024, 1024, 4096);
auto config = make_config(256, 256, 64, 16, 16, 16);
origami::heuristic_key_t key;
key.min_k = 2048;
key.max_k = 8192;
REQUIRE(key.matches(problem, hardware, config) == true);
// Test below minimum
problem = make_problem(1024, 1024, 1024);
REQUIRE(key.matches(problem, hardware, config) == false);
// Test above maximum
problem = make_problem(1024, 1024, 16384);
REQUIRE(key.matches(problem, hardware, config) == false);
}
TEST_CASE("Heuristics: Key specificity", "[heuristics]") {
origami::heuristic_key_t key1;
key1.arch = origami::hardware_t::architecture_t::gfx950;
REQUIRE(key1.specificity() == 1);
origami::heuristic_key_t key2;
key2.arch = origami::hardware_t::architecture_t::gfx950;
key2.mi_dtype = origami::data_type_t::BFloat16;
key2.mt_m = 256;
key2.mt_n = 256;
key2.mt_k = 64;
REQUIRE(key2.specificity() == 5);
// More specific key should have higher specificity
REQUIRE(key2.specificity() > key1.specificity());
}
TEST_CASE("Heuristics: Database lookup - successful call", "[heuristics]") {
auto& db = origami::heuristics_database_t::get_instance();
auto hardware = make_hardware(942);
auto problem = make_problem(128, 128, 128);
auto config = make_config(64, 64, 32, 16, 16, 16);
problem.a_dtype = origami::data_type_t::Half;
problem.b_dtype = origami::data_type_t::Half;
problem.mi_dtype = origami::data_type_t::Half;
problem.a_transpose = origami::transpose_t::N;
problem.b_transpose = origami::transpose_t::N;
// Test that lookup completes without error
auto params = db.lookup(problem, hardware, config);
// Verify that we got some parameters back
REQUIRE(params.main_memory_load_latency > 0.0);
REQUIRE(params.occupancy_decay_base > 0.0);
REQUIRE(params.occupancy_decay_base <= 1.0);
}
TEST_CASE("Heuristics: Optimized kernel efficiency lookup", "[heuristics]") {
auto& db = origami::heuristics_database_t::get_instance();
auto hardware = make_hardware(950);
auto problem = make_problem(1024, 1024, 1024);
auto config = make_config(256, 256, 64, 16, 16, 16);
problem.a_dtype = origami::data_type_t::BFloat16;
problem.b_dtype = origami::data_type_t::BFloat16;
problem.mi_dtype = origami::data_type_t::BFloat16;
problem.a_transpose = origami::transpose_t::N;
problem.b_transpose = origami::transpose_t::T;
config.hand_optimized_main_loop = true;
auto params = db.lookup(problem, hardware, config);
REQUIRE(params.main_loop_efficiency == Approx(100.0 / 115.0).epsilon(1e-6));
}
TEST_CASE("Heuristics: CMS macrotile differentiation", "[heuristics]") {
const double bf16_nt_160 =
lookup_cms_main_loop_efficiency(origami::data_type_t::BFloat16,
origami::transpose_t::N,
origami::transpose_t::T,
160,
256,
64);
const double bf16_nt_192 =
lookup_cms_main_loop_efficiency(origami::data_type_t::BFloat16,
origami::transpose_t::N,
origami::transpose_t::T,
192,
256,
64);
const double bf16_nt_256 =
lookup_cms_main_loop_efficiency(origami::data_type_t::BFloat16,
origami::transpose_t::N,
origami::transpose_t::T,
256,
256,
64);
REQUIRE(bf16_nt_160 == Approx(100.0 / 120.0).epsilon(1e-6));
REQUIRE(bf16_nt_192 == Approx(100.0 / 110.0).epsilon(1e-6));
REQUIRE(bf16_nt_256 == Approx(100.0 / 115.0).epsilon(1e-6));
REQUIRE(bf16_nt_160 != Approx(bf16_nt_192));
REQUIRE(bf16_nt_192 != Approx(bf16_nt_256));
const double tf32_tn_128 =
lookup_cms_main_loop_efficiency(origami::data_type_t::XFloat32,
origami::transpose_t::T,
origami::transpose_t::N,
128,
256,
32);
const double tf32_tn_192 =
lookup_cms_main_loop_efficiency(origami::data_type_t::XFloat32,
origami::transpose_t::T,
origami::transpose_t::N,
192,
256,
32);
REQUIRE(tf32_tn_128 == Approx(100.0 / 126.0).epsilon(1e-6));
REQUIRE(tf32_tn_192 == Approx(100.0 / 123.0).epsilon(1e-6));
REQUIRE(tf32_tn_128 != Approx(tf32_tn_192));
}
TEST_CASE("Heuristics: CMS dtype differentiation", "[heuristics]") {
const double bf16_nt_160 =
lookup_cms_main_loop_efficiency(origami::data_type_t::BFloat16,
origami::transpose_t::N,
origami::transpose_t::T,
160,
256,
64);
const double fp16_nt_160 = lookup_cms_main_loop_efficiency(origami::data_type_t::Half,
origami::transpose_t::N,
origami::transpose_t::T,
160,
256,
64);
REQUIRE(bf16_nt_160 == Approx(100.0 / 120.0).epsilon(1e-6));
REQUIRE(fp16_nt_160 == Approx(1.0).epsilon(1e-6));
REQUIRE(bf16_nt_160 != Approx(fp16_nt_160));
const double fp16_nt_unique =
lookup_cms_main_loop_efficiency(origami::data_type_t::Half,
origami::transpose_t::N,
origami::transpose_t::T,
192,
320,
64);
const double bf16_nt_missing =
lookup_cms_main_loop_efficiency(origami::data_type_t::BFloat16,
origami::transpose_t::N,
origami::transpose_t::T,
192,
320,
64);
REQUIRE(fp16_nt_unique == Approx(100.0 / 110.0).epsilon(1e-6));
REQUIRE(bf16_nt_missing == Approx(1.0).epsilon(1e-6));
REQUIRE(fp16_nt_unique != Approx(bf16_nt_missing));
const double tf32_tn =
lookup_cms_main_loop_efficiency(origami::data_type_t::XFloat32,
origami::transpose_t::T,
origami::transpose_t::N,
128,
256,
32);
const double bf16_tn =
lookup_cms_main_loop_efficiency(origami::data_type_t::BFloat16,
origami::transpose_t::T,
origami::transpose_t::N,
256,
256,
64);
REQUIRE(tf32_tn == Approx(100.0 / 126.0).epsilon(1e-6));
REQUIRE(bf16_tn == Approx(100.0 / 105.0).epsilon(1e-6));
REQUIRE(tf32_tn != Approx(bf16_tn));
}
TEST_CASE("Heuristics: CMS unregistered hand-optimized kernel", "[heuristics]") {
const double efficiency = lookup_cms_main_loop_efficiency(origami::data_type_t::BFloat16,
origami::transpose_t::N,
origami::transpose_t::T,
128,
256,
64);
REQUIRE(efficiency == Approx(1.0).epsilon(1e-6));
}
TEST_CASE("Heuristics: Problematic tile configuration (64x32x32)", "[heuristics]") {
auto& db = origami::heuristics_database_t::get_instance();
auto hardware = make_hardware(950);
auto problem = make_problem(1024, 1024, 1024);
auto config = make_config(64, 32, 32, 16, 16, 16);
problem.a_dtype = origami::data_type_t::BFloat16;
problem.b_dtype = origami::data_type_t::BFloat16;
problem.mi_dtype = origami::data_type_t::BFloat16;
problem.a_transpose = origami::transpose_t::N;
problem.b_transpose = origami::transpose_t::N;
auto params = db.lookup(problem, hardware, config);
// Should have 10x penalty for this problematic configuration
REQUIRE(params.weight_tile_total == 10.0);
}
TEST_CASE("Heuristics: TF32 emulation - memory bound", "[heuristics]") {
auto& db = origami::heuristics_database_t::get_instance();
auto hardware = make_hardware(950);
// Small problem: arith intensity = (3*2*512*512*512) / ((512*512 + 512*512 + 512*512) * 4) = 256
// < 1000
auto problem = make_problem(512, 512, 512);
auto config = make_config(256, 256, 32, 16, 16, 16);
problem.a_dtype = origami::data_type_t::Float;
problem.b_dtype = origami::data_type_t::Float;
problem.mi_dtype = origami::data_type_t::XFloat32;
problem.a_transpose = origami::transpose_t::N;
problem.b_transpose = origami::transpose_t::T;
auto params = db.lookup(problem, hardware, config);
// Should have optimization for memory-bound TF32 (arith < 1000)
REQUIRE(params.weight_tile_total == 0.6);
}
TEST_CASE("Heuristics: TF32 emulation - compute bound", "[heuristics]") {
auto& db = origami::heuristics_database_t::get_instance();
auto hardware = make_hardware(950);
// Large problem: arith intensity = (3*2*2048*2048*2048) / ((3 * 2048*2048) * 4) = 2048 > 1000
auto problem = make_problem(2048, 2048, 2048);
auto config = make_config(256, 256, 32, 16, 16, 16);
problem.a_dtype = origami::data_type_t::Float;
problem.b_dtype = origami::data_type_t::Float;
problem.mi_dtype = origami::data_type_t::XFloat32;
problem.a_transpose = origami::transpose_t::N;
problem.b_transpose = origami::transpose_t::T;
auto params = db.lookup(problem, hardware, config);
// Should have stronger optimization for compute-bound TF32 (arith >= 1000)
REQUIRE(params.weight_tile_total == 0.4);
}
TEST_CASE("Heuristics: Helper functions - make_kernel_variant_key", "[heuristics]") {
auto key = origami::make_hand_optimized_kernel_key(origami::hardware_t::architecture_t::gfx950,
origami::data_type_t::BFloat16,
origami::transpose_t::N,
origami::transpose_t::T,
256,
256,
64);
REQUIRE(key.arch.has_value());
REQUIRE(key.arch.value() == origami::hardware_t::architecture_t::gfx950);
REQUIRE(key.mi_dtype.has_value());
REQUIRE(key.mi_dtype.value() == origami::data_type_t::BFloat16);
REQUIRE(key.a_transpose.has_value());
REQUIRE(key.a_transpose.value() == origami::transpose_t::N);
REQUIRE(key.b_transpose.has_value());
REQUIRE(key.b_transpose.value() == origami::transpose_t::T);
REQUIRE(key.mt_m.has_value());
REQUIRE(key.mt_m.value() == 256);
REQUIRE(key.mt_n.has_value());
REQUIRE(key.mt_n.value() == 256);
REQUIRE(key.mt_k.has_value());
REQUIRE(key.mt_k.value() == 64);
REQUIRE(key.hand_optimized_main_loop.has_value());
REQUIRE(key.hand_optimized_main_loop.value() == true);
}
TEST_CASE("Heuristics: Helper functions - make_tile_key", "[heuristics]") {
auto key = origami::make_tile_key(128, 128, 32);
REQUIRE(key.mt_m.has_value());
REQUIRE(key.mt_m.value() == 128);
REQUIRE(key.mt_n.has_value());
REQUIRE(key.mt_n.value() == 128);
REQUIRE(key.mt_k.has_value());
REQUIRE(key.mt_k.value() == 32);
REQUIRE(!key.a_transpose.has_value());
REQUIRE(!key.b_transpose.has_value());
}
TEST_CASE("Heuristics: Helper functions - make_arch_dtype_key", "[heuristics]") {
auto key = origami::make_arch_dtype_key(origami::hardware_t::architecture_t::gfx950,
origami::data_type_t::XFloat32);
REQUIRE(key.arch.has_value());
REQUIRE(key.arch.value() == origami::hardware_t::architecture_t::gfx950);
REQUIRE(key.mi_dtype.has_value());
REQUIRE(key.mi_dtype.value() == origami::data_type_t::XFloat32);
REQUIRE(!key.a_transpose.has_value());
REQUIRE(!key.mt_m.has_value());
}
TEST_CASE("Heuristics: get_heuristic_params integration", "[heuristics]") {
auto hardware = make_hardware(950);
auto problem = make_problem(1024, 1024, 1024);
auto config = make_config(256, 256, 64, 16, 16, 16);
problem.a_dtype = origami::data_type_t::BFloat16;
problem.b_dtype = origami::data_type_t::BFloat16;
problem.mi_dtype = origami::data_type_t::BFloat16;
problem.a_transpose = origami::transpose_t::N;
problem.b_transpose = origami::transpose_t::T;
config.hand_optimized_main_loop = true;
// Test the main entry point function
auto params = origami::get_heuristic_params(problem, hardware, config);
REQUIRE(params.main_loop_efficiency == Approx(100.0 / 115.0).epsilon(1e-6));
}
TEST_CASE("Heuristics: Database add_entry and lookup", "[heuristics]") {
auto& db = origami::heuristics_database_t::get_instance();
// Create a very specific custom heuristic entry that won't conflict
origami::heuristic_key_t key;
key.arch = origami::hardware_t::architecture_t::gfx942;
key.mi_dtype = origami::data_type_t::Int8; // Unusual dtype
key.mt_m = 777; // Unique tile size
origami::heuristic_params_t params;
params.weight_wg_setup = 7.77; // Unique value
db.add_entry(key, params);
// Try to lookup this entry
auto hardware = make_hardware(942);
auto problem = make_problem(1024, 1024, 1024);
auto config = make_config(777, 128, 64, 16, 16, 16);
problem.mi_dtype = origami::data_type_t::Int8;
auto result = db.lookup(problem, hardware, config);
// Should find our custom entry
REQUIRE(result.weight_wg_setup == 7.77);
}
TEST_CASE("Heuristics: Hierarchical lookup (most specific wins)", "[heuristics]") {
auto& db = origami::heuristics_database_t::get_instance();
// Add a general rule
origami::heuristic_key_t general_key;
general_key.arch = origami::hardware_t::architecture_t::gfx942;
general_key.mi_dtype = origami::data_type_t::Float; // Specific dtype to avoid conflicts
origami::heuristic_params_t general_params;
general_params.weight_epilogue = 3.33; // Use a weight that's less likely to conflict
db.add_entry(general_key, general_params);
// Add a more specific rule
origami::heuristic_key_t specific_key;
specific_key.arch = origami::hardware_t::architecture_t::gfx942;
specific_key.mi_dtype = origami::data_type_t::Float;
specific_key.mt_m = 555; // Unique value
origami::heuristic_params_t specific_params;
specific_params.weight_epilogue = 5.55; // More specific value
db.add_entry(specific_key, specific_params);
// Lookup with matching specific case
auto hardware = make_hardware(942);
auto problem = make_problem(1024, 1024, 1024);
auto config = make_config(555, 128, 64, 16, 16, 16);
problem.mi_dtype = origami::data_type_t::Float;
auto result = db.lookup(problem, hardware, config);
// Should use more specific rule
REQUIRE(result.weight_epilogue == 5.55);
}
TEST_CASE("GEMM: compute_parallel_reduction_latency", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - returns zero when splitting_factor <= 1") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx(problem, hardware, config);
ctx.splitting_factor = 1;
ctx.reduction_strategy = origami::reduction_t::parallel;
auto latency = origami::gemm::compute_parallel_reduction_latency(problem, hardware, config, ctx);
REQUIRE(latency == 0.0);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - returns zero for non-parallel reduction") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx(problem, hardware, config);
ctx.splitting_factor = 4;
ctx.reduction_strategy = origami::reduction_t::none;
auto latency = origami::gemm::compute_parallel_reduction_latency(problem, hardware, config, ctx);
REQUIRE(latency == 0.0);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - positive latency for parallel split") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 8192);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx(problem, hardware, config);
ctx.splitting_factor = 4;
ctx.reduction_strategy = origami::reduction_t::parallel;
auto latency = origami::gemm::compute_parallel_reduction_latency(problem, hardware, config, ctx);
REQUIRE(latency > 0.0);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - higher split factor increases latency") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 8192);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx_lo(problem, hardware, config);
ctx_lo.splitting_factor = 2;
ctx_lo.reduction_strategy = origami::reduction_t::parallel;
origami::gemm::context_t ctx_hi(problem, hardware, config);
ctx_hi.splitting_factor = 8;
ctx_hi.reduction_strategy = origami::reduction_t::parallel;
auto lat_lo = origami::gemm::compute_parallel_reduction_latency(problem, hardware, config, ctx_lo);
auto lat_hi = origami::gemm::compute_parallel_reduction_latency(problem, hardware, config, ctx_hi);
REQUIRE(lat_hi > lat_lo);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - larger output increases latency") {
auto hardware = make_hardware(gpu_arch);
auto problem_small = make_problem(1024, 1024, 8192);
auto problem_large = make_problem(8192, 8192, 8192);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx_small(problem_small, hardware, config);
ctx_small.splitting_factor = 4;
ctx_small.reduction_strategy = origami::reduction_t::parallel;
origami::gemm::context_t ctx_large(problem_large, hardware, config);
ctx_large.splitting_factor = 4;
ctx_large.reduction_strategy = origami::reduction_t::parallel;
auto lat_small = origami::gemm::compute_parallel_reduction_latency(
problem_small, hardware, config, ctx_small);
auto lat_large = origami::gemm::compute_parallel_reduction_latency(
problem_large, hardware, config, ctx_large);
REQUIRE(lat_large > lat_small);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - batched problem scales latency") {
auto hardware = make_hardware(gpu_arch);
auto problem1 = make_problem(2048, 2048, 4096, origami::transpose_t::T, origami::transpose_t::N, 1);
auto problem4 = make_problem(2048, 2048, 4096, origami::transpose_t::T, origami::transpose_t::N, 4);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx1(problem1, hardware, config);
ctx1.splitting_factor = 4;
ctx1.reduction_strategy = origami::reduction_t::parallel;
origami::gemm::context_t ctx4(problem4, hardware, config);
ctx4.splitting_factor = 4;
ctx4.reduction_strategy = origami::reduction_t::parallel;
auto lat1 = origami::gemm::compute_parallel_reduction_latency(problem1, hardware, config, ctx1);
auto lat4 = origami::gemm::compute_parallel_reduction_latency(problem4, hardware, config, ctx4);
REQUIRE(lat4 > lat1);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - includes kernel launch overhead") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(64, 64, 4096);
auto config = make_config(64, 64, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx(problem, hardware, config);
ctx.splitting_factor = 2;
ctx.reduction_strategy = origami::reduction_t::parallel;
auto latency = origami::gemm::compute_parallel_reduction_latency(problem, hardware, config, ctx);
REQUIRE(latency >= ctx.heuristic.postgsu_kernel_launch_overhead);
}
}
}
TEST_CASE("GEMM: compute_epilogue_latency", "[gemm]") {
for (int gpu_arch : test_architectures) {
DYNAMIC_SECTION("gfx" << gpu_arch << " - positive for aligned interior tiles") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx(problem, hardware, config);
auto latency = origami::gemm::compute_epilogue_latency(problem, hardware, config, ctx);
REQUIRE(latency > 0.0);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - edge tiles cost more than interior") {
auto hardware = make_hardware(gpu_arch);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
auto problem_aligned = make_problem(4096, 4096, 1024);
origami::gemm::context_t ctx_aligned(problem_aligned, hardware, config);
auto lat_aligned =
origami::gemm::compute_epilogue_latency(problem_aligned, hardware, config, ctx_aligned);
auto problem_edge = make_problem(4097, 4097, 1024);
origami::gemm::context_t ctx_edge(problem_edge, hardware, config);
auto lat_edge = origami::gemm::compute_epilogue_latency(problem_edge, hardware, config, ctx_edge);
REQUIRE(lat_edge > lat_aligned);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - larger tiles have higher epilogue cost") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 1024);
auto config_small = make_config(64, 64, 64, 32, 32, 8, false, 1);
auto config_large = make_config(256, 256, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx_small(problem, hardware, config_small);
origami::gemm::context_t ctx_large(problem, hardware, config_large);
auto lat_small = origami::gemm::compute_epilogue_latency(problem, hardware, config_small, ctx_small);
auto lat_large = origami::gemm::compute_epilogue_latency(problem, hardware, config_large, ctx_large);
REQUIRE(lat_large > lat_small);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - serial split-K adds reduction cost") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 8192);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx_nosplit(problem, hardware, config);
ctx_nosplit.splitting_factor = 1;
ctx_nosplit.reduction_strategy = origami::reduction_t::none;
origami::gemm::context_t ctx_spinlock(problem, hardware, config);
ctx_spinlock.splitting_factor = 4;
ctx_spinlock.reduction_strategy = origami::reduction_t::spinlock;
auto lat_nosplit = origami::gemm::compute_epilogue_latency(problem, hardware, config, ctx_nosplit);
auto lat_spinlock = origami::gemm::compute_epilogue_latency(problem, hardware, config, ctx_spinlock);
REQUIRE(lat_spinlock > lat_nosplit);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - parallel reduction skips in-kernel reduce") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 8192);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx_spinlock(problem, hardware, config);
ctx_spinlock.splitting_factor = 4;
ctx_spinlock.reduction_strategy = origami::reduction_t::spinlock;
origami::gemm::context_t ctx_parallel(problem, hardware, config);
ctx_parallel.splitting_factor = 4;
ctx_parallel.reduction_strategy = origami::reduction_t::parallel;
auto lat_spinlock = origami::gemm::compute_epilogue_latency(problem, hardware, config, ctx_spinlock);
auto lat_parallel = origami::gemm::compute_epilogue_latency(problem, hardware, config, ctx_parallel);
REQUIRE(lat_spinlock > lat_parallel);
}
DYNAMIC_SECTION("gfx" << gpu_arch << " - returns zero when d_bytes is zero") {
auto hardware = make_hardware(gpu_arch);
auto problem = make_problem(4096, 4096, 1024);
auto config = make_config(128, 128, 64, 32, 32, 8, false, 1);
origami::gemm::context_t ctx(problem, hardware, config);
ctx.d_bytes = 0;
auto latency = origami::gemm::compute_epilogue_latency(problem, hardware, config, ctx);
REQUIRE(latency == 0.0);
}
}
}