// src/kernels/kv_q8_parity.cpp - plan v0.3 P7: INT8 KV append/gather against a host reference (GPU, no model). // // Appends random K/V cells at scattered positions through a non-identity page table into both the INT8 pools // (kv_append_q8_step) and the FP16 pools (kv_append_step), gathers random cell sets from both, and checks: // 1. INT8 codes and FP16 scales are BITWISE equal to the host reference (same f16_from_f32 rounding); // 2. the INT8 gather is BITWISE equal to the host dequantization of those codes; // 3. against the FP16 path, the INT8 values differ by at most half a quantization step of their group plus the // fp16 rounding of both sides (0.624 steps). #include "strata/kernels/f16_bits.hpp" #include "strata/kernels/kv_q8.hpp" #include "strata/kernels/qsa.hpp" #include #include #include #include #include #include #include namespace k = strata::kernels; namespace { int g_fail = 0; void ck(cudaError_t e, const char* w) { if (e != cudaSuccess) { std::fprintf(stderr, "%s: %s\n", w, cudaGetErrorString(e)); std::exit(2); } } template T* dalloc(size_t n) { T* p = nullptr; ck(cudaMalloc(&p, n * sizeof(T) + 64), "malloc"); ck(cudaMemset(p, 0, n * sizeof(T) + 64), "memset"); return p; } } // namespace int main() { k::QsaShapes s = k::qsa_real_shapes(); s.page_size = 64; // several pages in a small pool const int H = (int) s.n_head_kv, D = (int) s.head_dim, P = (int) s.page_size, G = D / k::KV_Q8_GROUP; const int pages = 8, cells = pages * P; std::mt19937 rng(17); std::vector table(pages); for (int i = 0; i < pages; ++i) table[i] = (i * 5 + 3) % pages; // a non-identity permutation int32_t* d_table = dalloc(pages); ck(cudaMemcpy(d_table, table.data(), pages * 4, cudaMemcpyHostToDevice), "table"); int8_t *kq = dalloc((size_t) cells * H * D), *vq = dalloc((size_t) cells * H * D); uint16_t *ks = dalloc((size_t) cells * H * G), *vs = dalloc((size_t) cells * H * G); uint16_t *kp = dalloc((size_t) cells * H * D), *vp = dalloc((size_t) cells * H * D); float *kcur = dalloc(H * D), *vcur = dalloc(H * D); int32_t* step = dalloc(k::kStepCount); std::vector> hk(cells), hv(cells); std::normal_distribution nd(0.f, 1.f); std::vector positions(cells); for (int i = 0; i < cells; ++i) positions[i] = i; std::shuffle(positions.begin(), positions.end(), rng); const int n_fill = cells - 37; // leave some cells empty for (int n = 0; n < n_fill; ++n) { const int pos = positions[n]; std::vector kv(H * D), vv(H * D); const float scale = (n % 7 == 0) ? 1e-3f : (n % 11 == 0 ? 40.f : 1.f); // small and large groups for (auto& x : kv) x = nd(rng) * scale; for (auto& x : vv) x = nd(rng) * scale; if (n % 13 == 0) std::fill(kv.begin(), kv.begin() + 64, 0.f); // an all-zero group hk[pos] = kv; hv[pos] = vv; int32_t hstep[k::kStepCount] = {pos, pos + 1, 0, 0}; ck(cudaMemcpy(step, hstep, sizeof hstep, cudaMemcpyHostToDevice), "step"); ck(cudaMemcpy(kcur, kv.data(), kv.size() * 4, cudaMemcpyHostToDevice), "k"); ck(cudaMemcpy(vcur, vv.data(), vv.size() * 4, cudaMemcpyHostToDevice), "v"); k::kv_append_q8_step(kq, vq, ks, vs, d_table, step, kcur, vcur, s, nullptr); k::kv_append_step(kp, vp, d_table, step, kcur, vcur, s, nullptr); ck(cudaDeviceSynchronize(), "append"); } // 1. codes and scales bitwise vs the host reference std::vector hkq((size_t) cells * H * D), hvq(hkq.size()); std::vector hks((size_t) cells * H * G), hvs(hks.size()); ck(cudaMemcpy(hkq.data(), kq, hkq.size(), cudaMemcpyDeviceToHost), "d2h"); ck(cudaMemcpy(hvq.data(), vq, hvq.size(), cudaMemcpyDeviceToHost), "d2h"); ck(cudaMemcpy(hks.data(), ks, hks.size() * 2, cudaMemcpyDeviceToHost), "d2h"); ck(cudaMemcpy(hvs.data(), vs, hvs.size() * 2, cudaMemcpyDeviceToHost), "d2h"); long bad_codes = 0; for (int n = 0; n < n_fill; ++n) { const int pos = positions[n]; for (int kvsel = 0; kvsel < 2; ++kvsel) for (int h = 0; h < H; ++h) for (int g = 0; g < G; ++g) { const float* x = (kvsel ? hv[pos] : hk[pos]).data() + h * D + g * 64; float amax = 0.f; for (int t = 0; t < 64; ++t) amax = std::fmax(amax, std::fabs(x[t])); const uint16_t sb = k::f16_from_f32(amax / 127.0f); const float sf = k::f32_from_f16(sb); const long long row = ((long long) table[pos / P] * H + h) * P + pos % P; if ((kvsel ? hvs : hks)[row * G + g] != sb) ++bad_codes; for (int t = 0; t < 64; ++t) { int q = sf > 0.f ? (int) std::nearbyint(x[t] / sf) : 0; q = std::clamp(q, -127, 127); if ((kvsel ? hvq : hkq)[row * D + g * 64 + t] != (int8_t) q) ++bad_codes; } } } if (bad_codes) { std::fprintf(stderr, "FAIL: %ld codes/scales differ from the host reference\n", bad_codes); ++g_fail; } // 2 + 3. gather random selections from both paths const int max_ids = 200; int32_t* d_ids = dalloc(max_ids); uint16_t *k8 = dalloc((size_t) max_ids * H * D), *v8 = dalloc((size_t) max_ids * H * D); uint16_t *k16 = dalloc((size_t) max_ids * H * D), *v16 = dalloc((size_t) max_ids * H * D); double worst = 0.0; for (int trial = 0; trial < 20; ++trial) { const int n_ids = 1 + (int) (rng() % max_ids); std::vector ids(n_ids); for (auto& id : ids) id = positions[rng() % n_fill]; ck(cudaMemcpy(d_ids, ids.data(), n_ids * 4, cudaMemcpyHostToDevice), "ids"); int32_t hstep[k::kStepCount] = {0, 0, 0, n_ids}; ck(cudaMemcpy(step, hstep, sizeof hstep, cudaMemcpyHostToDevice), "step"); k::kv_gather_q8_step(kq, vq, ks, vs, d_table, d_ids, step, max_ids, s, k8, v8, nullptr); k::kv_gather_step(kp, vp, d_table, d_ids, step, max_ids, s, k16, v16, nullptr); ck(cudaDeviceSynchronize(), "gather"); std::vector a8((size_t) n_ids * H * D), b8(a8.size()), a16(a8.size()), b16(a8.size()); ck(cudaMemcpy(a8.data(), k8, a8.size() * 2, cudaMemcpyDeviceToHost), "d2h"); ck(cudaMemcpy(b8.data(), v8, b8.size() * 2, cudaMemcpyDeviceToHost), "d2h"); ck(cudaMemcpy(a16.data(), k16, a16.size() * 2, cudaMemcpyDeviceToHost), "d2h"); ck(cudaMemcpy(b16.data(), v16, b16.size() * 2, cudaMemcpyDeviceToHost), "d2h"); for (int j = 0; j < n_ids; ++j) for (int h = 0; h < H; ++h) { const long long row = ((long long) table[ids[j] / P] * H + h) * P + ids[j] % P; for (int d = 0; d < D; ++d) { const size_t o = ((size_t) j * H + h) * D + d; for (int kvsel = 0; kvsel < 2; ++kvsel) { const float sf = k::f32_from_f16((kvsel ? hvs : hks)[row * G + d / 64]); const int8_t q = (kvsel ? hvq : hkq)[row * D + d]; const uint16_t want = k::f16_from_f32((float) q * sf); const uint16_t got = (kvsel ? b8 : a8)[o]; if (got != want) { if (g_fail < 5) std::fprintf(stderr, "FAIL: gather id %d h %d d %d\n", j, h, d); ++g_fail; } const float ref = k::f32_from_f16((kvsel ? b16 : a16)[o]); const double err = std::fabs(k::f32_from_f16(got) - ref) / (sf > 0 ? sf : 1.0); worst = std::max(worst, err); } } } } // |x - q*sf| <= sf/2 (the stored scale keeps |x|/sf <= 127.06, so no clamping), plus the fp16 rounding of each // side: at most 2^-11 relative of a value up to 127*sf, i.e. 127/2048 = 0.062 steps each -> 0.5 + 0.124 if (!(worst <= 0.5 + 2.0 * 127.0 / 2048.0 + 1e-3)) { std::fprintf(stderr, "FAIL: INT8 vs FP16 error %.3f steps\n", worst); ++g_fail; } std::printf("kv_q8_parity: %s (worst INT8-vs-FP16 error %.3f quantization steps)\n", g_fail ? "FAILED" : "OK", worst); return g_fail ? 1 : 0; }