Files
nvfp4-megamoe-kernel/tests/unit/test_fmha_6warp_multihead.cu

407 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Test multi-head FMHA kernel (6-warp, grid launch).
* Compile with -DHD_VAL=64 etc.
*
* Tests:
* 1. Multi-head MHA: n_h independent heads, each with its own K/V
* 2. Multi-head MQA: n_h Q heads sharing 1 K/V head
* 3. Batched: batch_size > 1
* 4. LSE output correctness
*/
#include <cuda_runtime.h>
#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <cstring>
#ifndef HD_VAL
#define HD_VAL 64
#endif
#include "dsv4/kernels/attention/fmha_common.cuh"
#include "dsv4/kernels/attention/fmha_umma_desc.cuh"
using namespace dsv4::kernels::attention;
static bf16_t f32_to_bf16_host(float f) { uint32_t u; memcpy(&u,&f,4); return (uint16_t)(u>>16); }
static float bf16_to_f32_host(bf16_t h) { uint32_t u=(uint32_t)h<<16; float f; memcpy(&f,&u,4); return f; }
constexpr int HD = HD_VAL;
constexpr int SK = 128;
constexpr int TILE_SZ = 128 * MMA_K_BF16;
constexpr int V_SUB_SZ = 256;
constexpr int TMEM_N = (HD <= 128) ? 128 : 256;
#include "dsv4/kernels/attention/fmha_6warp_multihead.cuh"
// Reference: compute attention for one head
static void reference_attention(
const bf16_t* q, const bf16_t* k, const bf16_t* v,
float* o_ref, float* lse_ref,
int hd, int s_k, float scale
) {
float s[512]; // max s_k
for (int j = 0; j < s_k; j++) {
float dot = 0.0f;
for (int d = 0; d < hd; d++) dot += bf16_to_f32_host(q[d]) * bf16_to_f32_host(k[j * hd + d]);
s[j] = dot * scale;
}
float mx = -INFINITY;
for (int j = 0; j < s_k; j++) mx = fmaxf(mx, s[j]);
float sm = 0.0f;
for (int j = 0; j < s_k; j++) { s[j] = expf(s[j] - mx); sm += s[j]; }
for (int j = 0; j < s_k; j++) s[j] /= sm;
for (int d = 0; d < hd; d++) {
float ov = 0.0f;
for (int int_j = 0; int_j < s_k; int_j++) ov += s[int_j] * bf16_to_f32_host(v[d * s_k + int_j]);
o_ref[d] = ov;
}
if (lse_ref) *lse_ref = logf(sm) + mx;
}
static int test_mha(int n_h) {
printf("\n=== Test MHA: n_h=%d, HD=%d, SK=%d ===\n", n_h, HD, SK);
const float SCALE = 1.0f / sqrtf((float)HD);
int pass = 1;
// Allocate host tensors: Q(n_h, hd), K(n_h, SK*hd), V(n_h, hd*SK), O(n_h, hd)
bf16_t* h_q = (bf16_t*)malloc(n_h * HD * sizeof(bf16_t));
bf16_t* h_k = (bf16_t*)malloc(n_h * SK * HD * sizeof(bf16_t));
bf16_t* h_v = (bf16_t*)malloc(n_h * HD * SK * sizeof(bf16_t));
bf16_t* h_o = (bf16_t*)calloc(n_h * HD, sizeof(bf16_t));
float* h_lse = (float*)calloc(n_h, sizeof(float));
srand(42);
for (int i = 0; i < n_h * HD; i++) h_q[i] = f32_to_bf16_host((float)(rand()%100)/100.0f - 0.5f);
for (int i = 0; i < n_h * SK * HD; i++) h_k[i] = f32_to_bf16_host((float)(rand()%100)/100.0f - 0.5f);
for (int i = 0; i < n_h * HD * SK; i++) h_v[i] = f32_to_bf16_host((float)(rand()%100)/100.0f - 0.5f);
bf16_t *d_q, *d_k, *d_v, *d_o;
float *d_lse;
cudaMalloc(&d_q, n_h * HD * sizeof(bf16_t));
cudaMalloc(&d_k, n_h * SK * HD * sizeof(bf16_t));
cudaMalloc(&d_v, n_h * HD * SK * sizeof(bf16_t));
cudaMalloc(&d_o, n_h * HD * sizeof(bf16_t));
cudaMalloc(&d_lse, n_h * sizeof(float));
cudaMemcpy(d_q, h_q, n_h * HD * sizeof(bf16_t), cudaMemcpyHostToDevice);
cudaMemcpy(d_k, h_k, n_h * SK * HD * sizeof(bf16_t), cudaMemcpyHostToDevice);
cudaMemcpy(d_v, h_v, n_h * HD * SK * sizeof(bf16_t), cudaMemcpyHostToDevice);
cudaMemset(d_o, 0, n_h * HD * sizeof(bf16_t));
cudaMemset(d_lse, 0, n_h * sizeof(float));
FmhaParams params;
params.q = d_q;
params.k = d_k;
params.v = d_v;
params.o = d_o;
params.lse = d_lse;
params.s_k = SK;
params.scale = SCALE;
params.head_dim = HD;
params.q_head_stride = HD; // T=1, stride = 1 * hd
params.q_batch_stride = n_h * HD;
params.k_head_stride = SK * HD; // each head has its own K
params.k_batch_stride = n_h * SK * HD;
params.v_head_stride = HD * SK; // each head has its own V
params.v_batch_stride = n_h * HD * SK;
params.o_head_stride = HD;
params.o_batch_stride = n_h * HD;
params.lse_head_stride = 1;
params.lse_batch_stride = n_h;
int smem = (4 + 8 + 16 + TILE_SZ*2 + TILE_SZ*2 + TILE_SZ*2 + V_SUB_SZ*2 + SK*4 + 256 + 127) & ~127;
if (smem > 48 * 1024) {
cudaFuncSetAttribute(fmha_6warp_multihead_kernel<HD>, cudaFuncAttributeMaxDynamicSharedMemorySize, smem);
}
fmha_6warp_multihead_kernel<HD><<<dim3(1, n_h, 1), 192, smem>>>(params);
cudaError_t launch_err = cudaGetLastError();
cudaError_t sync_err = cudaSuccess;
if (launch_err != cudaSuccess) {
printf("LAUNCH ERROR: %s\n", cudaGetErrorString(launch_err));
pass = 0; goto cleanup;
}
sync_err = cudaDeviceSynchronize();
if (sync_err != cudaSuccess) {
printf("CUDA ERROR: %s\n", cudaGetErrorString(sync_err));
pass = 0; goto cleanup;
}
cudaMemcpy(h_o, d_o, n_h * HD * sizeof(bf16_t), cudaMemcpyDeviceToHost);
cudaMemcpy(h_lse, d_lse, n_h * sizeof(float), cudaMemcpyDeviceToHost);
// Verify each head
for (int h = 0; h < n_h; h++) {
float o_ref[512]; // max HD
float lse_ref;
reference_attention(
h_q + h * HD, h_k + h * SK * HD, h_v + h * HD * SK,
o_ref, &lse_ref, HD, SK, SCALE
);
float cs = 0, na = 0, nb = 0;
for (int d = 0; d < HD; d++) {
float a = bf16_to_f32_host(h_o[h * HD + d]), b = o_ref[d];
if (fabsf(b) > 1e-4f) { cs += a*b; na += a*a; nb += b*b; }
}
cs /= (sqrtf(na) * sqrtf(nb) + 1e-10f);
float lse_err = fabsf(h_lse[h] - lse_ref) / (fabsf(lse_ref) + 1e-10f);
printf(" Head %2d: cos=%.8f lse_err=%.6f (kernel=%.6f ref=%.6f)\n",
h, cs, lse_err, h_lse[h], lse_ref);
if (cs < 0.999f) {
printf(" HEAD %d FAILED (cos=%.6f < 0.999)\n", h, cs);
pass = 0;
}
}
printf("MHA test %s\n", pass ? "PASSED" : "FAILED");
cleanup:
cudaFree(d_q); cudaFree(d_k); cudaFree(d_v); cudaFree(d_o); cudaFree(d_lse);
free(h_q); free(h_k); free(h_v); free(h_o); free(h_lse);
return pass;
}
static int test_mqa(int n_q, int n_kv) {
printf("\n=== Test MQA: n_q=%d, n_kv=%d, HD=%d, SK=%d ===\n", n_q, n_kv, HD, SK);
const float SCALE = 1.0f / sqrtf((float)HD);
int pass = 1;
int q_per_kv = n_q / n_kv;
// Q: (n_q, hd), K: (n_kv, SK*hd), V: (n_kv, hd*SK), O: (n_q, hd)
bf16_t* h_q = (bf16_t*)malloc(n_q * HD * sizeof(bf16_t));
bf16_t* h_k = (bf16_t*)malloc(n_kv * SK * HD * sizeof(bf16_t));
bf16_t* h_v = (bf16_t*)malloc(n_kv * HD * SK * sizeof(bf16_t));
bf16_t* h_o = (bf16_t*)calloc(n_q * HD, sizeof(bf16_t));
srand(123);
for (int i = 0; i < n_q * HD; i++) h_q[i] = f32_to_bf16_host((float)(rand()%100)/100.0f - 0.5f);
for (int i = 0; i < n_kv * SK * HD; i++) h_k[i] = f32_to_bf16_host((float)(rand()%100)/100.0f - 0.5f);
for (int i = 0; i < n_kv * HD * SK; i++) h_v[i] = f32_to_bf16_host((float)(rand()%100)/100.0f - 0.5f);
bf16_t *d_q, *d_k, *d_v, *d_o;
cudaMalloc(&d_q, n_q * HD * sizeof(bf16_t));
cudaMalloc(&d_k, n_kv * SK * HD * sizeof(bf16_t));
cudaMalloc(&d_v, n_kv * HD * SK * sizeof(bf16_t));
cudaMalloc(&d_o, n_q * HD * sizeof(bf16_t));
cudaMemcpy(d_q, h_q, n_q * HD * sizeof(bf16_t), cudaMemcpyHostToDevice);
cudaMemcpy(d_k, h_k, n_kv * SK * HD * sizeof(bf16_t), cudaMemcpyHostToDevice);
cudaMemcpy(d_v, h_v, n_kv * HD * SK * sizeof(bf16_t), cudaMemcpyHostToDevice);
cudaMemset(d_o, 0, n_q * HD * sizeof(bf16_t));
// For MQA: n_q heads each with different Q, but shared K/V
// We launch n_q CTAs. Each CTA reads its own Q head but the SAME K/V
// (k_head_stride=0, v_head_stride=0 for pure MQA)
// But we need to map Q head h to KV head (h / q_per_kv)
// The kernel doesn't know about the mapping — it just uses blockIdx.y as head_idx
// and k_head_stride to index K. For MQA, we set k_head_stride=0 so all CTAs read the same K.
// For GQA, we'd need a different approach (grouped launches).
//
// Pure MQA test: 1 KV head, all Q heads share it
if (n_kv != 1) {
printf("MQA test requires n_kv=1 for stride=0 trick, skipping\n");
pass = 1; goto cleanup;
}
{
FmhaParams params;
params.q = d_q;
params.k = d_k;
params.v = d_v;
params.o = d_o;
params.lse = nullptr; // skip LSE for MQA test
params.s_k = SK;
params.scale = SCALE;
params.head_dim = HD;
params.q_head_stride = HD;
params.q_batch_stride = n_q * HD;
params.k_head_stride = 0; // MQA: all heads share same K
params.k_batch_stride = SK * HD;
params.v_head_stride = 0; // MQA: all heads share same V
params.v_batch_stride = HD * SK;
params.o_head_stride = HD;
params.o_batch_stride = n_q * HD;
params.lse_head_stride = 0;
params.lse_batch_stride = 0;
int smem = (4 + 8 + 16 + TILE_SZ*2 + TILE_SZ*2 + TILE_SZ*2 + V_SUB_SZ*2 + SK*4 + 256 + 127) & ~127;
if (smem > 48 * 1024) {
cudaFuncSetAttribute(fmha_6warp_multihead_kernel<HD>, cudaFuncAttributeMaxDynamicSharedMemorySize, smem);
}
fmha_6warp_multihead_kernel<HD><<<dim3(1, n_q, 1), 192, smem>>>(params);
cudaError_t launch_err = cudaGetLastError();
cudaError_t sync_err2 = cudaSuccess;
if (launch_err != cudaSuccess) {
printf("LAUNCH ERROR: %s\n", cudaGetErrorString(launch_err));
pass = 0; goto cleanup;
}
sync_err2 = cudaDeviceSynchronize();
if (sync_err2 != cudaSuccess) {
printf("CUDA ERROR: %s\n", cudaGetErrorString(sync_err2));
pass = 0; goto cleanup;
}
cudaMemcpy(h_o, d_o, n_q * HD * sizeof(bf16_t), cudaMemcpyDeviceToHost);
// All Q heads share the same K/V (k_head[0], v_head[0])
for (int h = 0; h < n_q; h++) {
float o_ref[512];
reference_attention(
h_q + h * HD, h_k, h_v,
o_ref, nullptr, HD, SK, SCALE
);
float cs = 0, na = 0, nb = 0;
for (int d = 0; d < HD; d++) {
float a = bf16_to_f32_host(h_o[h * HD + d]), b = o_ref[d];
if (fabsf(b) > 1e-4f) { cs += a*b; na += a*a; nb += b*b; }
}
cs /= (sqrtf(na) * sqrtf(nb) + 1e-10f);
printf(" Q-head %2d (shared KV): cos=%.8f\n", h, cs);
if (cs < 0.999f) { printf(" HEAD %d FAILED\n", h); pass = 0; }
}
}
printf("MQA test %s\n", pass ? "PASSED" : "FAILED");
cleanup:
cudaFree(d_q); cudaFree(d_k); cudaFree(d_v); cudaFree(d_o);
free(h_q); free(h_k); free(h_v); free(h_o);
return pass;
}
static int test_batched(int n_h, int batch_size) {
printf("\n=== Test Batched: n_h=%d, batch=%d, HD=%d, SK=%d ===\n", n_h, batch_size, HD, SK);
const float SCALE = 1.0f / sqrtf((float)HD);
int pass = 1;
int total_q = batch_size * n_h;
bf16_t* h_q = (bf16_t*)malloc(total_q * HD * sizeof(bf16_t));
bf16_t* h_k = (bf16_t*)malloc(total_q * SK * HD * sizeof(bf16_t));
bf16_t* h_v = (bf16_t*)malloc(total_q * HD * SK * sizeof(bf16_t));
bf16_t* h_o = (bf16_t*)calloc(total_q * HD, sizeof(bf16_t));
srand(999);
for (int i = 0; i < total_q * HD; i++) h_q[i] = f32_to_bf16_host((float)(rand()%100)/100.0f - 0.5f);
for (int i = 0; i < total_q * SK * HD; i++) h_k[i] = f32_to_bf16_host((float)(rand()%100)/100.0f - 0.5f);
for (int i = 0; i < total_q * HD * SK; i++) h_v[i] = f32_to_bf16_host((float)(rand()%100)/100.0f - 0.5f);
bf16_t *d_q, *d_k, *d_v, *d_o;
cudaMalloc(&d_q, total_q * HD * sizeof(bf16_t));
cudaMalloc(&d_k, total_q * SK * HD * sizeof(bf16_t));
cudaMalloc(&d_v, total_q * HD * SK * sizeof(bf16_t));
cudaMalloc(&d_o, total_q * HD * sizeof(bf16_t));
cudaMemcpy(d_q, h_q, total_q * HD * sizeof(bf16_t), cudaMemcpyHostToDevice);
cudaMemcpy(d_k, h_k, total_q * SK * HD * sizeof(bf16_t), cudaMemcpyHostToDevice);
cudaMemcpy(d_v, h_v, total_q * HD * SK * sizeof(bf16_t), cudaMemcpyHostToDevice);
cudaMemset(d_o, 0, total_q * HD * sizeof(bf16_t));
FmhaParams params;
params.q = d_q;
params.k = d_k;
params.v = d_v;
params.o = d_o;
params.lse = nullptr;
params.s_k = SK;
params.scale = SCALE;
params.head_dim = HD;
params.q_head_stride = HD;
params.q_batch_stride = n_h * HD;
params.k_head_stride = SK * HD;
params.k_batch_stride = n_h * SK * HD;
params.v_head_stride = HD * SK;
params.v_batch_stride = n_h * HD * SK;
params.o_head_stride = HD;
params.o_batch_stride = n_h * HD;
params.lse_head_stride = 0;
params.lse_batch_stride = 0;
int smem = (4 + 8 + 16 + TILE_SZ*2 + TILE_SZ*2 + TILE_SZ*2 + V_SUB_SZ*2 + SK*4 + 256 + 127) & ~127;
if (smem > 48 * 1024) {
cudaFuncSetAttribute(fmha_6warp_multihead_kernel<HD>, cudaFuncAttributeMaxDynamicSharedMemorySize, smem);
}
fmha_6warp_multihead_kernel<HD><<<dim3(1, n_h, batch_size), 192, smem>>>(params);
cudaError_t launch_err = cudaGetLastError();
cudaError_t sync_err3 = cudaSuccess;
int checked = 0, failed = 0;
if (launch_err != cudaSuccess) {
printf("LAUNCH ERROR: %s\n", cudaGetErrorString(launch_err));
pass = 0; goto cleanup;
}
sync_err3 = cudaDeviceSynchronize();
if (sync_err3 != cudaSuccess) {
printf("CUDA ERROR: %s\n", cudaGetErrorString(sync_err3));
pass = 0; goto cleanup;
}
cudaMemcpy(h_o, d_o, total_q * HD * sizeof(bf16_t), cudaMemcpyDeviceToHost);
// Verify a sample of heads across batches
for (int b = 0; b < batch_size; b++) {
for (int h = 0; h < n_h; h++) {
int idx = b * n_h + h;
float o_ref[512];
reference_attention(
h_q + idx * HD,
h_k + idx * SK * HD,
h_v + idx * HD * SK,
o_ref, nullptr, HD, SK, SCALE
);
float cs = 0, na = 0, nb = 0;
for (int d = 0; d < HD; d++) {
float a = bf16_to_f32_host(h_o[idx * HD + d]), b2 = o_ref[d];
if (fabsf(b2) > 1e-4f) { cs += a*b2; na += a*a; nb += b2*b2; }
}
cs /= (sqrtf(na) * sqrtf(nb) + 1e-10f);
checked++;
if (cs < 0.999f) {
printf(" FAIL batch=%d head=%d: cos=%.6f\n", b, h, cs);
failed++;
}
}
}
printf(" Checked %d heads, %d failed\n", checked, failed);
pass = (failed == 0);
printf("Batched test %s\n", pass ? "PASSED" : "FAILED");
cleanup:
cudaFree(d_q); cudaFree(d_k); cudaFree(d_v); cudaFree(d_o);
free(h_q); free(h_k); free(h_v); free(h_o);
return pass;
}
int main() {
printf("========================================\n");
printf("Multi-head FMHA test suite (HD=%d)\n", HD);
printf("========================================\n");
int all_pass = 1;
// Test 1: MHA with 4 heads
all_pass &= test_mha(4);
// Test 2: MHA with 8 heads (covers Pro's hd=128 with 128 heads in smaller test)
all_pass &= test_mha(8);
// Test 3: MQA: 4 Q heads sharing 1 KV head
all_pass &= test_mqa(4, 1);
// Test 4: Batched: 4 heads × 2 batch
all_pass &= test_batched(4, 2);
printf("\n========================================\n");
printf("Overall: %s\n", all_pass ? "ALL PASSED" : "SOME FAILED");
printf("========================================\n");
return all_pass ? 0 : 1;
}