/** * 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 #include #include #include #include #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, cudaFuncAttributeMaxDynamicSharedMemorySize, smem); } fmha_6warp_multihead_kernel<<>>(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, cudaFuncAttributeMaxDynamicSharedMemorySize, smem); } fmha_6warp_multihead_kernel<<>>(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, cudaFuncAttributeMaxDynamicSharedMemorySize, smem); } fmha_6warp_multihead_kernel<<>>(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; }