Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

276 lines
16 KiB
C++

// mul_mm_bench.cpp — Mali mul_mm optimization R&D harness (#9584 / #9715).
//
// Mali-G715 reports `matrix cores: none` (no VK_KHR_cooperative_matrix), so LLM
// prefill is mul_mat-bound on raw global-memory bandwidth — the #9584 ceiling.
// This bench measures the canonical cooperative-matrix-free win on the real
// device: a shared-memory-tiled mul_mm vs the scalar (one-MAC-per-global-read)
// baseline. It verifies the tiled kernel matches the scalar result (and a CPU
// spot-check), then reports GPU time / GFLOPS / speedup per shape.
//
// Build (arm64, NDK):
// aarch64-linux-android28-clang++ -O2 -std=c++17 mul_mm_bench.cpp -lvulkan -o mul_mm_bench
// SPIR-V (glslc): ../vulkan/mul_mm_scalar.spv, ../vulkan/mul_mm_tiled.spv
// Run on device: LD_LIBRARY_PATH=. ./mul_mm_bench [--spv-dir DIR] [--runs N]
#include <vulkan/vulkan.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
namespace {
#define VK_CHECK(expr) do { VkResult _r = (expr); if (_r != VK_SUCCESS) { \
std::fprintf(stderr, "%s failed: %d\n", #expr, (int)_r); std::exit(1); } } while (0)
static std::string lower_ascii(const char * s) {
std::string out = s ? s : "";
for (char & c : out) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
return out;
}
static bool software_vulkan_allowed() {
const char * v = std::getenv("ELIZA_ALLOW_SOFTWARE_VULKAN");
return v && std::strcmp(v, "1") == 0;
}
static bool looks_like_software_vulkan_device(const char * name) {
const std::string d = lower_ascii(name);
return d.find("llvmpipe") != std::string::npos || d.find("lavapipe") != std::string::npos ||
d.find("swiftshader") != std::string::npos;
}
static std::vector<uint8_t> load_spirv(const std::string & path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) { std::fprintf(stderr, "cannot open SPIR-V %s\n", path.c_str()); std::exit(1); }
auto sz = (size_t)f.tellg();
std::vector<uint8_t> bytes(sz);
f.seekg(0); f.read((char *)bytes.data(), (std::streamsize)sz);
return bytes;
}
struct Vk {
VkInstance instance = VK_NULL_HANDLE;
VkPhysicalDevice pd = VK_NULL_HANDLE;
uint32_t qfam = (uint32_t)-1;
VkDevice device = VK_NULL_HANDLE;
VkQueue queue = VK_NULL_HANDLE;
double ts_period_ns = 1.0;
std::string device_name;
};
static Vk init_vk() {
Vk v;
VkApplicationInfo ai{}; ai.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
ai.pApplicationName = "eliza-mul-mm-bench"; ai.apiVersion = VK_API_VERSION_1_2;
VkInstanceCreateInfo ici{}; ici.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; ici.pApplicationInfo = &ai;
VK_CHECK(vkCreateInstance(&ici, nullptr, &v.instance));
uint32_t pdc = 0; VK_CHECK(vkEnumeratePhysicalDevices(v.instance, &pdc, nullptr));
if (pdc == 0) { std::fprintf(stderr, "no Vulkan devices\n"); std::exit(1); }
std::vector<VkPhysicalDevice> pds(pdc); VK_CHECK(vkEnumeratePhysicalDevices(v.instance, &pdc, pds.data()));
std::string want_substr;
if (const char * e = std::getenv("ELIZA_VK_DEVICE_SUBSTR")) want_substr = lower_ascii(e);
for (uint32_t idx = 0; idx < pdc; idx++) {
uint32_t qc = 0; vkGetPhysicalDeviceQueueFamilyProperties(pds[idx], &qc, nullptr);
std::vector<VkQueueFamilyProperties> qf(qc); vkGetPhysicalDeviceQueueFamilyProperties(pds[idx], &qc, qf.data());
uint32_t qfam = (uint32_t)-1;
for (uint32_t i = 0; i < qc; i++) if (qf[i].queueFlags & VK_QUEUE_COMPUTE_BIT) { qfam = i; break; }
if (qfam == (uint32_t)-1) continue;
if (!want_substr.empty()) {
VkPhysicalDeviceProperties p; vkGetPhysicalDeviceProperties(pds[idx], &p);
if (lower_ascii(p.deviceName).find(want_substr) == std::string::npos) continue;
}
v.pd = pds[idx]; v.qfam = qfam; break;
}
if (v.pd == VK_NULL_HANDLE) { std::fprintf(stderr, "no matching compute device\n"); std::exit(1); }
VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(v.pd, &props);
v.ts_period_ns = props.limits.timestampPeriod; v.device_name = props.deviceName;
if (!software_vulkan_allowed() && looks_like_software_vulkan_device(props.deviceName)) {
std::fprintf(stderr, "[mul_mm_bench] refusing software Vulkan '%s' (set ELIZA_ALLOW_SOFTWARE_VULKAN=1)\n", props.deviceName);
std::exit(2);
}
float prio = 1.0f;
VkDeviceQueueCreateInfo qci{}; qci.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
qci.queueFamilyIndex = v.qfam; qci.queueCount = 1; qci.pQueuePriorities = &prio;
VkDeviceCreateInfo dci{}; dci.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
dci.queueCreateInfoCount = 1; dci.pQueueCreateInfos = &qci;
VK_CHECK(vkCreateDevice(v.pd, &dci, nullptr, &v.device));
vkGetDeviceQueue(v.device, v.qfam, 0, &v.queue);
return v;
}
struct Buf { VkBuffer buf = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; void * mapped = nullptr; VkDeviceSize size = 0; };
static uint32_t find_mem(const Vk & v, uint32_t type_bits, VkMemoryPropertyFlags want) {
VkPhysicalDeviceMemoryProperties mp; vkGetPhysicalDeviceMemoryProperties(v.pd, &mp);
for (uint32_t i = 0; i < mp.memoryTypeCount; i++)
if ((type_bits & (1u << i)) && (mp.memoryTypes[i].propertyFlags & want) == want) return i;
std::fprintf(stderr, "no compatible memory type\n"); std::exit(1);
}
static Buf alloc_buf(const Vk & v, VkDeviceSize bytes) {
Buf b{}; b.size = bytes == 0 ? 4 : bytes;
VkBufferCreateInfo bi{}; bi.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bi.size = b.size; bi.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; bi.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_CHECK(vkCreateBuffer(v.device, &bi, nullptr, &b.buf));
VkMemoryRequirements mr; vkGetBufferMemoryRequirements(v.device, b.buf, &mr);
VkMemoryAllocateInfo mi{}; mi.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; mi.allocationSize = mr.size;
mi.memoryTypeIndex = find_mem(v, mr.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK_CHECK(vkAllocateMemory(v.device, &mi, nullptr, &b.mem));
VK_CHECK(vkBindBufferMemory(v.device, b.buf, b.mem, 0));
VK_CHECK(vkMapMemory(v.device, b.mem, 0, b.size, 0, &b.mapped));
std::memset(b.mapped, 0, (size_t)b.size);
return b;
}
static void free_buf(const Vk & v, Buf & b) {
if (b.mapped) vkUnmapMemory(v.device, b.mem);
if (b.buf) vkDestroyBuffer(v.device, b.buf, nullptr);
if (b.mem) vkFreeMemory(v.device, b.mem, nullptr);
b = Buf{};
}
struct Push { uint32_t M, N, K; };
// Build pipeline + descriptors, run warmup + N timed dispatches, return median GPU microseconds.
static double bench(const Vk & v, const std::vector<uint8_t> & spv,
const Buf & A, const Buf & B, const Buf & C, Push pc,
uint32_t gx, uint32_t gy, int warmup, int runs) {
VkDescriptorSetLayoutBinding dslb[3]{};
for (uint32_t i = 0; i < 3; i++) { dslb[i].binding = i; dslb[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; dslb[i].descriptorCount = 1; dslb[i].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; }
VkDescriptorSetLayoutCreateInfo dslci{}; dslci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; dslci.bindingCount = 3; dslci.pBindings = dslb;
VkDescriptorSetLayout dsl; VK_CHECK(vkCreateDescriptorSetLayout(v.device, &dslci, nullptr, &dsl));
VkPushConstantRange pcr{}; pcr.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; pcr.offset = 0; pcr.size = sizeof(Push);
VkPipelineLayoutCreateInfo plci{}; plci.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
plci.setLayoutCount = 1; plci.pSetLayouts = &dsl; plci.pushConstantRangeCount = 1; plci.pPushConstantRanges = &pcr;
VkPipelineLayout pll; VK_CHECK(vkCreatePipelineLayout(v.device, &plci, nullptr, &pll));
VkShaderModuleCreateInfo smci{}; smci.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; smci.codeSize = spv.size(); smci.pCode = (const uint32_t *)spv.data();
VkShaderModule sm; VK_CHECK(vkCreateShaderModule(v.device, &smci, nullptr, &sm));
VkComputePipelineCreateInfo cpci{}; cpci.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
cpci.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; cpci.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
cpci.stage.module = sm; cpci.stage.pName = "main"; cpci.layout = pll;
VkPipeline pipeline; VK_CHECK(vkCreateComputePipelines(v.device, VK_NULL_HANDLE, 1, &cpci, nullptr, &pipeline));
VkDescriptorPoolSize dps{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3 };
VkDescriptorPoolCreateInfo dpci{}; dpci.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; dpci.maxSets = 1; dpci.poolSizeCount = 1; dpci.pPoolSizes = &dps;
VkDescriptorPool dp; VK_CHECK(vkCreateDescriptorPool(v.device, &dpci, nullptr, &dp));
VkDescriptorSetAllocateInfo dsai{}; dsai.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; dsai.descriptorPool = dp; dsai.descriptorSetCount = 1; dsai.pSetLayouts = &dsl;
VkDescriptorSet ds; VK_CHECK(vkAllocateDescriptorSets(v.device, &dsai, &ds));
const Buf * bufs[3] = { &A, &B, &C };
VkDescriptorBufferInfo dbi[3]; VkWriteDescriptorSet wds[3]{};
for (uint32_t i = 0; i < 3; i++) {
dbi[i] = { bufs[i]->buf, 0, VK_WHOLE_SIZE };
wds[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; wds[i].dstSet = ds; wds[i].dstBinding = i; wds[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; wds[i].descriptorCount = 1; wds[i].pBufferInfo = &dbi[i];
}
vkUpdateDescriptorSets(v.device, 3, wds, 0, nullptr);
VkQueryPoolCreateInfo qpci{}; qpci.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; qpci.queryType = VK_QUERY_TYPE_TIMESTAMP; qpci.queryCount = 2;
VkQueryPool qpool; VK_CHECK(vkCreateQueryPool(v.device, &qpci, nullptr, &qpool));
VkCommandPoolCreateInfo cpinf{}; cpinf.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; cpinf.queueFamilyIndex = v.qfam; cpinf.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
VkCommandPool cmdpool; VK_CHECK(vkCreateCommandPool(v.device, &cpinf, nullptr, &cmdpool));
VkCommandBufferAllocateInfo cbai{}; cbai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; cbai.commandPool = cmdpool; cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cbai.commandBufferCount = 1;
VkCommandBuffer cb; VK_CHECK(vkAllocateCommandBuffers(v.device, &cbai, &cb));
auto submit = [&](bool ts) {
VkCommandBufferBeginInfo cbi{}; cbi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; cbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(vkBeginCommandBuffer(cb, &cbi));
if (ts) vkCmdResetQueryPool(cb, qpool, 0, 2);
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pll, 0, 1, &ds, 0, nullptr);
vkCmdPushConstants(cb, pll, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(Push), &pc);
if (ts) vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, qpool, 0);
vkCmdDispatch(cb, gx, gy, 1);
if (ts) vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, qpool, 1);
VK_CHECK(vkEndCommandBuffer(cb));
VkSubmitInfo si{}; si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; si.commandBufferCount = 1; si.pCommandBuffers = &cb;
VK_CHECK(vkQueueSubmit(v.queue, 1, &si, VK_NULL_HANDLE));
VK_CHECK(vkQueueWaitIdle(v.queue));
};
for (int i = 0; i < warmup; i++) submit(false);
std::vector<double> us;
for (int i = 0; i < runs; i++) {
submit(true);
uint64_t ts[2] = { 0, 0 };
if (vkGetQueryPoolResults(v.device, qpool, 0, 2, sizeof(ts), ts, sizeof(uint64_t),
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) == VK_SUCCESS && ts[1] >= ts[0])
us.push_back((double)(ts[1] - ts[0]) * v.ts_period_ns / 1000.0);
}
double median = -1.0;
if (!us.empty()) { std::sort(us.begin(), us.end()); median = us[us.size() / 2]; }
vkDestroyCommandPool(v.device, cmdpool, nullptr); vkDestroyQueryPool(v.device, qpool, nullptr);
vkDestroyDescriptorPool(v.device, dp, nullptr); vkDestroyPipeline(v.device, pipeline, nullptr);
vkDestroyShaderModule(v.device, sm, nullptr); vkDestroyPipelineLayout(v.device, pll, nullptr);
vkDestroyDescriptorSetLayout(v.device, dsl, nullptr);
return median;
}
} // namespace
int main(int argc, char ** argv) {
std::string spv_dir = "../vulkan";
int runs = 5, warmup = 2;
for (int i = 1; i < argc; i++) {
if (!std::strcmp(argv[i], "--spv-dir") && i + 1 < argc) spv_dir = argv[++i];
else if (!std::strcmp(argv[i], "--runs") && i + 1 < argc) runs = std::atoi(argv[++i]);
}
Vk v = init_vk();
std::printf("[mul_mm_bench] device=%s ts_period=%.2fns runs=%d\n", v.device_name.c_str(), v.ts_period_ns, runs);
auto spv_scalar = load_spirv(spv_dir + "/mul_mm_scalar.spv");
auto spv_tiled = load_spirv(spv_dir + "/mul_mm_tiled.spv");
auto spv_reg = load_spirv(spv_dir + "/mul_mm_reg.spv");
struct Shape { uint32_t M, N, K; const char * tag; };
Shape shapes[] = {
{ 512, 512, 512, "square-512" },
{ 1024, 1024, 1024, "square-1024" },
{ 256, 2048, 2048, "prefill-FFN (256x2048x2048)" },
{ 2048, 2048, 256, "proj (2048x2048x256)" },
};
int fails = 0;
for (const Shape & s : shapes) {
Buf A = alloc_buf(v, (VkDeviceSize)s.M * s.K * 4);
Buf B = alloc_buf(v, (VkDeviceSize)s.K * s.N * 4);
Buf C = alloc_buf(v, (VkDeviceSize)s.M * s.N * 4);
float * Am = (float *)A.mapped, * Bm = (float *)B.mapped, * Cm = (float *)C.mapped;
uint32_t seed = 0x1234u;
auto rnd = [&]() { seed = seed * 1103515245u + 12345u; return ((seed >> 9) / 8388608.0f) - 1.0f; };
for (uint64_t i = 0; i < (uint64_t)s.M * s.K; i++) Am[i] = rnd();
for (uint64_t i = 0; i < (uint64_t)s.K * s.N; i++) Bm[i] = rnd();
Push pc{ s.M, s.N, s.K };
const double flop = 2.0 * s.M * s.N * s.K;
const double tol = 1e-2 * s.K; // f32 accumulation tolerance scales with K
// scalar baseline (16x16 grid) — the trusted reference output.
double t_scalar = bench(v, spv_scalar, A, B, C, pc, (s.N + 15) / 16, (s.M + 15) / 16, warmup, runs);
std::vector<float> Cscalar((size_t)s.M * s.N);
std::memcpy(Cscalar.data(), Cm, Cscalar.size() * 4);
// CPU spot-check anchors the scalar baseline on a few elements.
double spot = 0.0;
for (int t = 0; t < 8; t++) {
uint32_t r = (uint32_t)((t * 131 + 7) % s.M), c = (uint32_t)((t * 197 + 11) % s.N);
double acc = 0.0; for (uint32_t k = 0; k < s.K; k++) acc += (double)Am[(uint64_t)r * s.K + k] * (double)Bm[(uint64_t)k * s.N + c];
spot = std::max(spot, std::fabs(acc - (double)Cscalar[(uint64_t)r * s.N + c]));
}
auto run_variant = [&](const std::vector<uint8_t> & spv, uint32_t gx, uint32_t gy) {
std::memset(Cm, 0, (size_t)s.M * s.N * 4);
double us = bench(v, spv, A, B, C, pc, gx, gy, warmup, runs);
double md = 0.0;
for (uint64_t i = 0; i < (uint64_t)s.M * s.N; i++) md = std::max(md, (double)std::fabs(Cm[i] - Cscalar[i]));
return std::pair<double, double>(us, md);
};
auto [t_tiled, d_tiled] = run_variant(spv_tiled, (s.N + 15) / 16, (s.M + 15) / 16);
auto [t_reg, d_reg] = run_variant(spv_reg, (s.N + 63) / 64, (s.M + 63) / 64);
bool ok = d_tiled < tol && d_reg < tol && spot < tol;
if (!ok) fails++;
std::printf("[%-26s] scalar=%8.1fus (%5.1f GF/s) | tiled=%8.1fus %.2fx | reg=%8.1fus %.2fx (%6.1f GF/s) | maxdiff tiled=%.2g reg=%.2g cpu=%.2g %s\n",
s.tag, t_scalar, flop / (t_scalar * 1e3),
t_tiled, t_scalar / t_tiled,
t_reg, t_scalar / t_reg, flop / (t_reg * 1e3),
d_tiled, d_reg, spot, ok ? "PASS" : "FAIL");
free_buf(v, A); free_buf(v, B); free_buf(v, C);
}
std::printf("[mul_mm_bench] %s\n", fails == 0 ? "PASS — tiled matches scalar on all shapes" : "FAIL — correctness mismatch");
return fails == 0 ? 0 : 1;
}