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
1094 lines
51 KiB
C++
1094 lines
51 KiB
C++
// Host-side Vulkan verification harness for the turbo3 / turbo4 / turbo3_tcq /
|
||
// qjl / polar compute shaders. Loads the JSON fixture written by gen_fixture,
|
||
// branches on the `kernel` field to choose the correct bind-set + push-constant
|
||
// shape, dispatches the shader, and compares scalar scores against the
|
||
// reference (default tolerance 1e-3 absolute).
|
||
//
|
||
// Build (only when VULKAN_SDK is set):
|
||
// VULKAN_SDK=/opt/vulkan-sdk make vulkan
|
||
//
|
||
// Run:
|
||
// ./vulkan_verify ../vulkan/turbo4.spv fixtures/turbo4.json
|
||
// ./vulkan_verify ../vulkan/qjl.spv fixtures/qjl.json
|
||
// ./vulkan_verify ../vulkan/polar.spv fixtures/polar.json
|
||
//
|
||
// The harness expects pre-compiled SPIR-V. To compile the shaders:
|
||
// glslc --target-env=vulkan1.1 --target-spv=spv1.3 \
|
||
// -fshader-stage=compute ../vulkan/<name>.comp -o ../vulkan/<name>.spv
|
||
|
||
#include "turbo_kernels.h"
|
||
extern "C" {
|
||
#include "qjl_polar_ref.h"
|
||
}
|
||
|
||
#include <vulkan/vulkan.h>
|
||
|
||
#include <cassert>
|
||
#include <cmath>
|
||
#include <cstdint>
|
||
#include <cstdio>
|
||
#include <cstdlib>
|
||
#include <cstring>
|
||
#include <fstream>
|
||
#include <sstream>
|
||
#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 * value = std::getenv("ELIZA_ALLOW_SOFTWARE_VULKAN");
|
||
return value && std::strcmp(value, "1") == 0;
|
||
}
|
||
|
||
static bool looks_like_software_vulkan_device(const char * name) {
|
||
const std::string device = lower_ascii(name);
|
||
return device.find("llvmpipe") != std::string::npos ||
|
||
device.find("lavapipe") != std::string::npos ||
|
||
device.find("swiftshader") != std::string::npos ||
|
||
device.find("software rasterizer") != std::string::npos;
|
||
}
|
||
|
||
// --- Fixture: union of every kernel's input shape. Only fields relevant to
|
||
// the loaded fixture's `kernel` are populated; the rest stay default. ---
|
||
struct Fixture {
|
||
std::string kernel;
|
||
// turbo*: head_dim, n_kv, blocks_per_kv, q (n_head*head_dim), k_blocks
|
||
// qjl: head_dim=128, proj_dim=256, n_heads, n_kv_heads, n_tokens,
|
||
// q_sketch, k_blocks
|
||
// polar: head_dim=128 (== QK_POLAR), n_rows, use_qjl, q (head_dim),
|
||
// k_blocks
|
||
int head_dim = 0;
|
||
int n_kv = 0; // turbo only
|
||
int block_bytes = 0;
|
||
int blocks_per_kv= 0; // turbo only
|
||
int proj_dim = 0; // qjl only
|
||
int n_heads = 0; // qjl only
|
||
int n_kv_heads = 0; // qjl only
|
||
int n_tokens = 0; // qjl only
|
||
int n_rows = 0; // polar only
|
||
int use_qjl = 0; // polar only
|
||
std::vector<float> q; // turbo / polar
|
||
std::vector<float> q_sketch; // qjl
|
||
std::vector<uint8_t> k_blocks;
|
||
std::vector<float> expected_scores;
|
||
};
|
||
|
||
static std::string slurp(const char * path) {
|
||
std::ifstream f(path);
|
||
if (!f) { std::fprintf(stderr, "cannot open %s\n", path); std::exit(1); }
|
||
std::stringstream ss; ss << f.rdbuf(); return ss.str();
|
||
}
|
||
|
||
static bool find_key(const std::string & s, const char * key, size_t & pos) {
|
||
std::string needle = std::string("\"") + key + "\"";
|
||
size_t k = s.find(needle);
|
||
if (k == std::string::npos) return false;
|
||
size_t colon = s.find(':', k);
|
||
pos = colon + 1;
|
||
while (pos < s.size() && std::isspace((unsigned char)s[pos])) pos++;
|
||
return true;
|
||
}
|
||
|
||
static int parse_int_after(const std::string & s, size_t pos) {
|
||
while (pos < s.size() && std::isspace((unsigned char)s[pos])) pos++;
|
||
char * end = nullptr;
|
||
long v = std::strtol(s.c_str() + pos, &end, 10);
|
||
return (int)v;
|
||
}
|
||
|
||
static std::vector<float> parse_float_array_at(const std::string & s, size_t pos) {
|
||
while (s[pos] != '[') pos++;
|
||
pos++;
|
||
std::vector<float> out;
|
||
while (s[pos] != ']') {
|
||
char * end = nullptr;
|
||
float v = std::strtof(s.c_str() + pos, &end);
|
||
out.push_back(v);
|
||
pos = (size_t)(end - s.c_str());
|
||
while (s[pos] == ',' || std::isspace((unsigned char)s[pos])) pos++;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
static std::vector<uint8_t> parse_byte_array_at(const std::string & s, size_t pos) {
|
||
while (s[pos] != '[') pos++;
|
||
pos++;
|
||
std::vector<uint8_t> out;
|
||
while (s[pos] != ']') {
|
||
char * end = nullptr;
|
||
long v = std::strtol(s.c_str() + pos, &end, 10);
|
||
out.push_back((uint8_t)v);
|
||
pos = (size_t)(end - s.c_str());
|
||
while (s[pos] == ',' || std::isspace((unsigned char)s[pos])) pos++;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
static std::string parse_string_at(const std::string & s, size_t pos) {
|
||
while (s[pos] != '"') pos++;
|
||
pos++;
|
||
size_t start = pos;
|
||
while (s[pos] != '"') pos++;
|
||
return s.substr(start, pos - start);
|
||
}
|
||
|
||
static int get_int(const std::string & s, const char * key, int dflt = 0) {
|
||
size_t pos = 0;
|
||
if (!find_key(s, key, pos)) return dflt;
|
||
return parse_int_after(s, pos);
|
||
}
|
||
|
||
static std::vector<float> get_floats(const std::string & s, const char * key) {
|
||
size_t pos = 0;
|
||
if (!find_key(s, key, pos)) return {};
|
||
return parse_float_array_at(s, pos);
|
||
}
|
||
|
||
static std::vector<uint8_t> get_bytes(const std::string & s, const char * key) {
|
||
size_t pos = 0;
|
||
if (!find_key(s, key, pos)) return {};
|
||
return parse_byte_array_at(s, pos);
|
||
}
|
||
|
||
static Fixture load_fixture(const char * path) {
|
||
std::string s = slurp(path);
|
||
Fixture fx;
|
||
{
|
||
size_t pos = 0;
|
||
if (!find_key(s, "kernel", pos)) {
|
||
std::fprintf(stderr, "fixture missing 'kernel' field\n"); std::exit(1);
|
||
}
|
||
fx.kernel = parse_string_at(s, pos);
|
||
}
|
||
fx.head_dim = get_int(s, "head_dim");
|
||
fx.n_kv = get_int(s, "n_kv");
|
||
fx.block_bytes = get_int(s, "block_bytes");
|
||
fx.blocks_per_kv = get_int(s, "blocks_per_kv");
|
||
fx.proj_dim = get_int(s, "proj_dim");
|
||
fx.n_heads = get_int(s, "n_heads");
|
||
fx.n_kv_heads = get_int(s, "n_kv_heads");
|
||
fx.n_tokens = get_int(s, "n_tokens");
|
||
fx.n_rows = get_int(s, "n_rows");
|
||
fx.use_qjl = get_int(s, "use_qjl");
|
||
fx.q = get_floats(s, "q");
|
||
fx.q_sketch = get_floats(s, "q_sketch");
|
||
fx.k_blocks = get_bytes(s, "k_blocks");
|
||
fx.expected_scores = get_floats(s, "expected_scores");
|
||
return fx;
|
||
}
|
||
|
||
static std::vector<uint8_t> load_spirv(const char * path) {
|
||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||
if (!f) { std::fprintf(stderr, "cannot open SPIR-V %s\n", path); std::exit(1); }
|
||
auto sz = (size_t)f.tellg();
|
||
if (sz % 4 != 0) { std::fprintf(stderr, "%s is not 4-byte aligned\n", path); std::exit(1); }
|
||
std::vector<uint8_t> bytes(sz);
|
||
f.seekg(0); f.read((char *)bytes.data(), (std::streamsize)sz);
|
||
return bytes;
|
||
}
|
||
|
||
static void hadamard128_inplace(std::vector<float> & x) {
|
||
if (x.size() != 128) {
|
||
std::fprintf(stderr, "hadamard128_inplace: expected 128 floats, got %zu\n", x.size());
|
||
std::exit(2);
|
||
}
|
||
for (size_t h = 1; h < x.size(); h <<= 1) {
|
||
for (size_t i = 0; i < x.size(); i += h << 1) {
|
||
for (size_t j = i; j < i + h; ++j) {
|
||
const float a = x[j];
|
||
const float b = x[j + h];
|
||
x[j] = a + b;
|
||
x[j + h] = a - b;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Push-constant structs. One per kernel family. Strong typing only — no
|
||
// catch-all union — so a mismatch between fixture and shader is a compile
|
||
// error in the harness, not a silent garbage push.
|
||
struct TurboPush {
|
||
uint32_t head_dim;
|
||
uint32_t n_kv;
|
||
uint32_t kv_stride_blocks;
|
||
uint32_t q_head;
|
||
uint32_t head_offset_bytes;
|
||
};
|
||
|
||
struct QjlPush {
|
||
uint32_t n_heads;
|
||
uint32_t n_kv_heads;
|
||
uint32_t n_tokens;
|
||
uint32_t proj_dim;
|
||
};
|
||
|
||
struct PolarPush {
|
||
uint32_t n_rows;
|
||
uint32_t head_dim;
|
||
uint32_t use_qjl;
|
||
uint32_t k_offset_bytes;
|
||
uint32_t q_offset;
|
||
uint32_t y_offset;
|
||
};
|
||
|
||
// Staged fallback entrypoints (qjl_get_rows / qjl_mul_mv / polar_get_rows).
|
||
struct QjlMulMvPush {
|
||
uint32_t n_rows;
|
||
uint32_t proj_dim;
|
||
};
|
||
struct QjlDequantPush {
|
||
uint32_t head_dim;
|
||
uint32_t proj_dim;
|
||
};
|
||
struct PolarDequantPush {
|
||
uint32_t head_dim;
|
||
uint32_t use_qjl;
|
||
};
|
||
|
||
// --- Kernel-specific dispatch parameters resolved from the fixture. ---
|
||
struct KernelBindings {
|
||
// Storage buffer #i payload: pointer + byte size. Bound at descriptor slot i.
|
||
struct Slot { const void * data; size_t bytes; };
|
||
std::vector<Slot> inputs; // bindings 0..N-1
|
||
size_t output_bytes; // last binding is the writeonly output
|
||
uint32_t n_outputs; // number of fp32 scalars expected
|
||
uint32_t dispatch_x;
|
||
uint32_t dispatch_y;
|
||
uint32_t dispatch_z;
|
||
// Push constants serialized to bytes for vkCmdPushConstants.
|
||
std::vector<uint8_t> push_bytes;
|
||
};
|
||
|
||
// --- Fused-attention fixture (the `cases`-array schema). One workgroup per
|
||
// (q_head, q_pos); the fixtures have n_q_pos == 1 (q_pos == 0). ---
|
||
struct FusedAttnPush {
|
||
uint32_t n_heads;
|
||
uint32_t n_kv_heads;
|
||
uint32_t n_tokens;
|
||
uint32_t q_pos;
|
||
uint32_t sm_scale_bits; // float bit pattern of sm_scale
|
||
uint32_t v_use_qjl_or_kv_tile; // tbq: kv_tile(=0); polar: v_use_qjl
|
||
uint32_t kv_tile; // polar only (tbq's 6th field is kv_tile already)
|
||
uint32_t causal;
|
||
uint32_t q_pos_base;
|
||
};
|
||
|
||
struct FusedAttnCase {
|
||
int n_heads = 0, n_kv_heads = 0, n_kv = 0;
|
||
int causal = 0;
|
||
int q_pos_base = 0;
|
||
std::vector<float> q_sketch; // n_heads * 256
|
||
std::vector<uint8_t> k_blocks; // n_kv_heads * n_kv * 34
|
||
std::vector<uint8_t> v_blocks; // tbq: *4*14 ; polar: *82
|
||
std::vector<float> expected_out; // n_heads * 128
|
||
};
|
||
|
||
// Extract the byte range [openIdx, matchingCloseIdx] of the first balanced
|
||
// `[` ... `]` array starting at-or-after `from`.
|
||
static bool find_balanced(const std::string & s, size_t from, char open, char close,
|
||
size_t & out_open, size_t & out_close) {
|
||
size_t i = s.find(open, from);
|
||
if (i == std::string::npos) return false;
|
||
int depth = 0;
|
||
for (size_t j = i; j < s.size(); ++j) {
|
||
if (s[j] == open) ++depth;
|
||
else if (s[j] == close) { --depth; if (depth == 0) { out_open = i; out_close = j; return true; } }
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Split a JSON array body (the chars between [ and ]) into the substrings of its
|
||
// top-level `{ ... }` objects.
|
||
static std::vector<std::string> split_object_array(const std::string & body) {
|
||
std::vector<std::string> out;
|
||
size_t pos = 0;
|
||
while (true) {
|
||
size_t o, c;
|
||
if (!find_balanced(body, pos, '{', '}', o, c)) break;
|
||
out.push_back(body.substr(o, c - o + 1));
|
||
pos = c + 1;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
static std::vector<FusedAttnCase> load_fused_cases(const std::string & s) {
|
||
size_t kpos = 0;
|
||
if (!find_key(s, "cases", kpos)) {
|
||
std::fprintf(stderr, "fused-attn fixture missing 'cases' array\n"); std::exit(1);
|
||
}
|
||
size_t arr_open, arr_close;
|
||
if (!find_balanced(s, kpos, '[', ']', arr_open, arr_close)) {
|
||
std::fprintf(stderr, "fused-attn fixture: malformed 'cases' array\n"); std::exit(1);
|
||
}
|
||
const std::string body = s.substr(arr_open + 1, arr_close - arr_open - 1);
|
||
std::vector<FusedAttnCase> out;
|
||
for (const std::string & obj : split_object_array(body)) {
|
||
FusedAttnCase c;
|
||
c.n_heads = get_int(obj, "n_heads");
|
||
c.n_kv_heads = get_int(obj, "n_kv_heads");
|
||
c.n_kv = get_int(obj, "n_kv");
|
||
c.causal = get_int(obj, "causal", 0);
|
||
c.q_pos_base = get_int(obj, "q_pos_base", 0);
|
||
c.q_sketch = get_floats(obj, "q_sketch");
|
||
c.k_blocks = get_bytes(obj, "k_blocks");
|
||
c.v_blocks = get_bytes(obj, "v_blocks");
|
||
c.expected_out = get_floats(obj, "expected_out");
|
||
out.push_back(std::move(c));
|
||
}
|
||
if (out.empty()) {
|
||
std::fprintf(stderr, "fused-attn fixture: 'cases' array is empty\n"); std::exit(1);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// --- Self-contained Vulkan run for the fused-attention shaders. Re-creates the
|
||
// pipeline once and per-case buffers/descriptors (the cases are small and
|
||
// few). Returns 0 iff every case passes within `tol`. The two shaders share
|
||
// the same 4-SSBO bind set (q_sketch, packed_k, packed_v, out) and an
|
||
// 8/9-uint push constant; the TBQ variant appends causal/q_pos_base after
|
||
// kv_tile, while Polar keeps v_use_qjl before kv_tile and then appends the
|
||
// same causal fields. ---
|
||
static int run_fused_attn(const char * spv_path, const char * fx_path, float tol) {
|
||
const std::string s = slurp(fx_path);
|
||
std::string kernel;
|
||
{ size_t p = 0; if (!find_key(s, "kernel", p)) { std::fprintf(stderr, "fixture missing 'kernel'\n"); return 2; }
|
||
kernel = parse_string_at(s, p); }
|
||
const bool is_polar = kernel == "fused_attn_qjl_polar";
|
||
if (!is_polar && kernel != "fused_attn_qjl_tbq") {
|
||
std::fprintf(stderr, "run_fused_attn: unexpected kernel '%s'\n", kernel.c_str()); return 2;
|
||
}
|
||
float sm_scale_v = 0.0f;
|
||
{ size_t p = 0; if (find_key(s, "sm_scale", p)) sm_scale_v = std::strtof(s.c_str() + p, nullptr); }
|
||
const uint32_t use_qjl = is_polar ? (uint32_t)get_int(s, "use_qjl", 0) : 0u;
|
||
const int v_block_bytes = get_int(s, "v_block_bytes", is_polar ? 82 : 14);
|
||
const int v_blocks_per_token = get_int(s, "v_blocks_per_token", is_polar ? 1 : 4);
|
||
const uint32_t v_token_bytes = (uint32_t)(v_block_bytes * v_blocks_per_token);
|
||
const std::vector<FusedAttnCase> cases = load_fused_cases(s);
|
||
|
||
std::printf("[vulkan_verify] kernel=%s spv=%s (fused-attention, %zu case(s))\n",
|
||
kernel.c_str(), spv_path, cases.size());
|
||
|
||
const auto spv = load_spirv(spv_path);
|
||
|
||
// --- Vulkan instance + device (once for the whole run). ---
|
||
VkApplicationInfo ai{}; ai.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
|
||
ai.pApplicationName = "eliza-fused-attn-verify"; ai.apiVersion = VK_API_VERSION_1_2;
|
||
VkInstanceCreateInfo ici{}; ici.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
|
||
ici.pApplicationInfo = &ai;
|
||
const char * inst_exts[] = { "VK_KHR_portability_enumeration" };
|
||
ici.enabledExtensionCount = 1; ici.ppEnabledExtensionNames = inst_exts; ici.flags = 0x00000001;
|
||
VkInstance instance;
|
||
if (vkCreateInstance(&ici, nullptr, &instance) != VK_SUCCESS) {
|
||
ici.enabledExtensionCount = 0; ici.ppEnabledExtensionNames = nullptr; ici.flags = 0;
|
||
VK_CHECK(vkCreateInstance(&ici, nullptr, &instance));
|
||
}
|
||
uint32_t pd_count = 0;
|
||
VK_CHECK(vkEnumeratePhysicalDevices(instance, &pd_count, nullptr));
|
||
if (pd_count == 0) { std::fprintf(stderr, "no Vulkan devices\n"); return 1; }
|
||
std::vector<VkPhysicalDevice> pds(pd_count);
|
||
VK_CHECK(vkEnumeratePhysicalDevices(instance, &pd_count, pds.data()));
|
||
VkPhysicalDevice pd = VK_NULL_HANDLE; uint32_t qfam = (uint32_t)-1;
|
||
for (VkPhysicalDevice cand : pds) {
|
||
uint32_t qc = 0; vkGetPhysicalDeviceQueueFamilyProperties(cand, &qc, nullptr);
|
||
std::vector<VkQueueFamilyProperties> qf(qc);
|
||
vkGetPhysicalDeviceQueueFamilyProperties(cand, &qc, qf.data());
|
||
for (uint32_t i = 0; i < qc; i++) if (qf[i].queueFlags & VK_QUEUE_COMPUTE_BIT) { pd = cand; qfam = i; break; }
|
||
if (pd != VK_NULL_HANDLE) break;
|
||
}
|
||
if (pd == VK_NULL_HANDLE) { std::fprintf(stderr, "no compute-capable Vulkan device\n"); return 1; }
|
||
{ VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(pd, &props);
|
||
std::printf("[vulkan_verify] device=%s api=%u.%u.%u\n", props.deviceName,
|
||
VK_VERSION_MAJOR(props.apiVersion), VK_VERSION_MINOR(props.apiVersion), VK_VERSION_PATCH(props.apiVersion));
|
||
if (!software_vulkan_allowed() && looks_like_software_vulkan_device(props.deviceName)) {
|
||
std::fprintf(stderr, "[vulkan_verify] refusing software Vulkan device '%s'. Set ELIZA_ALLOW_SOFTWARE_VULKAN=1 for diagnostics only.\n", props.deviceName);
|
||
return 2;
|
||
}
|
||
}
|
||
float prio = 1.0f;
|
||
VkDeviceQueueCreateInfo qci{}; qci.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
|
||
qci.queueFamilyIndex = qfam; qci.queueCount = 1; qci.pQueuePriorities = &prio;
|
||
VkDeviceCreateInfo dci{}; dci.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
||
dci.queueCreateInfoCount = 1; dci.pQueueCreateInfos = &qci;
|
||
uint32_t dec = 0; vkEnumerateDeviceExtensionProperties(pd, nullptr, &dec, nullptr);
|
||
std::vector<VkExtensionProperties> de(dec); vkEnumerateDeviceExtensionProperties(pd, nullptr, &dec, de.data());
|
||
std::vector<const char *> ede;
|
||
for (auto & e : de) if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0) ede.push_back("VK_KHR_portability_subset");
|
||
dci.enabledExtensionCount = (uint32_t)ede.size(); dci.ppEnabledExtensionNames = ede.empty() ? nullptr : ede.data();
|
||
VkDevice device; VK_CHECK(vkCreateDevice(pd, &dci, nullptr, &device));
|
||
VkQueue queue; vkGetDeviceQueue(device, qfam, 0, &queue);
|
||
|
||
auto find_mem = [&](uint32_t type_bits, VkMemoryPropertyFlags want) {
|
||
VkPhysicalDeviceMemoryProperties mp; vkGetPhysicalDeviceMemoryProperties(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);
|
||
};
|
||
struct Buf { VkBuffer buf; VkDeviceMemory mem; void * mapped; VkDeviceSize size; };
|
||
auto alloc_buf = [&](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(device, &bi, nullptr, &b.buf));
|
||
VkMemoryRequirements mr; vkGetBufferMemoryRequirements(device, b.buf, &mr);
|
||
VkMemoryAllocateInfo mi{}; mi.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; mi.allocationSize = mr.size;
|
||
mi.memoryTypeIndex = find_mem(mr.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||
VK_CHECK(vkAllocateMemory(device, &mi, nullptr, &b.mem));
|
||
VK_CHECK(vkBindBufferMemory(device, b.buf, b.mem, 0));
|
||
VK_CHECK(vkMapMemory(device, b.mem, 0, b.size, 0, &b.mapped));
|
||
return b;
|
||
};
|
||
auto free_buf = [&](Buf & b) { vkUnmapMemory(device, b.mem); vkDestroyBuffer(device, b.buf, nullptr); vkFreeMemory(device, b.mem, nullptr); };
|
||
|
||
VkDescriptorSetLayoutBinding dslb[4];
|
||
for (uint32_t i = 0; i < 4; i++) { dslb[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 = 4; dslci.pBindings = dslb;
|
||
VkDescriptorSetLayout dsl; VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &dsl));
|
||
const uint32_t push_size = is_polar ? (uint32_t)(9 * sizeof(uint32_t)) : (uint32_t)(8 * sizeof(uint32_t));
|
||
VkPushConstantRange pcr{}; pcr.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; pcr.offset = 0; pcr.size = push_size;
|
||
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(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(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(device, VK_NULL_HANDLE, 1, &cpci, nullptr, &pipeline));
|
||
VkCommandPoolCreateInfo cpinf{}; cpinf.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; cpinf.queueFamilyIndex = qfam;
|
||
cpinf.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||
VkCommandPool cmdpool; VK_CHECK(vkCreateCommandPool(device, &cpinf, nullptr, &cmdpool));
|
||
|
||
int total_fail = 0, total_n = 0;
|
||
float global_max_diff = 0.0f;
|
||
for (size_t ci = 0; ci < cases.size(); ++ci) {
|
||
const FusedAttnCase & c = cases[ci];
|
||
const uint32_t n_heads = (uint32_t)c.n_heads, n_kv_heads = (uint32_t)c.n_kv_heads, n_kv = (uint32_t)c.n_kv;
|
||
if ((uint32_t)c.q_sketch.size() != n_heads * 256u) { std::fprintf(stderr, "case %zu: q_sketch size mismatch\n", ci); return 2; }
|
||
if ((uint32_t)c.k_blocks.size() != n_kv_heads * n_kv * 34u) { std::fprintf(stderr, "case %zu: k_blocks size mismatch\n", ci); return 2; }
|
||
if ((uint32_t)c.v_blocks.size() != n_kv_heads * n_kv * v_token_bytes) { std::fprintf(stderr, "case %zu: v_blocks size mismatch (have %zu, want %u)\n", ci, c.v_blocks.size(), n_kv_heads * n_kv * v_token_bytes); return 2; }
|
||
if ((uint32_t)c.expected_out.size() != n_heads * 128u) { std::fprintf(stderr, "case %zu: expected_out size mismatch\n", ci); return 2; }
|
||
|
||
auto padded = [](const std::vector<uint8_t> & v) { std::vector<uint8_t> o(v.size() + 16, 0); std::memcpy(o.data(), v.data(), v.size()); return o; };
|
||
const std::vector<uint8_t> kpad = padded(c.k_blocks);
|
||
const std::vector<uint8_t> vpad = padded(c.v_blocks);
|
||
|
||
Buf q_buf = alloc_buf((VkDeviceSize)c.q_sketch.size() * sizeof(float));
|
||
Buf k_buf = alloc_buf((VkDeviceSize)kpad.size());
|
||
Buf v_buf = alloc_buf((VkDeviceSize)vpad.size());
|
||
Buf o_buf = alloc_buf((VkDeviceSize)c.expected_out.size() * sizeof(float));
|
||
std::memcpy(q_buf.mapped, c.q_sketch.data(), c.q_sketch.size() * sizeof(float));
|
||
std::memcpy(k_buf.mapped, kpad.data(), kpad.size());
|
||
std::memcpy(v_buf.mapped, vpad.data(), vpad.size());
|
||
std::memset(o_buf.mapped, 0, o_buf.size);
|
||
|
||
VkDescriptorPoolSize dps{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4 };
|
||
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(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(device, &dsai, &ds));
|
||
VkBuffer bufs[4] = { q_buf.buf, k_buf.buf, v_buf.buf, o_buf.buf };
|
||
VkDescriptorBufferInfo bi4[4]; VkWriteDescriptorSet wds[4];
|
||
for (uint32_t i = 0; i < 4; i++) {
|
||
bi4[i] = { bufs[i], 0, VK_WHOLE_SIZE };
|
||
wds[i] = {}; 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 = &bi4[i];
|
||
}
|
||
vkUpdateDescriptorSets(device, 4, wds, 0, nullptr);
|
||
|
||
uint32_t pc[9] = { n_heads, n_kv_heads, n_kv, 0u, 0u, 0u, 0u, 0u, 0u };
|
||
std::memcpy(&pc[4], &sm_scale_v, sizeof(uint32_t));
|
||
if (is_polar) {
|
||
pc[5] = use_qjl;
|
||
pc[6] = 0u; // kv_tile
|
||
pc[7] = (uint32_t)c.causal;
|
||
pc[8] = (uint32_t)c.q_pos_base;
|
||
} else {
|
||
pc[5] = 0u; // kv_tile
|
||
pc[6] = (uint32_t)c.causal;
|
||
pc[7] = (uint32_t)c.q_pos_base;
|
||
}
|
||
|
||
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(device, &cbai, &cb));
|
||
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));
|
||
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, push_size, pc);
|
||
vkCmdDispatch(cb, n_heads, 1, 1);
|
||
VK_CHECK(vkEndCommandBuffer(cb));
|
||
VkSubmitInfo si{}; si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; si.commandBufferCount = 1; si.pCommandBuffers = &cb;
|
||
VK_CHECK(vkQueueSubmit(queue, 1, &si, VK_NULL_HANDLE));
|
||
VK_CHECK(vkQueueWaitIdle(queue));
|
||
|
||
const float * out = (const float *)o_buf.mapped;
|
||
int fail = 0; float max_diff = 0.0f;
|
||
const int n = (int)c.expected_out.size();
|
||
for (int i = 0; i < n; i++) {
|
||
float diff = std::fabs(out[i] - c.expected_out[i]);
|
||
if (diff > max_diff) max_diff = diff;
|
||
if (diff >= tol) {
|
||
fail++;
|
||
if (fail <= 8) std::printf(" case %zu i=%d expected=%+.6f got=%+.6f diff=%.3e FAIL\n", ci, i, (double)c.expected_out[i], (double)out[i], (double)diff);
|
||
}
|
||
}
|
||
std::printf(" case %zu (n_heads=%u n_kv_heads=%u n_kv=%u causal=%d q_pos_base=%d): %s — %d/%d passed (max_diff=%.3e)\n",
|
||
ci, n_heads, n_kv_heads, n_kv, c.causal, c.q_pos_base,
|
||
fail == 0 ? "PASS" : "FAIL", n - fail, n, (double)max_diff);
|
||
if (max_diff > global_max_diff) global_max_diff = max_diff;
|
||
total_fail += fail; total_n += n;
|
||
|
||
vkFreeCommandBuffers(device, cmdpool, 1, &cb);
|
||
vkDestroyDescriptorPool(device, dp, nullptr);
|
||
free_buf(q_buf); free_buf(k_buf); free_buf(v_buf); free_buf(o_buf);
|
||
}
|
||
|
||
vkDestroyCommandPool(device, cmdpool, nullptr);
|
||
vkDestroyPipeline(device, pipeline, nullptr);
|
||
vkDestroyShaderModule(device, sm, nullptr);
|
||
vkDestroyPipelineLayout(device, pll, nullptr);
|
||
vkDestroyDescriptorSetLayout(device, dsl, nullptr);
|
||
vkDestroyDevice(device, nullptr);
|
||
vkDestroyInstance(instance, nullptr);
|
||
|
||
std::printf("[vulkan_verify] %s — %d/%d outputs passed across %zu case(s) (tol=%.0e, max_diff=%.3e)\n",
|
||
total_fail == 0 ? "PASS" : "FAIL", total_n - total_fail, total_n, cases.size(), (double)tol, (double)global_max_diff);
|
||
return total_fail == 0 ? 0 : 1;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
int main(int argc, char ** argv) {
|
||
if (argc < 3) {
|
||
std::fprintf(stderr, "usage: %s <kernel.spv> <fixture.json> [tolerance=1e-3]\n", argv[0]);
|
||
return 2;
|
||
}
|
||
const char * spv_path = argv[1];
|
||
const char * fx_path = argv[2];
|
||
float tol = 1e-3f;
|
||
uint32_t multi_per_wg = 0; // 0 == not a multi-block kernel run
|
||
// Parse positional tolerance + `--multi N`. `--multi N` drives the
|
||
// turbo*_multi.comp / qjl_multi.comp variants: N is the SPIR-V
|
||
// specialization constant (blocks/tokens per workgroup) and the dispatch
|
||
// grid shrinks by N×. Default 1 makes a multi variant identical to its
|
||
// base kernel.
|
||
for (int i = 3; i < argc; ++i) {
|
||
if (std::strcmp(argv[i], "--multi") == 0 && i + 1 < argc) {
|
||
multi_per_wg = (uint32_t)std::strtoul(argv[++i], nullptr, 10);
|
||
if (multi_per_wg == 0) {
|
||
std::fprintf(stderr, "--multi requires N >= 1\n");
|
||
return 2;
|
||
}
|
||
} else {
|
||
tol = std::strtof(argv[i], nullptr);
|
||
}
|
||
}
|
||
// Peek at the fixture's `kernel` field: the fused-attention shaders use the
|
||
// `cases`-array schema, which the flat-fixture path below cannot parse, so
|
||
// route them through the dedicated runner.
|
||
{
|
||
std::string head = slurp(fx_path);
|
||
size_t kp = 0;
|
||
if (find_key(head, "kernel", kp)) {
|
||
std::string kn = parse_string_at(head, kp);
|
||
if (kn == "fused_attn_qjl_tbq" || kn == "fused_attn_qjl_polar") {
|
||
return run_fused_attn(spv_path, fx_path, tol);
|
||
}
|
||
}
|
||
}
|
||
|
||
const bool kernel_uses_preht = std::strstr(spv_path, "preht") != nullptr;
|
||
const bool kernel_is_multi = std::strstr(spv_path, "_multi") != nullptr;
|
||
const bool kernel_is_qjl_getr = std::strstr(spv_path, "qjl_get_rows") != nullptr;
|
||
const bool kernel_is_qjl_mv = std::strstr(spv_path, "qjl_mul_mv") != nullptr;
|
||
const bool kernel_is_polar_getr = std::strstr(spv_path, "polar_get_rows") != nullptr;
|
||
if (kernel_is_multi && multi_per_wg == 0) multi_per_wg = 1;
|
||
|
||
Fixture fx = load_fixture(fx_path);
|
||
const char * variant_tag = kernel_is_multi ? " (multi-block variant)"
|
||
: (kernel_is_qjl_getr || kernel_is_qjl_mv || kernel_is_polar_getr)
|
||
? " (staged fallback entrypoint)" : "";
|
||
std::printf("[vulkan_verify] kernel=%s spv=%s%s\n", fx.kernel.c_str(), spv_path, variant_tag);
|
||
if (kernel_is_multi) {
|
||
std::printf("[vulkan_verify] multi-block: %u block(s)/token(s) per workgroup (spec constant 0)\n",
|
||
multi_per_wg);
|
||
}
|
||
|
||
// --- Resolve kernel-specific bind-set, push constants, dispatch shape ---
|
||
KernelBindings kb{};
|
||
std::vector<float> polar_q_storage;
|
||
// Storage that backs the staged-fallback inputs/expected outputs computed
|
||
// from the C reference (the fixtures only carry attention scores).
|
||
std::vector<float> fallback_prj;
|
||
std::vector<float> fallback_expected;
|
||
std::vector<uint8_t> fallback_block; // single block, padded to a uint16 boundary
|
||
// In production these decode shaders see the block as a sub-array of a
|
||
// larger contiguous KV tensor, so byte_offset reads up to the last
|
||
// 4-byte word land harmlessly inside the next block. The harness must
|
||
// mimic that: a tightly-sized single-block buffer would make Mesa's
|
||
// robustBufferAccess zero the last (partially-OOB) uint, breaking the
|
||
// bf16 norm read. Pad by 16 bytes of zeros.
|
||
auto pad_block = [](const uint8_t * src, size_t n) {
|
||
std::vector<uint8_t> out(n + 16, 0);
|
||
std::memcpy(out.data(), src, n);
|
||
return out;
|
||
};
|
||
if (kernel_is_qjl_getr || kernel_is_qjl_mv || kernel_is_polar_getr) {
|
||
// Staged Vulkan fallback entrypoints. They consume the qjl/polar
|
||
// fixture's packed-K bytes, but their expected outputs are computed
|
||
// here from the bit-exact C reference (qjl_polar_ref.{h,c}) since the
|
||
// fixtures only store attention scores.
|
||
if (kernel_is_qjl_mv) {
|
||
if (fx.kernel != "qjl") { std::fprintf(stderr, "qjl_mul_mv needs the qjl fixture\n"); return 2; }
|
||
const auto * blocks =
|
||
reinterpret_cast<const eliza_block_qjl1_256 *>(fx.k_blocks.data());
|
||
const int n_rows = fx.n_tokens; // head 0's token stream == rows
|
||
if (fx.k_blocks.size() < (size_t)n_rows * sizeof(eliza_block_qjl1_256)) {
|
||
std::fprintf(stderr, "qjl_mul_mv: fixture k_blocks too short\n"); return 2;
|
||
}
|
||
fallback_expected.resize(n_rows);
|
||
eliza_qjl_mul_mv(blocks, fx.q_sketch.data(), n_rows, fallback_expected.data());
|
||
|
||
fallback_block = pad_block(fx.k_blocks.data(),
|
||
(size_t)n_rows * sizeof(eliza_block_qjl1_256));
|
||
kb.inputs.push_back({ fallback_block.data(), fallback_block.size() });
|
||
kb.inputs.push_back({ fx.q_sketch.data(), fx.q_sketch.size() * sizeof(float) });
|
||
kb.output_bytes = (size_t)n_rows * sizeof(float);
|
||
kb.n_outputs = (uint32_t)n_rows;
|
||
kb.dispatch_x = (uint32_t)n_rows; kb.dispatch_y = 1; kb.dispatch_z = 1;
|
||
QjlMulMvPush pc{ (uint32_t)n_rows, 256u };
|
||
kb.push_bytes.assign((const uint8_t *)&pc, (const uint8_t *)&pc + sizeof(pc));
|
||
} else if (kernel_is_qjl_getr) {
|
||
if (fx.kernel != "qjl") { std::fprintf(stderr, "qjl_get_rows needs the qjl fixture\n"); return 2; }
|
||
const auto * blk = reinterpret_cast<const eliza_block_qjl1_256 *>(fx.k_blocks.data());
|
||
fallback_prj.resize((size_t)ELIZA_QJL_HEAD_DIM * ELIZA_QJL_PROJECTION_DIM);
|
||
eliza_qjl_make_projection(fallback_prj.data(), 0xCAFEBABE12345678ULL);
|
||
fallback_expected.resize(ELIZA_QJL_HEAD_DIM);
|
||
eliza_qjl_dequantize_row(blk, fallback_prj.data(), fallback_expected.data());
|
||
|
||
fallback_block = pad_block(fx.k_blocks.data(), sizeof(eliza_block_qjl1_256));
|
||
kb.inputs.push_back({ fallback_block.data(), fallback_block.size() });
|
||
kb.inputs.push_back({ fallback_prj.data(), fallback_prj.size() * sizeof(float) });
|
||
kb.output_bytes = (size_t)ELIZA_QJL_HEAD_DIM * sizeof(float);
|
||
kb.n_outputs = (uint32_t)ELIZA_QJL_HEAD_DIM;
|
||
kb.dispatch_x = 1; kb.dispatch_y = 1; kb.dispatch_z = 1;
|
||
QjlDequantPush pc{ (uint32_t)ELIZA_QJL_HEAD_DIM, 256u };
|
||
kb.push_bytes.assign((const uint8_t *)&pc, (const uint8_t *)&pc + sizeof(pc));
|
||
} else { // kernel_is_polar_getr
|
||
if (fx.kernel != "polar") { std::fprintf(stderr, "polar_get_rows needs a polar fixture\n"); return 2; }
|
||
const auto * blk = reinterpret_cast<const eliza_block_q4_polar *>(fx.k_blocks.data());
|
||
fallback_expected.resize(ELIZA_QK_POLAR);
|
||
eliza_polar_dequantize_row(blk, fallback_expected.data(), ELIZA_QK_POLAR, fx.use_qjl);
|
||
|
||
fallback_block = pad_block(fx.k_blocks.data(), sizeof(eliza_block_q4_polar));
|
||
kb.inputs.push_back({ fallback_block.data(), fallback_block.size() });
|
||
kb.output_bytes = (size_t)ELIZA_QK_POLAR * sizeof(float);
|
||
kb.n_outputs = (uint32_t)ELIZA_QK_POLAR;
|
||
kb.dispatch_x = 1; kb.dispatch_y = 1; kb.dispatch_z = 1;
|
||
PolarDequantPush pc{ (uint32_t)ELIZA_QK_POLAR, (uint32_t)fx.use_qjl };
|
||
kb.push_bytes.assign((const uint8_t *)&pc, (const uint8_t *)&pc + sizeof(pc));
|
||
}
|
||
// Hand the expected vector to the comparison path below.
|
||
fx.expected_scores = fallback_expected;
|
||
} else if (fx.kernel == "turbo3" || fx.kernel == "turbo4" || fx.kernel == "turbo3_tcq") {
|
||
// 3 buffers (q, k_blocks, scores) + optional codebook for turbo3_tcq.
|
||
kb.inputs.push_back({ fx.q.data(), fx.q.size() * sizeof(float) });
|
||
kb.inputs.push_back({ fx.k_blocks.data(), fx.k_blocks.size() });
|
||
kb.output_bytes = (size_t)fx.n_kv * sizeof(float);
|
||
kb.n_outputs = (uint32_t)fx.n_kv;
|
||
kb.dispatch_x = (uint32_t)fx.n_kv;
|
||
kb.dispatch_y = 1;
|
||
kb.dispatch_z = 1;
|
||
|
||
TurboPush pc{};
|
||
pc.head_dim = (uint32_t)fx.head_dim;
|
||
pc.n_kv = (uint32_t)fx.n_kv;
|
||
pc.kv_stride_blocks = (uint32_t)fx.blocks_per_kv;
|
||
pc.q_head = 0;
|
||
pc.head_offset_bytes = 0;
|
||
kb.push_bytes.assign((const uint8_t *)&pc,
|
||
(const uint8_t *)&pc + sizeof(pc));
|
||
} else if (fx.kernel == "qjl") {
|
||
// bindings = q_sketch (fp32) + packed_k (34B-block stream) + scores (fp32)
|
||
if (fx.proj_dim != 256) {
|
||
std::fprintf(stderr, "qjl: proj_dim must be 256 (got %d)\n", fx.proj_dim);
|
||
return 1;
|
||
}
|
||
kb.inputs.push_back({ fx.q_sketch.data(), fx.q_sketch.size() * sizeof(float) });
|
||
kb.inputs.push_back({ fx.k_blocks.data(), fx.k_blocks.size() });
|
||
kb.output_bytes = (size_t)fx.n_heads * (size_t)fx.n_tokens * sizeof(float);
|
||
kb.n_outputs = (uint32_t)(fx.n_heads * fx.n_tokens);
|
||
kb.dispatch_x = (uint32_t)fx.n_heads;
|
||
kb.dispatch_y = (uint32_t)fx.n_tokens;
|
||
kb.dispatch_z = 1;
|
||
|
||
QjlPush pc{};
|
||
pc.n_heads = (uint32_t)fx.n_heads;
|
||
pc.n_kv_heads = (uint32_t)fx.n_kv_heads;
|
||
pc.n_tokens = (uint32_t)fx.n_tokens;
|
||
pc.proj_dim = (uint32_t)fx.proj_dim;
|
||
kb.push_bytes.assign((const uint8_t *)&pc,
|
||
(const uint8_t *)&pc + sizeof(pc));
|
||
} else if (fx.kernel == "polar") {
|
||
// bindings = k_blocks (82B-block stream) + q (fp32) + y (fp32)
|
||
if (fx.head_dim != 128) {
|
||
std::fprintf(stderr, "polar: head_dim must be 128 (got %d)\n", fx.head_dim);
|
||
return 1;
|
||
}
|
||
const float * q_data = fx.q.data();
|
||
if (kernel_uses_preht) {
|
||
polar_q_storage = fx.q;
|
||
hadamard128_inplace(polar_q_storage);
|
||
q_data = polar_q_storage.data();
|
||
std::printf("[vulkan_verify] polar pre-Hadamard query enabled by SPIR-V path\n");
|
||
}
|
||
kb.inputs.push_back({ fx.k_blocks.data(), fx.k_blocks.size() });
|
||
kb.inputs.push_back({ q_data, fx.q.size() * sizeof(float) });
|
||
kb.output_bytes = (size_t)fx.n_rows * sizeof(float);
|
||
kb.n_outputs = (uint32_t)fx.n_rows;
|
||
kb.dispatch_x = (uint32_t)fx.n_rows;
|
||
kb.dispatch_y = 1;
|
||
kb.dispatch_z = 1;
|
||
|
||
PolarPush pc{};
|
||
pc.n_rows = (uint32_t)fx.n_rows;
|
||
pc.head_dim = (uint32_t)fx.head_dim;
|
||
pc.use_qjl = (uint32_t)fx.use_qjl;
|
||
pc.k_offset_bytes = 0;
|
||
pc.q_offset = 0;
|
||
pc.y_offset = 0;
|
||
kb.push_bytes.assign((const uint8_t *)&pc,
|
||
(const uint8_t *)&pc + sizeof(pc));
|
||
} else {
|
||
std::fprintf(stderr, "unknown kernel '%s' in fixture\n", fx.kernel.c_str());
|
||
return 1;
|
||
}
|
||
|
||
// Multi-block dispatch: shrink the grid by the per-workgroup count. turbo*
|
||
// walk the n_kv axis (dispatch_x); qjl walks the n_tokens axis (dispatch_y).
|
||
// The shader's specialization constant gets `multi_per_wg`; outputs and push
|
||
// constants are unchanged (each workgroup still writes the same scores).
|
||
if (kernel_is_multi) {
|
||
if (fx.kernel == "qjl") {
|
||
kb.dispatch_y = (kb.dispatch_y + multi_per_wg - 1u) / multi_per_wg;
|
||
} else {
|
||
kb.dispatch_x = (kb.dispatch_x + multi_per_wg - 1u) / multi_per_wg;
|
||
}
|
||
}
|
||
|
||
if (fx.expected_scores.size() != kb.n_outputs) {
|
||
std::fprintf(stderr,
|
||
"fixture expected_scores length mismatch: got %zu, need %u\n",
|
||
fx.expected_scores.size(), kb.n_outputs);
|
||
return 2;
|
||
}
|
||
|
||
// --- Vulkan instance ---
|
||
VkApplicationInfo ai{};
|
||
ai.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
|
||
ai.pApplicationName = "eliza-kv-verify";
|
||
ai.apiVersion = VK_API_VERSION_1_2;
|
||
VkInstanceCreateInfo ici{};
|
||
ici.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
|
||
ici.pApplicationInfo = &ai;
|
||
// MoltenVK on macOS is a non-conformant ICD and the Vulkan loader requires
|
||
// VK_KHR_portability_enumeration + the ENUMERATE_PORTABILITY flag to be
|
||
// willing to enumerate it. Always-on here is safe — the extension is
|
||
// either present (macOS) or absent (Linux/Windows desktop ICDs are fully
|
||
// conformant) but harmless if the loader supports it.
|
||
const char * inst_exts[] = { "VK_KHR_portability_enumeration" };
|
||
ici.enabledExtensionCount = 1;
|
||
ici.ppEnabledExtensionNames = inst_exts;
|
||
ici.flags = 0x00000001; // VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR
|
||
VkInstance instance;
|
||
if (vkCreateInstance(&ici, nullptr, &instance) != VK_SUCCESS) {
|
||
// Fallback for loaders without the portability extension (Linux, Windows).
|
||
ici.enabledExtensionCount = 0;
|
||
ici.ppEnabledExtensionNames = nullptr;
|
||
ici.flags = 0;
|
||
VK_CHECK(vkCreateInstance(&ici, nullptr, &instance));
|
||
}
|
||
|
||
// --- Pick first physical device with a compute queue ---
|
||
uint32_t pd_count = 0;
|
||
VK_CHECK(vkEnumeratePhysicalDevices(instance, &pd_count, nullptr));
|
||
if (pd_count == 0) { std::fprintf(stderr, "no Vulkan devices\n"); return 1; }
|
||
std::vector<VkPhysicalDevice> pds(pd_count);
|
||
VK_CHECK(vkEnumeratePhysicalDevices(instance, &pd_count, pds.data()));
|
||
VkPhysicalDevice pd = VK_NULL_HANDLE;
|
||
uint32_t qfam = (uint32_t)-1;
|
||
for (VkPhysicalDevice cand : pds) {
|
||
uint32_t cand_qfam_count = 0;
|
||
vkGetPhysicalDeviceQueueFamilyProperties(cand, &cand_qfam_count, nullptr);
|
||
std::vector<VkQueueFamilyProperties> cand_qfams(cand_qfam_count);
|
||
vkGetPhysicalDeviceQueueFamilyProperties(cand, &cand_qfam_count, cand_qfams.data());
|
||
for (uint32_t i = 0; i < cand_qfam_count; i++) {
|
||
if (cand_qfams[i].queueFlags & VK_QUEUE_COMPUTE_BIT) {
|
||
pd = cand;
|
||
qfam = i;
|
||
break;
|
||
}
|
||
}
|
||
if (pd != VK_NULL_HANDLE) break;
|
||
}
|
||
if (pd == VK_NULL_HANDLE) { std::fprintf(stderr, "no compute-capable Vulkan device\n"); return 1; }
|
||
{
|
||
VkPhysicalDeviceProperties props;
|
||
vkGetPhysicalDeviceProperties(pd, &props);
|
||
std::printf("[vulkan_verify] device=%s api=%u.%u.%u\n", props.deviceName,
|
||
VK_VERSION_MAJOR(props.apiVersion),
|
||
VK_VERSION_MINOR(props.apiVersion),
|
||
VK_VERSION_PATCH(props.apiVersion));
|
||
if (!software_vulkan_allowed() &&
|
||
looks_like_software_vulkan_device(props.deviceName)) {
|
||
std::fprintf(stderr,
|
||
"[vulkan_verify] refusing software Vulkan device '%s'. "
|
||
"Set ELIZA_ALLOW_SOFTWARE_VULKAN=1 for diagnostics only.\n",
|
||
props.deviceName);
|
||
return 2;
|
||
}
|
||
}
|
||
|
||
float prio = 1.0f;
|
||
VkDeviceQueueCreateInfo qci{};
|
||
qci.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
|
||
qci.queueFamilyIndex = qfam;
|
||
qci.queueCount = 1;
|
||
qci.pQueuePriorities = &prio;
|
||
VkDeviceCreateInfo dci{};
|
||
dci.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
||
dci.queueCreateInfoCount = 1;
|
||
dci.pQueueCreateInfos = &qci;
|
||
// VK_KHR_portability_subset is a required device extension on MoltenVK.
|
||
// Probe and enable it if available; conformant ICDs ignore the request.
|
||
uint32_t dev_ext_count = 0;
|
||
vkEnumerateDeviceExtensionProperties(pd, nullptr, &dev_ext_count, nullptr);
|
||
std::vector<VkExtensionProperties> dev_exts(dev_ext_count);
|
||
vkEnumerateDeviceExtensionProperties(pd, nullptr, &dev_ext_count, dev_exts.data());
|
||
std::vector<const char *> enabled_dev_exts;
|
||
for (auto & e : dev_exts) {
|
||
if (std::strcmp(e.extensionName, "VK_KHR_portability_subset") == 0) {
|
||
enabled_dev_exts.push_back("VK_KHR_portability_subset");
|
||
}
|
||
}
|
||
dci.enabledExtensionCount = (uint32_t)enabled_dev_exts.size();
|
||
dci.ppEnabledExtensionNames = enabled_dev_exts.empty() ? nullptr : enabled_dev_exts.data();
|
||
VkDevice device;
|
||
VK_CHECK(vkCreateDevice(pd, &dci, nullptr, &device));
|
||
VkQueue queue;
|
||
vkGetDeviceQueue(device, qfam, 0, &queue);
|
||
|
||
// --- Helper: allocate a host-visible buffer + memory ---
|
||
auto find_mem = [&](uint32_t type_bits, VkMemoryPropertyFlags want) {
|
||
VkPhysicalDeviceMemoryProperties props;
|
||
vkGetPhysicalDeviceMemoryProperties(pd, &props);
|
||
for (uint32_t i = 0; i < props.memoryTypeCount; i++) {
|
||
if ((type_bits & (1 << i)) &&
|
||
(props.memoryTypes[i].propertyFlags & want) == want) {
|
||
return i;
|
||
}
|
||
}
|
||
std::fprintf(stderr, "no compatible memory type\n"); std::exit(1);
|
||
};
|
||
struct Buf { VkBuffer buf; VkDeviceMemory mem; void * mapped; VkDeviceSize size; };
|
||
auto alloc_buf = [&](VkDeviceSize bytes, VkBufferUsageFlags usage) {
|
||
Buf b{};
|
||
// Vulkan buffers must have nonzero size; round zero-byte payloads up.
|
||
b.size = bytes == 0 ? 4 : bytes;
|
||
VkBufferCreateInfo bi{};
|
||
bi.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
|
||
bi.size = b.size; bi.usage = usage;
|
||
bi.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||
VK_CHECK(vkCreateBuffer(device, &bi, nullptr, &b.buf));
|
||
VkMemoryRequirements mr;
|
||
vkGetBufferMemoryRequirements(device, b.buf, &mr);
|
||
VkMemoryAllocateInfo mi{};
|
||
mi.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
||
mi.allocationSize = mr.size;
|
||
mi.memoryTypeIndex = find_mem(mr.memoryTypeBits,
|
||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||
VK_CHECK(vkAllocateMemory(device, &mi, nullptr, &b.mem));
|
||
VK_CHECK(vkBindBufferMemory(device, b.buf, b.mem, 0));
|
||
VK_CHECK(vkMapMemory(device, b.mem, 0, b.size, 0, &b.mapped));
|
||
return b;
|
||
};
|
||
|
||
// --- Allocate input buffers + the output buffer + optional codebook ---
|
||
bool needs_codebook = (fx.kernel == "turbo3_tcq");
|
||
uint32_t n_inputs = (uint32_t)kb.inputs.size();
|
||
uint32_t n_bindings = n_inputs + 1 + (needs_codebook ? 1 : 0);
|
||
|
||
std::vector<Buf> in_bufs(n_inputs);
|
||
for (uint32_t i = 0; i < n_inputs; i++) {
|
||
in_bufs[i] = alloc_buf((VkDeviceSize)kb.inputs[i].bytes,
|
||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||
if (kb.inputs[i].bytes > 0) {
|
||
std::memcpy(in_bufs[i].mapped, kb.inputs[i].data, kb.inputs[i].bytes);
|
||
}
|
||
}
|
||
Buf out_buf = alloc_buf((VkDeviceSize)kb.output_bytes,
|
||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||
std::memset(out_buf.mapped, 0, out_buf.size);
|
||
|
||
Buf cb_buf{};
|
||
if (needs_codebook) {
|
||
cb_buf = alloc_buf(512 * sizeof(float), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||
std::memcpy(cb_buf.mapped, ELIZA_TURBO3_TCQ_CODEBOOK, 512 * sizeof(float));
|
||
}
|
||
|
||
// --- Descriptor set layout / pool / set ---
|
||
std::vector<VkDescriptorSetLayoutBinding> dslb(n_bindings);
|
||
for (uint32_t i = 0; i < n_bindings; 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 = n_bindings;
|
||
dslci.pBindings = dslb.data();
|
||
VkDescriptorSetLayout dsl;
|
||
VK_CHECK(vkCreateDescriptorSetLayout(device, &dslci, nullptr, &dsl));
|
||
|
||
VkDescriptorPoolSize dps{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, n_bindings };
|
||
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(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(device, &dsai, &ds));
|
||
|
||
std::vector<VkDescriptorBufferInfo> bi(n_bindings);
|
||
for (uint32_t i = 0; i < n_inputs; i++) {
|
||
bi[i] = { in_bufs[i].buf, 0, VK_WHOLE_SIZE };
|
||
}
|
||
bi[n_inputs] = { out_buf.buf, 0, VK_WHOLE_SIZE };
|
||
if (needs_codebook) bi[n_inputs + 1] = { cb_buf.buf, 0, VK_WHOLE_SIZE };
|
||
std::vector<VkWriteDescriptorSet> wds(n_bindings);
|
||
for (uint32_t i = 0; i < n_bindings; i++) {
|
||
wds[i] = {};
|
||
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 = &bi[i];
|
||
}
|
||
vkUpdateDescriptorSets(device, n_bindings, wds.data(), 0, nullptr);
|
||
|
||
// --- Shader module ---
|
||
auto spv = load_spirv(spv_path);
|
||
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(device, &smci, nullptr, &sm));
|
||
|
||
// --- Pipeline layout w/ push constants ---
|
||
VkPushConstantRange pcr{};
|
||
pcr.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
|
||
pcr.offset = 0;
|
||
pcr.size = (uint32_t)kb.push_bytes.size();
|
||
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(device, &plci, nullptr, &pll));
|
||
|
||
// Specialization constant for the multi-block variants: constant_id 0 is
|
||
// blocks_per_workgroup (turbo*) / tokens_per_workgroup (qjl). One SPV blob,
|
||
// device-tuned at pipeline create — this is exactly the path a runtime
|
||
// would use to pick a per-device value.
|
||
VkSpecializationMapEntry spec_entry{ 0, 0, sizeof(uint32_t) };
|
||
VkSpecializationInfo spec_info{ 1, &spec_entry, sizeof(uint32_t), &multi_per_wg };
|
||
|
||
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.stage.pSpecializationInfo = kernel_is_multi ? &spec_info : nullptr;
|
||
cpci.layout = pll;
|
||
VkPipeline pipeline;
|
||
VK_CHECK(vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &cpci, nullptr, &pipeline));
|
||
|
||
// --- Command buffer ---
|
||
VkCommandPoolCreateInfo cpi{};
|
||
cpi.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
|
||
cpi.queueFamilyIndex = qfam;
|
||
VkCommandPool pool;
|
||
VK_CHECK(vkCreateCommandPool(device, &cpi, nullptr, &pool));
|
||
VkCommandBufferAllocateInfo cbai{};
|
||
cbai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
||
cbai.commandPool = pool;
|
||
cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||
cbai.commandBufferCount = 1;
|
||
VkCommandBuffer cb;
|
||
VK_CHECK(vkAllocateCommandBuffers(device, &cbai, &cb));
|
||
|
||
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));
|
||
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,
|
||
(uint32_t)kb.push_bytes.size(), kb.push_bytes.data());
|
||
vkCmdDispatch(cb, kb.dispatch_x, kb.dispatch_y, kb.dispatch_z);
|
||
VK_CHECK(vkEndCommandBuffer(cb));
|
||
|
||
VkSubmitInfo si{};
|
||
si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
||
si.commandBufferCount = 1; si.pCommandBuffers = &cb;
|
||
VK_CHECK(vkQueueSubmit(queue, 1, &si, VK_NULL_HANDLE));
|
||
VK_CHECK(vkQueueWaitIdle(queue));
|
||
|
||
// --- Compare ---
|
||
const float * out = (const float *)out_buf.mapped;
|
||
int failures = 0;
|
||
int compare_n = (int)kb.n_outputs;
|
||
float max_diff = 0.0f;
|
||
for (int i = 0; i < compare_n; i++) {
|
||
float diff = std::fabs(out[i] - fx.expected_scores[i]);
|
||
if (diff > max_diff) max_diff = diff;
|
||
const char * tag = (diff < tol) ? "PASS" : "FAIL";
|
||
std::printf(" i=%d expected=%+.6f got=%+.6f diff=%.3e %s\n",
|
||
i, (double)fx.expected_scores[i], (double)out[i], (double)diff, tag);
|
||
if (diff >= tol) failures++;
|
||
}
|
||
|
||
std::printf("[vulkan_verify] %s — %d/%d passed (tol=%.0e, max_diff=%.3e)\n",
|
||
failures == 0 ? "PASS" : "FAIL",
|
||
compare_n - failures, compare_n, (double)tol, (double)max_diff);
|
||
return failures == 0 ? 0 : 1;
|
||
}
|