/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /** * @file test_kvq4_scale.c * @brief Q4_0 KV append kernels vs a host reference, byte for byte, on one K80 die. * * Runs fc_attn_kv_append_q4, _q4_dev and _slots_q4 from the EMBEDDED PTX * (cuda/fc_cuda_ptx.h, what ships) and compares every written Q4_0 block with * a host quantizer: scale = fp16(amax / -8) (round to nearest even), codes = * clamp(rint(x / scale) + 8). Scale bytes and code bytes are counted apart. * * Guards the 2026-10 K80 bug: ptxas 11.4 (sm_37) compiled the scale's low-byte * store as F2I.U32.F16.TRUNC, so every device-written scale had low byte 0. * Inputs stay in the fp32 normal range (the tree builds with -ffast-math, * which flushes host subnormals). * * Usage: CUDA_VISIBLE_DEVICES= ./cuda/tests/test_kvq4_scale (exit 0 = PASS) * * @status Working * @issues None * @todo None */ #include "../fc_cuda.h" #include #include #include #include #include #define KVQ4_BLOCK_ELEMS 32 #define KVQ4_BLOCK_BYTES 18 #define KVQ4_SINKMAX 8 #define KVQ4_WMAX 256 #define TEST_HEAD_DIM 256 #define TEST_N_HEAD_KV 2 #define TEST_MAX_SEQ 8 #define TEST_NPOS 6 #define TEST_NSLOTS 3 #define TEST_SINK 4 #define TEST_WIN 2 typedef struct { long scale_blocks; /* blocks whose two scale bytes differ */ long low_byte_zero; /* ...of which the device low byte is 0 and the host's is not */ long code_blocks; /* blocks whose 16 code bytes differ */ long blocks; /* blocks compared */ } kvq4_compare_counts; /* fp32 -> fp16 bits, round to nearest even (normal, subnormal, inf, NaN). */ static uint16_t host_float_to_half_bits(float value) { uint32_t bits; memcpy(&bits, &value, 4); const uint32_t sign = (bits >> 16) & 0x8000u; const int32_t exponent = (int32_t)((bits >> 23) & 0xFFu); uint32_t mantissa = bits & 0x7FFFFFu; if (exponent == 0xFF) return (uint16_t)(sign | 0x7C00u | (mantissa ? 0x200u : 0u)); int32_t half_exponent = exponent - 127 + 15; if (half_exponent >= 31) return (uint16_t)(sign | 0x7C00u); if (half_exponent <= 0) { if (half_exponent < -10) return (uint16_t)sign; mantissa |= 0x800000u; const int shift = 14 - half_exponent; uint32_t rounded = mantissa >> shift; const uint32_t rest = mantissa & ((1u << shift) - 1u); const uint32_t halfway = 1u << (shift - 1); if (rest > halfway || (rest == halfway && (rounded & 1u))) rounded++; return (uint16_t)(sign | rounded); } uint32_t result = ((uint32_t)half_exponent << 10) | (mantissa >> 13); const uint32_t rest = mantissa & 0x1FFFu; if (rest > 0x1000u || (rest == 0x1000u && (result & 1u))) result++; return (uint16_t)(sign | result); } static float host_half_bits_to_float(uint16_t half_bits) { const uint32_t sign = (uint32_t)(half_bits & 0x8000u) << 16; const uint32_t exponent = (half_bits >> 10) & 0x1Fu; const uint32_t mantissa = half_bits & 0x3FFu; if (exponent == 0) { const float magnitude = ldexpf((float)mantissa, -24); return sign ? -magnitude : magnitude; } uint32_t bits = sign | ((exponent == 31 ? 0xFFu : exponent - 15 + 127) << 23) | (mantissa << 13); float value; memcpy(&value, &bits, 4); return value; } /* Host Q4_0 quantizer, in the device's operation order (serial fmaxf fold, * d = amax / -8, id = 1 / d, code = rint(x * id) + 8 clamped to 0..15). */ static void host_quantize_block(const float *x32, uint8_t *out18) { float amax = 0.0f; for (int j = 0; j < KVQ4_BLOCK_ELEMS; j++) amax = fmaxf(amax, fabsf(x32[j])); const float d = amax / -8.0f; const float id = (d != 0.0f) ? 1.0f / d : 0.0f; const uint16_t d_bits = host_float_to_half_bits(d); out18[0] = (uint8_t)(d_bits & 0xFF); out18[1] = (uint8_t)(d_bits >> 8); for (int lane = 0; lane < KVQ4_BLOCK_ELEMS / 2; lane++) { int q_lo = (int)rintf(x32[lane] * id) + 8; int q_hi = (int)rintf(x32[lane + KVQ4_BLOCK_ELEMS / 2] * id) + 8; q_lo = q_lo < 0 ? 0 : (q_lo > 15 ? 15 : q_lo); q_hi = q_hi < 0 ? 0 : (q_hi > 15 ? 15 : q_hi); out18[2 + lane] = (uint8_t)(q_lo | (q_hi << 4)); } } /* Compare one device row (head_dim / 32 blocks) with the host quantizer. */ static void compare_row(const float *src_row, const uint8_t *device_row, kvq4_compare_counts *counts, int *printed) { for (int b = 0; b < TEST_HEAD_DIM / KVQ4_BLOCK_ELEMS; b++) { uint8_t ref[KVQ4_BLOCK_BYTES]; const uint8_t *dev = device_row + (size_t)b * KVQ4_BLOCK_BYTES; host_quantize_block(src_row + (size_t)b * KVQ4_BLOCK_ELEMS, ref); counts->blocks++; if (memcmp(dev, ref, 2) != 0) { counts->scale_blocks++; if (dev[0] == 0 && ref[0] != 0) counts->low_byte_zero++; if (*printed < 4) { const uint16_t dev_bits = (uint16_t)(dev[0] | (dev[1] << 8)); const uint16_t ref_bits = (uint16_t)(ref[0] | (ref[1] << 8)); printf(" scale: device 0x%04x (%.6g) host 0x%04x (%.6g)\n", dev_bits, host_half_bits_to_float(dev_bits), ref_bits, host_half_bits_to_float(ref_bits)); (*printed)++; } } if (memcmp(dev + 2, ref + 2, KVQ4_BLOCK_BYTES - 2) != 0) counts->code_blocks++; } } /* Deterministic inputs: per-block magnitudes from 1e-3 to ~1e2, mixed signs, * one all-zero block per row (d = -0.0). */ static void fill_inputs(float *values, size_t n, uint32_t seed) { uint32_t state = seed; for (size_t i = 0; i < n; i++) { state = state * 1664525u + 1013904223u; const size_t block = i / KVQ4_BLOCK_ELEMS; const float magnitude = powf(10.0f, (float)(block % 6) - 3.0f) * (1.0f + (float)(block % 7)); const float unit = (float)(state >> 8) / 16777216.0f * 2.0f - 1.0f; values[i] = (block % (TEST_HEAD_DIM / KVQ4_BLOCK_ELEMS) == 5) ? 0.0f : unit * magnitude; } } static int report(const char *name, const char *cache, const kvq4_compare_counts *c) { const int pass = c->scale_blocks == 0 && c->code_blocks == 0; printf(" %-24s %s: %ld blocks, scale mismatches %ld (low byte 0: %ld), code mismatches %ld %s\n", name, cache, c->blocks, c->scale_blocks, c->low_byte_zero, c->code_blocks, pass ? "OK" : "FAIL"); return pass ? 0 : 1; } typedef struct { fc_cuda_ctx *ctx; fc_buffer *k_new, *v_new, *k_cache, *v_cache, *k_sink, *v_sink, *k_win, *v_win; size_t cache_bytes; } kvq4_buffers; static int buffers_create(kvq4_buffers *b, fc_cuda_ctx *ctx, const float *k_src, const float *v_src, size_t src_floats, int nslots) { const size_t row_bytes = (TEST_HEAD_DIM / KVQ4_BLOCK_ELEMS) * KVQ4_BLOCK_BYTES; const size_t shadow = (size_t)nslots * TEST_N_HEAD_KV * KVQ4_WMAX * TEST_HEAD_DIM * 2; b->ctx = ctx; b->cache_bytes = (size_t)nslots * TEST_N_HEAD_KV * TEST_MAX_SEQ * row_bytes; uint8_t *zero = calloc(1, shadow > b->cache_bytes ? shadow : b->cache_bytes); b->k_new = fc_buffer_from_ptr_ctx(ctx, k_src, src_floats * sizeof(float)); b->v_new = fc_buffer_from_ptr_ctx(ctx, v_src, src_floats * sizeof(float)); b->k_cache = fc_buffer_from_ptr_ctx(ctx, zero, b->cache_bytes); b->v_cache = fc_buffer_from_ptr_ctx(ctx, zero, b->cache_bytes); b->k_sink = fc_buffer_from_ptr_ctx(ctx, zero, shadow); b->v_sink = fc_buffer_from_ptr_ctx(ctx, zero, shadow); b->k_win = fc_buffer_from_ptr_ctx(ctx, zero, shadow); b->v_win = fc_buffer_from_ptr_ctx(ctx, zero, shadow); free(zero); return (b->k_new && b->v_new && b->k_cache && b->v_cache && b->k_sink && b->v_sink && b->k_win && b->v_win) ? 0 : 1; } static void buffers_free(kvq4_buffers *b) { fc_buffer *all[] = {b->k_new, b->v_new, b->k_cache, b->v_cache, b->k_sink, b->v_sink, b->k_win, b->v_win}; for (size_t i = 0; i < sizeof all / sizeof all[0]; i++) fc_buffer_free(all[i]); } /* Read both caches back and compare every row that `src_of_row` maps to an input * row (-1 = row not written). Rows are [slot][head][max_seq]. */ static int check_caches(const char *name, kvq4_buffers *b, const float *k_src, const float *v_src, int nslots, int (*src_of_row)(int, int, int)) { const size_t row_bytes = (TEST_HEAD_DIM / KVQ4_BLOCK_ELEMS) * KVQ4_BLOCK_BYTES; uint8_t *k_dev = malloc(b->cache_bytes), *v_dev = malloc(b->cache_bytes); kvq4_compare_counts kc = {0}, vc = {0}; int printed = 0, fail = 0; fc_sync_ctx(b->ctx); fc_buffer_to_cpu(b->k_cache, k_dev); fc_buffer_to_cpu(b->v_cache, v_dev); for (int u = 0; u < nslots; u++) for (int h = 0; h < TEST_N_HEAD_KV; h++) for (int p = 0; p < TEST_MAX_SEQ; p++) { const int src = src_of_row(u, h, p); if (src < 0) continue; const size_t row = ((size_t)u * TEST_N_HEAD_KV + h) * TEST_MAX_SEQ + p; compare_row(k_src + (size_t)src * TEST_HEAD_DIM, k_dev + row * row_bytes, &kc, &printed); compare_row(v_src + (size_t)src * TEST_HEAD_DIM, v_dev + row * row_bytes, &vc, &printed); } fail |= report(name, "K", &kc); fail |= report(name, "V", &vc); if (kc.blocks == 0) fail = 1; free(k_dev); free(v_dev); return fail; } /* append_q4 (pos0 = 1, npos rows) and append_q4_dev (one row per launch from a * device pos) both write row p = 1 + t from input row (t, h). */ static int src_row_single(int u, int h, int p) { (void)u; return (p >= 1 && p < 1 + TEST_NPOS) ? (p - 1) * TEST_N_HEAD_KV + h : -1; } /* append_slots_q4: slot u writes position u + 2 from input row (u, h). */ static int src_row_slots(int u, int h, int p) { return p == u + 2 ? u * TEST_N_HEAD_KV + h : -1; } static int run_append_q4(fc_cuda_ctx *ctx, const float *k_src, const float *v_src) { kvq4_buffers b; int fail = buffers_create(&b, ctx, k_src, v_src, (size_t)TEST_NPOS * TEST_N_HEAD_KV * TEST_HEAD_DIM, 1); if (!fail) { fc_attn_kv_append_q4_ctx(ctx, b.k_new, b.v_new, b.k_cache, b.v_cache, b.k_sink, b.v_sink, b.k_win, b.v_win, TEST_N_HEAD_KV, TEST_HEAD_DIM, TEST_MAX_SEQ, 1, TEST_NPOS, TEST_SINK, TEST_WIN); fail = check_caches("fc_attn_kv_append_q4", &b, k_src, v_src, 1, src_row_single); } buffers_free(&b); return fail; } static int run_append_q4_dev(fc_cuda_ctx *ctx, const float *k_src, const float *v_src) { const size_t row_floats = (size_t)TEST_N_HEAD_KV * TEST_HEAD_DIM; kvq4_buffers b; int fail = buffers_create(&b, ctx, k_src, v_src, TEST_NPOS * row_floats, 1); fc_buffer *pos = fc_buffer_alloc_ctx(ctx, sizeof(int)); fc_buffer *k_one = fc_buffer_alloc_ctx(ctx, row_floats * sizeof(float)); fc_buffer *v_one = fc_buffer_alloc_ctx(ctx, row_floats * sizeof(float)); if (!pos || !k_one || !v_one) fail = 1; for (int t = 0; !fail && t < TEST_NPOS; t++) { const int p = 1 + t; fc_sync_ctx(ctx); fc_buffer_from_cpu(pos, &p); fc_buffer_from_cpu(k_one, k_src + (size_t)t * row_floats); fc_buffer_from_cpu(v_one, v_src + (size_t)t * row_floats); fc_attn_kv_append_q4_dev_ctx(ctx, k_one, v_one, b.k_cache, b.v_cache, b.k_sink, b.v_sink, b.k_win, b.v_win, TEST_N_HEAD_KV, TEST_HEAD_DIM, TEST_MAX_SEQ, pos, TEST_SINK, TEST_WIN); } if (!fail) fail = check_caches("fc_attn_kv_append_q4_dev", &b, k_src, v_src, 1, src_row_single); fc_buffer_free(pos); fc_buffer_free(k_one); fc_buffer_free(v_one); buffers_free(&b); return fail; } static int run_append_slots_q4(fc_cuda_ctx *ctx, const float *k_src, const float *v_src) { const int slot_pos[TEST_NSLOTS] = {2, 3, 4}; kvq4_buffers b; int fail = buffers_create(&b, ctx, k_src, v_src, (size_t)TEST_NSLOTS * TEST_N_HEAD_KV * TEST_HEAD_DIM, TEST_NSLOTS); fc_buffer *pos = fc_buffer_from_ptr_ctx(ctx, slot_pos, sizeof slot_pos); if (!pos) fail = 1; if (!fail) { fc_attn_kv_append_slots_q4_ctx(ctx, b.k_new, b.v_new, b.k_cache, b.v_cache, b.k_sink, b.v_sink, b.k_win, b.v_win, TEST_N_HEAD_KV, TEST_HEAD_DIM, TEST_MAX_SEQ, pos, TEST_NSLOTS, TEST_SINK, TEST_WIN); fail = check_caches("fc_attn_kv_append_slots_q4", &b, k_src, v_src, TEST_NSLOTS, src_row_slots); } fc_buffer_free(pos); buffers_free(&b); return fail; } int main(void) { const size_t n = (size_t)TEST_NPOS * TEST_N_HEAD_KV * TEST_HEAD_DIM; float *k_src = malloc(n * sizeof(float)), *v_src = malloc(n * sizeof(float)); int fail = 0; fill_inputs(k_src, n, 0x1234u); fill_inputs(v_src, n, 0xBEEFu); fc_cuda_ctx *ctx = fc_cuda_init_device(0); if (!ctx || !fc_attn_flash_q4_available_ctx(ctx) || !fc_attn_decode_graph_available_ctx(ctx, 1) || !fc_attn_flash_decode_slots_q4_available_ctx(ctx)) { printf("test_kvq4_scale: Q4 KV kernels not available on device 0 -- FAIL\n"); return 1; } printf("test_kvq4_scale: Q4_0 KV append vs host reference (head_dim %d)\n", TEST_HEAD_DIM); fail |= run_append_q4(ctx, k_src, v_src); fail |= run_append_q4_dev(ctx, k_src, v_src); fail |= run_append_slots_q4(ctx, k_src, v_src); printf("test_kvq4_scale: %s\n", fail ? "FAIL" : "PASS"); fc_cuda_ctx_free(ctx); free(k_src); free(v_src); return fail; }