659 lines
23 KiB
C
659 lines
23 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* test_kernel_parity.c — GPU-vs-CPU parity harness for the qwen35moe kernels
|
|
*
|
|
* Runs each new Foundry CUDA kernel (Q3_K matvec, partial NEOX RoPE,
|
|
* QK-RMSNorm, MoE routing) through the fc_cuda dlopen backend and diffs
|
|
* its output against the trusted CPU-reference golden vectors in
|
|
* the tests/golden directory. Prints, per kernel:
|
|
*
|
|
* [PASS]/[FAIL] <kernel> max_abs_err=... max_rel_err=...
|
|
*
|
|
* MoE top-k indices are integer and must match EXACTLY (golden README).
|
|
*
|
|
* Gated on fc_available(): with no CUDA driver (e.g. this Mac) it prints
|
|
* "[SKIP] no CUDA" and exits 0, so the harness compiles, links, and runs
|
|
* everywhere; the real GPU diffs happen when the operator runs it on the
|
|
* K80 host.
|
|
*
|
|
* Golden format (tests/golden/README.md):
|
|
* line 1: "PNGOLDEN1 <name> <dtype> <count> seed=0x9E3779B9\n"
|
|
* then: <count> little-endian <dtype> values (f32 / i32 / u8).
|
|
*
|
|
* @status GPU diffs pending K80 host; SKIP path validated on Mac.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include "cuda/fc_cuda.h"
|
|
|
|
#include <ctype.h>
|
|
#include <math.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include "barswap/barswap.h"
|
|
|
|
/* Bring nvidia up after a die is acquired (it's blacklisted; each submodule
|
|
* needs --ignore-install). Mirrors compute_persist's load_nvidia. */
|
|
static int parity_load_nvidia(void)
|
|
{
|
|
if (system("modprobe --ignore-install nvidia 2>/dev/null") != 0) return -1;
|
|
sleep(1);
|
|
if (system("modprobe --ignore-install nvidia_uvm 2>/dev/null") != 0) return -1;
|
|
sleep(1);
|
|
return 0;
|
|
}
|
|
|
|
/* qwen35moe config constants (golden README "Config constants used"). */
|
|
#define PARITY_N_EMBD 2048
|
|
#define PARITY_N_HEAD 16
|
|
#define PARITY_N_HEAD_KV 2
|
|
#define PARITY_HEAD_DIM 256
|
|
#define PARITY_ROPE_DIM 64
|
|
#define PARITY_ROPE_THETA 1e7f
|
|
#define PARITY_RMS_NORM_EPS 1e-6f
|
|
#define PARITY_N_EXPERT 256
|
|
#define PARITY_EXPERT_USED 8
|
|
#define PARITY_EXPERT_WT_SCALE 1.0f
|
|
|
|
/* Q3_K matvec test shape (golden README Kernel 1). */
|
|
#define PARITY_Q3K_ROWS 24
|
|
#define PARITY_Q3K_COLS 1024
|
|
#define PARITY_Q3K_BLOCK_BYTES 110 /* sizeof(pn_block_q3_k) — packed, no pad */
|
|
|
|
/* Gated-Delta-Net SSM dimensions (golden README Kernels 5-7). */
|
|
#define PARITY_SSM_N_V_HEADS 32
|
|
#define PARITY_SSM_STATE_SIZE 128
|
|
#define PARITY_SSM_CONV_DIM 8192
|
|
#define PARITY_SSM_CONV_KERNEL 4
|
|
|
|
/* Tolerances (golden README). fp32 trig/softmax on Kepler. */
|
|
#define PARITY_REL_TOL 1e-3f
|
|
|
|
#define GOLDEN_DIR "tests/golden/"
|
|
|
|
/* ============================================================
|
|
* Golden-vector loader
|
|
* ============================================================ */
|
|
|
|
/* A loaded golden file: the payload past the PNGOLDEN1 header line.
|
|
* `count` is the element count from the header; `bytes` is the raw
|
|
* payload length. Ownership: caller frees `data`. */
|
|
typedef struct {
|
|
char name[64];
|
|
char dtype[8];
|
|
size_t count;
|
|
size_t bytes;
|
|
void *data;
|
|
} golden_vec;
|
|
|
|
/* sizeof one element for a golden dtype. Returns 0 for unknown dtype. */
|
|
static size_t golden_dtype_size(const char *dtype)
|
|
{
|
|
if (strcmp(dtype, "f32") == 0) return 4;
|
|
if (strcmp(dtype, "i32") == 0) return 4;
|
|
if (strcmp(dtype, "u8") == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
/* Load a golden .bin (header line + little-endian payload) from GOLDEN_DIR.
|
|
* Returns 0 on success and fills *out; caller frees out->data. The host is
|
|
* assumed little-endian (x86_64 / aarch64), matching the file format. */
|
|
static int golden_load(const char *filename, golden_vec *out)
|
|
{
|
|
char path[256];
|
|
int n = snprintf(path, sizeof(path), "%s%s", GOLDEN_DIR, filename);
|
|
if (n < 0 || (size_t)n >= sizeof(path))
|
|
return -1;
|
|
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) {
|
|
fprintf(stderr, "[parity] cannot open %s\n", path);
|
|
return -1;
|
|
}
|
|
|
|
memset(out, 0, sizeof(*out));
|
|
int rc = -1;
|
|
|
|
/* Header line: PNGOLDEN1 <name> <dtype> <count> seed=0x... */
|
|
char magic[16] = {0};
|
|
unsigned long count = 0;
|
|
if (fscanf(f, "%15s %63s %7s %lu", magic, out->name, out->dtype, &count) != 4)
|
|
goto cleanup;
|
|
if (strcmp(magic, "PNGOLDEN1") != 0)
|
|
goto cleanup;
|
|
|
|
/* Consume the rest of the header line up to and including the newline. */
|
|
int c;
|
|
while ((c = fgetc(f)) != '\n' && c != EOF)
|
|
;
|
|
|
|
size_t elem = golden_dtype_size(out->dtype);
|
|
if (elem == 0)
|
|
goto cleanup;
|
|
|
|
out->count = (size_t)count;
|
|
out->bytes = out->count * elem;
|
|
out->data = malloc(out->bytes ? out->bytes : 1);
|
|
if (!out->data)
|
|
goto cleanup;
|
|
|
|
if (fread(out->data, 1, out->bytes, f) != out->bytes)
|
|
goto cleanup;
|
|
|
|
rc = 0;
|
|
|
|
cleanup:
|
|
fclose(f);
|
|
if (rc != 0) {
|
|
free(out->data);
|
|
out->data = NULL;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static void golden_free(golden_vec *g)
|
|
{
|
|
if (g) {
|
|
free(g->data);
|
|
g->data = NULL;
|
|
}
|
|
}
|
|
|
|
/* ============================================================
|
|
* Comparison + reporting
|
|
* ============================================================ */
|
|
|
|
static int g_failures = 0;
|
|
|
|
/* Max abs and max relative error between two f32 arrays. rel uses
|
|
* |a-b| / max(|gold|, tiny) so near-zero golden values do not blow up. */
|
|
static void f32_errors(const float *got, const float *gold, size_t n,
|
|
float *max_abs, float *max_rel)
|
|
{
|
|
float ma = 0.0f, mr = 0.0f;
|
|
for (size_t i = 0; i < n; i++) {
|
|
float diff = fabsf(got[i] - gold[i]);
|
|
if (diff > ma) ma = diff;
|
|
float denom = fabsf(gold[i]);
|
|
float rel = diff / (denom > 1e-6f ? denom : 1e-6f);
|
|
if (rel > mr) mr = rel;
|
|
}
|
|
*max_abs = ma;
|
|
*max_rel = mr;
|
|
}
|
|
|
|
/* Absolute-error floor: an element this close in absolute terms is correct
|
|
* regardless of its relative error (avoids near-zero-denominator inflation,
|
|
* e.g. a ~2e-5 state element with a 2e-8 fp32 reduction-order diff). */
|
|
#define PARITY_ABS_TOL 1e-5f
|
|
|
|
/* Report an f32 kernel result and tally failures. Pass = within EITHER the
|
|
* relative OR the absolute tolerance — the standard numerical-parity criterion. */
|
|
static void report_f32(const char *kernel, const float *got,
|
|
const float *gold, size_t n, float rel_tol)
|
|
{
|
|
float max_abs, max_rel;
|
|
f32_errors(got, gold, n, &max_abs, &max_rel);
|
|
bool pass = (max_rel <= rel_tol) || (max_abs <= PARITY_ABS_TOL);
|
|
if (!pass) g_failures++;
|
|
printf(" [%s] %-22s max_abs_err=%.3e max_rel_err=%.3e\n",
|
|
pass ? "PASS" : "FAIL", kernel, (double)max_abs, (double)max_rel);
|
|
}
|
|
|
|
/* ============================================================
|
|
* Per-kernel parity tests
|
|
* ============================================================ */
|
|
|
|
/* Kernel 1: Q3_K dequant + matvec. Loads the raw quantized weight bytes,
|
|
* the fp32 input vector, and the golden output; runs fc_matvec_q3k and
|
|
* diffs. row_stride = (cols/256) * 110, matching the CPU pn_qmv layout. */
|
|
static void test_q3k_matvec(void)
|
|
{
|
|
golden_vec w = {0}, x = {0}, out = {0};
|
|
if (golden_load("q3k_matvec_weight.bin", &w) != 0 ||
|
|
golden_load("q3k_matvec_input.bin", &x) != 0 ||
|
|
golden_load("q3k_matvec_output.bin", &out) != 0) {
|
|
fprintf(stderr, "[parity] q3k_matvec: golden load failed\n");
|
|
g_failures++;
|
|
goto cleanup;
|
|
}
|
|
|
|
int rows = PARITY_Q3K_ROWS;
|
|
int cols = PARITY_Q3K_COLS;
|
|
int row_stride = (cols / 256) * PARITY_Q3K_BLOCK_BYTES;
|
|
|
|
fc_buffer *d_w = fc_buffer_from_ptr(w.data, w.bytes);
|
|
fc_buffer *d_x = fc_buffer_from_ptr(x.data, x.bytes);
|
|
fc_buffer *d_out = fc_buffer_alloc((size_t)rows * sizeof(float));
|
|
|
|
fc_matvec_q3k(d_w, d_x, d_out, rows, cols, row_stride);
|
|
fc_sync();
|
|
|
|
float *got = malloc((size_t)rows * sizeof(float));
|
|
fc_buffer_to_cpu(d_out, got);
|
|
report_f32("q3k_matvec", got, (const float *)out.data, (size_t)rows,
|
|
PARITY_REL_TOL);
|
|
|
|
free(got);
|
|
fc_buffer_free(d_w);
|
|
fc_buffer_free(d_x);
|
|
fc_buffer_free(d_out);
|
|
|
|
cleanup:
|
|
golden_free(&w);
|
|
golden_free(&x);
|
|
golden_free(&out);
|
|
}
|
|
|
|
/* Rotate one tensor at one position and diff against its golden.
|
|
* data is rotated in place on the GPU, so it is re-uploaded per call. */
|
|
static void rope_one(const char *kernel, const float *input, size_t count,
|
|
int n_heads, int pos, const char *gold_file)
|
|
{
|
|
golden_vec gold = {0};
|
|
if (golden_load(gold_file, &gold) != 0) {
|
|
fprintf(stderr, "[parity] rope: golden load %s failed\n", gold_file);
|
|
g_failures++;
|
|
return;
|
|
}
|
|
|
|
fc_buffer *d_data = fc_buffer_from_ptr(input, count * sizeof(float));
|
|
fc_rope_neox_partial(d_data, n_heads, PARITY_HEAD_DIM, PARITY_ROPE_DIM,
|
|
pos, PARITY_ROPE_THETA);
|
|
fc_sync();
|
|
|
|
float *got = malloc(count * sizeof(float));
|
|
fc_buffer_to_cpu(d_data, got);
|
|
report_f32(kernel, got, (const float *)gold.data, count, PARITY_REL_TOL);
|
|
|
|
free(got);
|
|
fc_buffer_free(d_data);
|
|
golden_free(&gold);
|
|
}
|
|
|
|
/* Kernel 2: partial NEOX RoPE. Q (16 heads) and K (2 heads) at a couple of
|
|
* positions, including pos 0 (exact identity). */
|
|
static void test_rope(void)
|
|
{
|
|
golden_vec qin = {0}, kin = {0};
|
|
if (golden_load("rope_q_input.bin", &qin) != 0 ||
|
|
golden_load("rope_k_input.bin", &kin) != 0) {
|
|
fprintf(stderr, "[parity] rope: input load failed\n");
|
|
g_failures++;
|
|
goto cleanup;
|
|
}
|
|
|
|
rope_one("rope_q_pos0", (const float *)qin.data, qin.count,
|
|
PARITY_N_HEAD, 0, "rope_q_pos0.bin");
|
|
rope_one("rope_q_pos100", (const float *)qin.data, qin.count,
|
|
PARITY_N_HEAD, 100, "rope_q_pos100.bin");
|
|
rope_one("rope_k_pos0", (const float *)kin.data, kin.count,
|
|
PARITY_N_HEAD_KV, 0, "rope_k_pos0.bin");
|
|
rope_one("rope_k_pos100", (const float *)kin.data, kin.count,
|
|
PARITY_N_HEAD_KV, 100, "rope_k_pos100.bin");
|
|
|
|
cleanup:
|
|
golden_free(&qin);
|
|
golden_free(&kin);
|
|
}
|
|
|
|
/* Kernel 3: QK-RMSNorm. Per-head RMS over head_dim with the shared weight,
|
|
* applied in place to the 16-head Q layout. */
|
|
static void test_qk_rms_norm(void)
|
|
{
|
|
golden_vec in = {0}, weight = {0}, out = {0};
|
|
if (golden_load("qknorm_input.bin", &in) != 0 ||
|
|
golden_load("qknorm_weight.bin", &weight) != 0 ||
|
|
golden_load("qknorm_output.bin", &out) != 0) {
|
|
fprintf(stderr, "[parity] qknorm: golden load failed\n");
|
|
g_failures++;
|
|
goto cleanup;
|
|
}
|
|
|
|
fc_buffer *d_data = fc_buffer_from_ptr(in.data, in.bytes);
|
|
fc_buffer *d_weight = fc_buffer_from_ptr(weight.data, weight.bytes);
|
|
|
|
fc_qk_rms_norm(d_data, d_weight, PARITY_N_HEAD, PARITY_HEAD_DIM,
|
|
PARITY_RMS_NORM_EPS);
|
|
fc_sync();
|
|
|
|
float *got = malloc(in.bytes);
|
|
fc_buffer_to_cpu(d_data, got);
|
|
report_f32("qk_rms_norm", got, (const float *)out.data, in.count,
|
|
PARITY_REL_TOL);
|
|
|
|
free(got);
|
|
fc_buffer_free(d_data);
|
|
fc_buffer_free(d_weight);
|
|
|
|
cleanup:
|
|
golden_free(&in);
|
|
golden_free(&weight);
|
|
golden_free(&out);
|
|
}
|
|
|
|
/* Kernel 4a: MoE router top-k. Indices must match EXACTLY; weights within
|
|
* tolerance. fc_moe_router_topk overwrites logits in place with the softmax
|
|
* probs, so a fresh copy is uploaded. */
|
|
static void test_moe_router(void)
|
|
{
|
|
golden_vec logits = {0}, idx_gold = {0}, wt_gold = {0};
|
|
if (golden_load("moe_router_logits.bin", &logits) != 0 ||
|
|
golden_load("moe_topk_indices.bin", &idx_gold) != 0 ||
|
|
golden_load("moe_topk_weights.bin", &wt_gold) != 0) {
|
|
fprintf(stderr, "[parity] moe_router: golden load failed\n");
|
|
g_failures++;
|
|
goto cleanup;
|
|
}
|
|
|
|
int k = PARITY_EXPERT_USED;
|
|
fc_buffer *d_logits = fc_buffer_from_ptr(logits.data, logits.bytes);
|
|
fc_buffer *d_idx = fc_buffer_alloc((size_t)k * sizeof(int32_t));
|
|
fc_buffer *d_wt = fc_buffer_alloc((size_t)k * sizeof(float));
|
|
|
|
fc_moe_router_topk(d_logits, d_idx, d_wt, PARITY_N_EXPERT, k,
|
|
PARITY_EXPERT_WT_SCALE);
|
|
fc_sync();
|
|
|
|
int32_t *got_idx = malloc((size_t)k * sizeof(int32_t));
|
|
float *got_wt = malloc((size_t)k * sizeof(float));
|
|
fc_buffer_to_cpu(d_idx, got_idx);
|
|
fc_buffer_to_cpu(d_wt, got_wt);
|
|
|
|
/* Indices: exact integer match. */
|
|
const int32_t *gold_idx = (const int32_t *)idx_gold.data;
|
|
int idx_mismatch = 0;
|
|
for (int i = 0; i < k; i++)
|
|
if (got_idx[i] != gold_idx[i]) idx_mismatch++;
|
|
if (idx_mismatch) g_failures++;
|
|
printf(" [%s] %-22s mismatches=%d/%d\n",
|
|
idx_mismatch ? "FAIL" : "PASS", "moe_topk_indices",
|
|
idx_mismatch, k);
|
|
|
|
/* Weights: relative tolerance. */
|
|
report_f32("moe_topk_weights", got_wt, (const float *)wt_gold.data,
|
|
(size_t)k, PARITY_REL_TOL);
|
|
|
|
free(got_idx);
|
|
free(got_wt);
|
|
fc_buffer_free(d_logits);
|
|
fc_buffer_free(d_idx);
|
|
fc_buffer_free(d_wt);
|
|
|
|
cleanup:
|
|
golden_free(&logits);
|
|
golden_free(&idx_gold);
|
|
golden_free(&wt_gold);
|
|
}
|
|
|
|
/* Kernel 4b: shared-expert sigmoid gate. The host computes the gate logit
|
|
* (gate_w . x) — exactly as the CPU path does — then fc_moe_shared_gate
|
|
* applies sigmoid in place. Seeding shexp_out with 1.0 makes element 0
|
|
* read back as sigmoid(gate_logit), the golden scalar. */
|
|
static void test_moe_shared_gate(void)
|
|
{
|
|
golden_vec gw = {0}, gx = {0}, gscalar = {0};
|
|
if (golden_load("moe_shared_gate_weight.bin", &gw) != 0 ||
|
|
golden_load("moe_shared_gate_input.bin", &gx) != 0 ||
|
|
golden_load("moe_shared_gate_scalar.bin", &gscalar) != 0) {
|
|
fprintf(stderr, "[parity] moe_shared_gate: golden load failed\n");
|
|
g_failures++;
|
|
goto cleanup;
|
|
}
|
|
|
|
const float *w = (const float *)gw.data;
|
|
const float *x = (const float *)gx.data;
|
|
float gate_logit = 0.0f;
|
|
for (size_t i = 0; i < gw.count; i++)
|
|
gate_logit += w[i] * x[i];
|
|
|
|
int n = PARITY_N_EMBD;
|
|
float *ones = malloc((size_t)n * sizeof(float));
|
|
for (int i = 0; i < n; i++)
|
|
ones[i] = 1.0f;
|
|
|
|
fc_buffer *d_out = fc_buffer_from_ptr(ones, (size_t)n * sizeof(float));
|
|
fc_moe_shared_gate(d_out, gate_logit, n);
|
|
fc_sync();
|
|
|
|
float got = 0.0f;
|
|
fc_buffer_to_cpu(d_out, ones); /* ones now holds the gated output */
|
|
got = ones[0];
|
|
report_f32("moe_shared_gate", &got, (const float *)gscalar.data, 1,
|
|
PARITY_REL_TOL);
|
|
|
|
free(ones);
|
|
fc_buffer_free(d_out);
|
|
|
|
cleanup:
|
|
golden_free(&gw);
|
|
golden_free(&gx);
|
|
golden_free(&gscalar);
|
|
}
|
|
|
|
/* Kernel 5: SSM per-token delta-rule recurrence. Uploads the initial state,
|
|
* q/k/v, g/beta; runs fc_ssm_recurrence (updates state in place + writes y);
|
|
* diffs both the output y and the updated state against the goldens. */
|
|
static void test_ssm_recurrence(void)
|
|
{
|
|
golden_vec s_in = {0}, q = {0}, k = {0}, v = {0}, g = {0}, beta = {0};
|
|
golden_vec y_gold = {0}, s_out_gold = {0};
|
|
if (golden_load("ssm_recur_state_in.bin", &s_in) != 0 ||
|
|
golden_load("ssm_recur_q.bin", &q) != 0 ||
|
|
golden_load("ssm_recur_k.bin", &k) != 0 ||
|
|
golden_load("ssm_recur_v.bin", &v) != 0 ||
|
|
golden_load("ssm_recur_g.bin", &g) != 0 ||
|
|
golden_load("ssm_recur_beta.bin", &beta) != 0 ||
|
|
golden_load("ssm_recur_y.bin", &y_gold) != 0 ||
|
|
golden_load("ssm_recur_state_out.bin", &s_out_gold) != 0) {
|
|
fprintf(stderr, "[parity] ssm_recurrence: golden load failed\n");
|
|
g_failures++;
|
|
goto cleanup;
|
|
}
|
|
|
|
int H = PARITY_SSM_N_V_HEADS;
|
|
int S = PARITY_SSM_STATE_SIZE;
|
|
float scale = 1.0f / sqrtf((float)S);
|
|
|
|
fc_buffer *d_s = fc_buffer_from_ptr(s_in.data, s_in.bytes);
|
|
fc_buffer *d_q = fc_buffer_from_ptr(q.data, q.bytes);
|
|
fc_buffer *d_k = fc_buffer_from_ptr(k.data, k.bytes);
|
|
fc_buffer *d_v = fc_buffer_from_ptr(v.data, v.bytes);
|
|
fc_buffer *d_g = fc_buffer_from_ptr(g.data, g.bytes);
|
|
fc_buffer *d_beta = fc_buffer_from_ptr(beta.data, beta.bytes);
|
|
fc_buffer *d_y = fc_buffer_alloc((size_t)H * S * sizeof(float));
|
|
|
|
fc_ssm_recurrence(d_s, d_q, d_k, d_v, d_g, d_beta, d_y, H, S, scale);
|
|
fc_sync();
|
|
|
|
float *got_y = malloc((size_t)H * S * sizeof(float));
|
|
float *got_s = malloc(s_in.bytes);
|
|
fc_buffer_to_cpu(d_y, got_y);
|
|
fc_buffer_to_cpu(d_s, got_s); /* state was updated in place */
|
|
report_f32("ssm_recur_y", got_y, (const float *)y_gold.data,
|
|
(size_t)H * S, PARITY_REL_TOL);
|
|
report_f32("ssm_recur_state", got_s, (const float *)s_out_gold.data,
|
|
s_out_gold.count, PARITY_REL_TOL);
|
|
|
|
free(got_y);
|
|
free(got_s);
|
|
fc_buffer_free(d_s);
|
|
fc_buffer_free(d_q);
|
|
fc_buffer_free(d_k);
|
|
fc_buffer_free(d_v);
|
|
fc_buffer_free(d_g);
|
|
fc_buffer_free(d_beta);
|
|
fc_buffer_free(d_y);
|
|
|
|
cleanup:
|
|
golden_free(&s_in);
|
|
golden_free(&q);
|
|
golden_free(&k);
|
|
golden_free(&v);
|
|
golden_free(&g);
|
|
golden_free(&beta);
|
|
golden_free(&y_gold);
|
|
golden_free(&s_out_gold);
|
|
}
|
|
|
|
/* Kernel 6: SSM causal conv1d + SiLU. Uploads qkv, weight, conv_state; runs
|
|
* fc_ssm_conv1d_silu (writes out, rolls conv_state in place); diffs out and
|
|
* the rolled conv_state against the goldens. */
|
|
static void test_ssm_conv1d(void)
|
|
{
|
|
golden_vec qkv = {0}, w = {0}, st_in = {0}, out_gold = {0}, st_out_gold = {0};
|
|
if (golden_load("ssm_conv_qkv.bin", &qkv) != 0 ||
|
|
golden_load("ssm_conv_weight.bin", &w) != 0 ||
|
|
golden_load("ssm_conv_state_in.bin", &st_in) != 0 ||
|
|
golden_load("ssm_conv_out.bin", &out_gold) != 0 ||
|
|
golden_load("ssm_conv_state_out.bin", &st_out_gold) != 0) {
|
|
fprintf(stderr, "[parity] ssm_conv1d: golden load failed\n");
|
|
g_failures++;
|
|
goto cleanup;
|
|
}
|
|
|
|
int C = PARITY_SSM_CONV_DIM;
|
|
int kk = PARITY_SSM_CONV_KERNEL;
|
|
|
|
fc_buffer *d_qkv = fc_buffer_from_ptr(qkv.data, qkv.bytes);
|
|
fc_buffer *d_w = fc_buffer_from_ptr(w.data, w.bytes);
|
|
fc_buffer *d_state = fc_buffer_from_ptr(st_in.data, st_in.bytes);
|
|
fc_buffer *d_out = fc_buffer_alloc((size_t)C * sizeof(float));
|
|
|
|
fc_ssm_conv1d_silu(d_qkv, d_w, d_state, d_out, C, kk);
|
|
fc_sync();
|
|
|
|
float *got_out = malloc((size_t)C * sizeof(float));
|
|
float *got_state = malloc(st_in.bytes);
|
|
fc_buffer_to_cpu(d_out, got_out);
|
|
fc_buffer_to_cpu(d_state, got_state); /* conv_state rolled in place */
|
|
report_f32("ssm_conv_out", got_out, (const float *)out_gold.data,
|
|
(size_t)C, PARITY_REL_TOL);
|
|
report_f32("ssm_conv_state", got_state, (const float *)st_out_gold.data,
|
|
st_out_gold.count, PARITY_REL_TOL);
|
|
|
|
free(got_out);
|
|
free(got_state);
|
|
fc_buffer_free(d_qkv);
|
|
fc_buffer_free(d_w);
|
|
fc_buffer_free(d_state);
|
|
fc_buffer_free(d_out);
|
|
|
|
cleanup:
|
|
golden_free(&qkv);
|
|
golden_free(&w);
|
|
golden_free(&st_in);
|
|
golden_free(&out_gold);
|
|
golden_free(&st_out_gold);
|
|
}
|
|
|
|
/* Kernel 7: SSM gated RMSNorm. Uploads y, weight, z; runs fc_ssm_gated_norm
|
|
* (writes gated); diffs gated against the golden. */
|
|
static void test_ssm_gated_norm(void)
|
|
{
|
|
golden_vec y = {0}, w = {0}, z = {0}, out_gold = {0};
|
|
if (golden_load("ssm_gated_y.bin", &y) != 0 ||
|
|
golden_load("ssm_gated_weight.bin", &w) != 0 ||
|
|
golden_load("ssm_gated_z.bin", &z) != 0 ||
|
|
golden_load("ssm_gated_out.bin", &out_gold) != 0) {
|
|
fprintf(stderr, "[parity] ssm_gated_norm: golden load failed\n");
|
|
g_failures++;
|
|
goto cleanup;
|
|
}
|
|
|
|
int H = PARITY_SSM_N_V_HEADS;
|
|
int S = PARITY_SSM_STATE_SIZE;
|
|
|
|
fc_buffer *d_y = fc_buffer_from_ptr(y.data, y.bytes);
|
|
fc_buffer *d_w = fc_buffer_from_ptr(w.data, w.bytes);
|
|
fc_buffer *d_z = fc_buffer_from_ptr(z.data, z.bytes);
|
|
fc_buffer *d_gated = fc_buffer_alloc((size_t)H * S * sizeof(float));
|
|
|
|
fc_ssm_gated_norm(d_y, d_w, d_z, d_gated, H, S, PARITY_RMS_NORM_EPS);
|
|
fc_sync();
|
|
|
|
float *got = malloc((size_t)H * S * sizeof(float));
|
|
fc_buffer_to_cpu(d_gated, got);
|
|
report_f32("ssm_gated_norm", got, (const float *)out_gold.data,
|
|
(size_t)H * S, PARITY_REL_TOL);
|
|
|
|
free(got);
|
|
fc_buffer_free(d_y);
|
|
fc_buffer_free(d_w);
|
|
fc_buffer_free(d_z);
|
|
fc_buffer_free(d_gated);
|
|
|
|
cleanup:
|
|
golden_free(&y);
|
|
golden_free(&w);
|
|
golden_free(&z);
|
|
golden_free(&out_gold);
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
/* Unbuffered so a hung kernel can't swallow the last line printed before
|
|
* the wedge (a buffered run lost everything on the K80). */
|
|
setvbuf(stdout, NULL, _IONBF, 0);
|
|
setvbuf(stderr, NULL, _IONBF, 0);
|
|
|
|
printf("=== qwen35moe kernel parity harness ===\n");
|
|
|
|
/* Args: [die_id] [stage]
|
|
* - a leading numeric arg = barswap die to acquire + bring nvidia up on
|
|
* (self-contained; no compute_persist needed to set up the die).
|
|
* - stage (optional) runs exactly one of: init q3k rope qknorm
|
|
* moe_router moe_gate, to pinpoint a hang without touching the rest. */
|
|
int ai = 1;
|
|
int die_id = -1;
|
|
if (argc > ai && isdigit((unsigned char)argv[ai][0]))
|
|
die_id = atoi(argv[ai++]);
|
|
const char *only = (argc > ai) ? argv[ai] : NULL;
|
|
|
|
if (die_id >= 0) {
|
|
printf("[step] bs_acquire die %d ...\n", die_id);
|
|
bs_context *bs = bs_init();
|
|
if (!bs) { fprintf(stderr, "bs_init failed — is bar_swap.ko loaded?\n"); _Exit(1); }
|
|
if (!bs_acquire(bs, die_id)) { fprintf(stderr, "bs_acquire(%d) failed\n", die_id); _Exit(1); }
|
|
printf("[step] die %d acquired; loading nvidia ...\n", die_id);
|
|
if (parity_load_nvidia() != 0) { fprintf(stderr, "nvidia load failed\n"); _Exit(1); }
|
|
printf("[step] nvidia up\n");
|
|
}
|
|
|
|
printf("[step] fc_init ...\n");
|
|
if (fc_init() != 0 || !fc_available()) {
|
|
printf("[SKIP] no CUDA — host compile/link OK, GPU diffs deferred "
|
|
"to a K80 host\n");
|
|
return 0;
|
|
}
|
|
printf("[step] fc_init OK, CUDA available\n");
|
|
/* NOTE: on the barswap K80 rig, CUDA's atexit context teardown hangs a
|
|
* thread (zombies the process, half-unloads nvidia). So we DON'T call
|
|
* fc_free()/return — we _Exit() to let the kernel reclaim the context on
|
|
* process death, skipping the wedging userspace teardown. */
|
|
if (only && strcmp(only, "init") == 0) { printf("[step] init-only done\n"); fflush(stdout); _Exit(0); }
|
|
|
|
if (!only || !strcmp(only, "q3k")) { printf("[step] q3k_matvec ...\n"); test_q3k_matvec(); }
|
|
if (!only || !strcmp(only, "rope")) { printf("[step] rope ...\n"); test_rope(); }
|
|
if (!only || !strcmp(only, "qknorm")) { printf("[step] qk_rms_norm ...\n"); test_qk_rms_norm(); }
|
|
if (!only || !strcmp(only, "moe_router")) { printf("[step] moe_router ...\n"); test_moe_router(); }
|
|
if (!only || !strcmp(only, "moe_gate")) { printf("[step] moe_shared_gate ...\n"); test_moe_shared_gate(); }
|
|
if (!only || !strcmp(only, "ssm")) { printf("[step] ssm_recurrence ...\n"); test_ssm_recurrence(); }
|
|
if (!only || !strcmp(only, "ssm_conv")) { printf("[step] ssm_conv1d ...\n"); test_ssm_conv1d(); }
|
|
if (!only || !strcmp(only, "ssm_gated")) { printf("[step] ssm_gated_norm ...\n"); test_ssm_gated_norm(); }
|
|
|
|
printf("=== %s (%d failure%s) ===\n",
|
|
g_failures ? "FAIL" : "PASS", g_failures,
|
|
g_failures == 1 ? "" : "s");
|
|
/* _Exit (not fc_free/return): skip the CUDA atexit teardown that hangs a
|
|
* thread on this barswap rig. Kernel reclaims the context on process death. */
|
|
fflush(stdout);
|
|
_Exit(g_failures ? 1 : 0);
|
|
}
|