File size: 9,042 Bytes
bbb6388 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | // src/kernels/s2_gemv_q8_parity.cpp - the Q8_0-activation GEMV, and the size of the gap it closes.
//
// THREE CHECKS:
// 1. The quantized-activation GEMV matches a host reference that quantizes the activation exactly as
// `ref/quant.py::q8_0` does and then dots it against the same weights.
// 2. **The gap is REAL and MEASURABLE in this project's own kernels**: the same weights driven by an FP16
// activation and by a Q8_0 activation must DIFFER, by roughly the activation contract's ~1%. Without this
// the first check would pass on a kernel that quietly used fp16 anyway, and the round-186 gap would look
// fixed without being fixed.
// 3. The measured size of that difference is reported, so the number in the state file is one this test
// produces rather than one quoted from elsewhere.
#include "strata/kernels/quantize_act.hpp"
#include "strata/kernels/s_gemv.hpp"
#include "strata/kernels/s2_gemv_q8.hpp"
#include <cuda_runtime.h>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <random>
#include <vector>
namespace {
void check(cudaError_t e, const char* what) {
if (e != cudaSuccess) {
std::fprintf(stderr, "%s: %s\n", what, cudaGetErrorString(e));
std::exit(1);
}
}
} // namespace
int main(int argc, char** argv) {
bool selftest = false;
for (int i = 1; i < argc; ++i) {
if (std::string(argv[i]) == "--selftest") selftest = true;
else { std::fprintf(stderr, "usage: s2_gemv_q8_parity [--selftest]\n"); return 2; }
}
const long long n_in = 2560, n_out = 64;
const int tpr = 32;
std::mt19937 rng(31);
std::normal_distribution<float> gauss(0.0f, 1.0f);
std::vector<float> x((size_t) n_in);
for (auto& v : x) v = gauss(rng);
std::vector<uint8_t> codes((size_t) n_out * n_in / 4);
for (auto& v : codes) v = (uint8_t) (rng() & 0xFF);
std::vector<float> scales((size_t) n_out * (n_in / 64));
for (auto& v : scales) v = 0.001f + 0.0001f * (float) (rng() % 100);
// ---- device buffers
float* d_x = nullptr;
uint8_t* d_act = nullptr;
uint8_t* d_codes = nullptr;
float *d_scales = nullptr, *d_y_fp16 = nullptr, *d_y_q8 = nullptr;
check(cudaMalloc(&d_x, (size_t) n_in * sizeof(float)), "m x");
check(cudaMalloc(&d_act, (size_t) (n_in / 32) * 34), "m act");
check(cudaMalloc(&d_codes, codes.size()), "m codes");
check(cudaMalloc(&d_scales, scales.size() * sizeof(float)), "m scales");
check(cudaMalloc(&d_y_fp16, (size_t) n_out * sizeof(float)), "m y1");
check(cudaMalloc(&d_y_q8, (size_t) n_out * sizeof(float)), "m y2");
check(cudaMemcpy(d_x, x.data(), x.size() * sizeof(float), cudaMemcpyHostToDevice), "c x");
check(cudaMemcpy(d_codes, codes.data(), codes.size(), cudaMemcpyHostToDevice), "c codes");
check(cudaMemcpy(d_scales, scales.data(), scales.size() * sizeof(float), cudaMemcpyHostToDevice), "c scales");
strata::kernels::quantize_q8_0(d_x, d_act, n_in, nullptr);
// ---- host reference: quantize the activation with the project's own rule, then dot
std::vector<uint8_t> h_act((size_t) (n_in / 32) * 34);
check(cudaMemcpy(h_act.data(), d_act, h_act.size(), cudaMemcpyDeviceToHost), "c act back");
auto act_val = [&](long long i) {
const uint8_t* blk = &h_act[(size_t) (i / 32) * 34];
const uint16_t dbits = (uint16_t) (blk[0] | (blk[1] << 8));
// fp16 -> f32 without a CUDA header: 0x3C00 = 1.0, and the block scales here are normal
float d;
const uint32_t sign = (uint32_t) (dbits & 0x8000) << 16;
uint32_t ex = (dbits >> 10) & 0x1F, man = dbits & 0x3FF;
uint32_t out;
if (ex == 0) { out = sign; }
else { out = sign | ((ex - 15 + 127) << 23) | (man << 13); }
std::memcpy(&d, &out, 4);
return (float) ((int8_t) blk[2 + (i % 32)]) * d;
};
std::vector<float> ref((size_t) n_out, 0.0f);
for (long long o = 0; o < n_out; ++o) {
float acc = 0.0f;
for (long long g = 0; g < n_in / 64; ++g) {
const float d = scales[(size_t) (o * (n_in / 64) + g)];
for (int j = 0; j < 64; ++j) {
const long long i = g * 64 + j;
const uint8_t byte = codes[(size_t) (o * (n_in / 4) + i / 4)];
const int code = (byte >> ((i % 4) * 2)) & 3;
acc += (float) (code - 1) * d * act_val(i);
}
}
ref[(size_t) o] = acc;
}
strata::kernels::s2_gemv_q8(d_act, d_codes, d_scales, d_y_q8, n_in, n_out, tpr, nullptr);
std::vector<float> got((size_t) n_out);
check(cudaMemcpy(got.data(), d_y_q8, got.size() * sizeof(float), cudaMemcpyDeviceToHost), "c y2");
int bad = 0;
double worst = 0.0, sum_abs = 0.0;
for (long long o = 0; o < n_out; ++o) {
const double a = ref[(size_t) o], b = got[(size_t) o];
sum_abs += std::fabs(a);
// relative to the row magnitude is not needed here - these rows are not cancelling - but the metric is
// stated because round 169 and round 189 both showed the wrong denominator reads as a wrong kernel
const double rel = std::fabs(a - b) / (std::fabs(a) > 1e-30 ? std::fabs(a) : 1e-30);
if (!(rel <= worst)) worst = rel;
// 1e-4, not 1e-5: the measured worst is 1.479e-05, which is FMA contraction over 2560 terms rather
// than a structural error, and a threshold that passes 62 of 64 by luck is not a threshold. The phase
// allows 1e-3 for FP16 paths, so this is ten times tighter than the spec and ten times looser than the
// measurement.
if (!(rel <= 1e-4)) ++bad;
}
std::printf(" %-38s %s (%d of %lld over 1e-4, worst rel %.3e, mean |ref| %.3f)\n",
"q8-activation GEMV vs host reference", bad ? "*** WRONG ***" : "matches", bad, n_out, worst,
sum_abs / (double) n_out);
// ---- CHECK 2: the gap is real. The SAME weights driven by an fp16 activation must differ.
// The fp16 path wants fp16 activations, and `x` is f32, so it is converted exactly (the values are normal
// and small), which keeps the comparison about the ACTIVATION FORMAT and nothing else.
// A REAL fp32 -> fp16 conversion. The first version wrote `0x3C00 + (int)(x*64)`, which adds to the raw
// BITS and therefore changes the EXPONENT - x = 2.0 became 0x3C80 = 1.25 - so the two paths were driven by
// unrelated activations and the comparison reported 2770%. The number was a symptom of the fixture, and
// the fixture is what the check exists to be able to trust.
auto f32_to_f16 = [](float f) -> uint16_t {
uint32_t b;
std::memcpy(&b, &f, 4);
const uint32_t sign = (b >> 16) & 0x8000u;
int exp = (int) ((b >> 23) & 0xFF) - 127 + 15;
uint32_t man = b & 0x7FFFFFu;
if (exp >= 31) return (uint16_t) (sign | 0x7C00u | (man ? 0x200u : 0u));
if (exp <= 0) {
if (exp < -10) return (uint16_t) sign;
man |= 0x800000u;
const uint32_t sh = (uint32_t) (14 - exp);
uint32_t h = (man >> sh) & 0x3FFu;
const uint32_t rem = man & ((1u << sh) - 1u);
if (rem > (1u << (sh - 1)) || (rem == (1u << (sh - 1)) && (h & 1u))) ++h;
return (uint16_t) (sign | h);
}
uint16_t h = (uint16_t) (sign | ((uint32_t) exp << 10) | (man >> 13));
const uint32_t rem = man & 0x1FFFu;
if (rem > 0x1000u || (rem == 0x1000u && (h & 1u))) ++h;
return h;
};
std::vector<uint16_t> hx((size_t) n_in);
for (long long i = 0; i < n_in; ++i) hx[(size_t) i] = f32_to_f16(x[(size_t) i]);
uint16_t* d_hx = nullptr;
check(cudaMalloc(&d_hx, hx.size() * sizeof(uint16_t)), "m hx");
check(cudaMemcpy(d_hx, hx.data(), hx.size() * sizeof(uint16_t), cudaMemcpyHostToDevice), "c hx");
strata::kernels::SForm form{2, -1, 64, strata::kernels::Codebook::Affine, false};
strata::kernels::s_gemv_split(d_hx, d_codes, d_scales, nullptr, d_y_fp16, n_in, n_out, form, tpr);
std::vector<float> y16((size_t) n_out);
check(cudaMemcpy(y16.data(), d_y_fp16, y16.size() * sizeof(float), cudaMemcpyDeviceToHost), "c y1");
double diff = 0.0;
for (long long o = 0; o < n_out; ++o) {
diff += std::fabs((double) y16[(size_t) o] - (double) got[(size_t) o]);
}
const double rel_gap = diff / (sum_abs > 1e-30 ? sum_abs : 1e-30);
std::printf(" %-38s %.4f%% of the reference magnitude\n", "fp16-activation vs q8-activation", rel_gap * 100);
if (rel_gap < 1e-4) {
std::fprintf(stderr,
"the two activation paths agree to %.2e, so this test CANNOT see the difference it exists "
"to measure - either the q8 path silently used fp16, or the fixture is degenerate\n",
rel_gap);
++bad;
}
std::printf("\ns2_gemv_q8: %d failures\n", bad);
if (bad) return 1;
if (selftest) std::printf("s2_gemv_q8_parity OK\n");
return 0;
}
|