ik_llama_opt/tests/test-iq4-ks-kt-decode.cpp

444 lines
18 KiB
C++

// Checks the IQ4_KS and IQ4_KT decode arithmetic against ggml's to_float.
//
// This is a CPU test and contains no Vulkan: it re-implements, in C++, the indexing that
// each of the four Vulkan shader families uses for these two types, and diffs the result
// against the CPU reference. It therefore validates the format transcription, not the
// compiled shaders -- it cannot catch a driver or codegen fault, and it has to be kept in
// step with ggml/src/vulkan-shaders/types.comp by hand. The shaders themselves are covered
// on device by test-backend-ops and by GGML_VULKAN_CHECK_RESULTS.
//
// dequant_iq4_k*.comp one thread per 32-weight subblock, global block index
// get_rows_iq4_k*.comp two weights per invocation, row index taken from the shape
// mul_mm.comp A-load LOAD_VEC_A weights per load index
// mul_mat_vec_iq4_k*.comp one thread per subblock, rows walked by the row stride
//
// Buffers are synthesised directly, so every bit pattern is reachable and no quantizer is
// needed, and they are laid out with ggml_row_size() so that a wrong stride in the mirrored
// a_row_words() shows up as a mismatch rather than shifting the reference too.
#include "ggml.h"
#define GGML_COMMON_DECL_C
#define GGML_COMMON_IMPL_C
#include "ggml-common.h"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>
static const uint32_t QK_K_ = 256;
static const uint32_t KS_BLOCK_W = 34; // 8 scale bytes + 128 quant bytes
static const uint32_t KT_BLOCK_W = 32; // 8 shb words + 64 ql bytes + 32 qh bytes
static const uint32_t ROW_META_W = 1; // the row scale
static int g_checks = 0;
static int g_failures = 0;
static int g_reported = 0;
static float bits_to_float(uint32_t u) {
float f;
memcpy(&f, &u, sizeof(f));
return f;
}
static uint32_t a_row_words(uint32_t ncols, uint32_t block_words) {
return ROW_META_W + (ncols / QK_K_) * block_words;
}
static uint32_t rd_byte(const uint32_t * w, uint32_t base_word, uint32_t byte_idx) {
return (w[base_word + byte_idx / 4] >> (8 * (byte_idx % 4))) & 0xFF;
}
// ---------------------------------------------------------------------------------------
// IQ4_KS, once per shader family
static float ks_scale(const uint32_t * w, uint32_t row_word, uint32_t blk, uint32_t ib32, uint32_t & vo) {
const uint32_t sc = rd_byte(w, blk, ib32);
vo = (sc & 1) << 4;
return bits_to_float(w[row_word]) * (float)((int)(sc & 254) - 127);
}
// the 32 weights of one subblock; shared by the dequant and mul_mat_vec mirrors, which
// differ in addressing, not in how a subblock is decoded
static void ks_emit_subblock(const uint32_t * w, uint32_t blk, uint32_t ib32, float dl, uint32_t vo, float * out) {
for (uint32_t l = 0; l < 4; ++l) {
const uint32_t qs = w[blk + 2 + 4 * ib32 + l];
for (uint32_t k = 0; k < 4; ++k) {
const uint32_t q = (qs >> (8 * k)) & 0xFF;
out[4 * l + k ] = dl * (float)iq4k_values[vo + (q & 0xF)];
out[4 * l + k + 16] = dl * (float)iq4k_values[vo + (q >> 4)];
}
}
}
static void ks_dequant_shader(const uint32_t * w, uint32_t ncols, uint32_t nrows, float * out) {
const uint32_t blocks_per_row = ncols / QK_K_;
for (uint32_t ib = 0; ib < blocks_per_row * nrows; ++ib) {
const uint32_t row_word = (ib / blocks_per_row) * a_row_words(ncols, KS_BLOCK_W);
const uint32_t blk = row_word + ROW_META_W + (ib % blocks_per_row) * KS_BLOCK_W;
for (uint32_t ib32 = 0; ib32 < 8; ++ib32) {
uint32_t vo;
const float dl = ks_scale(w, row_word, blk, ib32, vo);
ks_emit_subblock(w, blk, ib32, dl, vo, out + 256 * ib + 32 * ib32);
}
}
}
static float ks_get_rows_elem(const uint32_t * w, uint32_t row_word, uint32_t i00) {
const uint32_t blk = row_word + ROW_META_W + (i00 / QK_K_) * KS_BLOCK_W;
const uint32_t pos = i00 % QK_K_;
const uint32_t ib32 = pos / 32;
const uint32_t j = pos % 32;
uint32_t vo;
const float dl = ks_scale(w, row_word, blk, ib32, vo);
const uint32_t qb = 16 * ib32 + (j & 15);
const uint32_t q = rd_byte(w, blk + 2, qb);
return dl * (float)iq4k_values[vo + (j < 16 ? (q & 0xF) : (q >> 4))];
}
static void ks_get_rows_shader(const uint32_t * w, uint32_t ncols, uint32_t nrows, float * out) {
for (uint32_t r = 0; r < nrows; ++r) {
const uint32_t row_word = r * a_row_words(ncols, KS_BLOCK_W);
for (uint32_t i00 = 0; i00 < ncols; i00 += 2) {
out[r * ncols + i00 ] = ks_get_rows_elem(w, row_word, i00);
out[r * ncols + i00 + 1] = ks_get_rows_elem(w, row_word, i00 + 1);
}
}
}
static void ks_mul_mm_shader(const uint32_t * w, uint32_t ncols, uint32_t nrows, float * out) {
const uint32_t load_vec_a = 2;
for (uint32_t idx = 0; idx < nrows * ncols / load_vec_a; ++idx) {
const uint32_t elem = idx * load_vec_a;
const uint32_t row_word = (elem / ncols) * a_row_words(ncols, KS_BLOCK_W);
const uint32_t acol = elem % ncols;
const uint32_t blk = row_word + ROW_META_W + (acol / QK_K_) * KS_BLOCK_W;
const uint32_t ib32 = (acol % QK_K_) / 32;
const uint32_t j = acol % 32;
uint32_t vo;
const float dl = ks_scale(w, row_word, blk, ib32, vo);
const uint32_t qb = 16 * ib32 + (j & 15);
const uint32_t qw = w[blk + 2 + qb / 4] >> (8 * (qb % 4));
const uint32_t qshift = j < 16 ? 0 : 4;
out[elem ] = dl * (float)iq4k_values[vo + ((qw >> qshift) & 0xF)];
out[elem + 1] = dl * (float)iq4k_values[vo + ((qw >> (8 + qshift)) & 0xF)];
}
}
static void ks_mul_mat_vec_shader(const uint32_t * w, uint32_t ncols, uint32_t nrows, float * out) {
const uint32_t words_per_row = a_row_words(ncols, KS_BLOCK_W);
for (uint32_t i = 0; i < ncols / QK_K_; ++i) {
for (uint32_t ib32 = 0; ib32 < 8; ++ib32) {
uint32_t row_word = 0;
for (uint32_t n = 0; n < nrows; ++n) {
const uint32_t blk = row_word + ROW_META_W + i * KS_BLOCK_W;
uint32_t vo;
const float dl = ks_scale(w, row_word, blk, ib32, vo);
ks_emit_subblock(w, blk, ib32, dl, vo, out + n * ncols + i * QK_K_ + 32 * ib32);
row_word += words_per_row;
}
}
}
}
// ---------------------------------------------------------------------------------------
// IQ4_KT, once per shader family
// the single trellis step, as written in types.comp
static float kt_next(uint32_t & x) {
x *= 0xCBAC1FEDu;
const uint32_t s = x & 0x3F3F3F3Fu;
return (float)((int32_t)((s & 0xFF) + ((s >> 8) & 0xFF) + ((s >> 16) & 0xFF) + (s >> 24)) - 126);
}
// index assembly for one group of four weights, shared by the four mirrors below
static uint32_t kt_group_x(const uint32_t * w, uint32_t blk, uint32_t ib32, uint32_t ig, float d, float & sl) {
const uint32_t shb = w[blk + ib32];
sl = d * (float)((int)((shb & 0xFF) >> 1) - 64);
const uint32_t offset = (shb & 1) != 0 ? 4096 + 32768 : 4096;
const uint32_t jj = 8 * ib32 + ig;
const uint32_t ql = rd_byte(w, blk + 8, jj);
const uint32_t jh = jj % 32;
const uint32_t qh = rd_byte(w, blk + 24, jh);
return offset + (ql | ((qh << (8 - 4 * (jj / 32))) & 0xF00) | (((shb >> (8 + 3 * ig)) & 7) << 12));
}
// the 32 weights of one subblock; shared by the dequant and mul_mat_vec mirrors, which
// differ in addressing, not in how a subblock is decoded
static void kt_emit_subblock(const uint32_t * w, uint32_t blk, uint32_t ib32, float d, float * out) {
for (uint32_t ig = 0; ig < 8; ++ig) {
float sl;
uint32_t x = kt_group_x(w, blk, ib32, ig, d, sl);
for (uint32_t k = 0; k < 4; ++k) {
out[4 * ig + k] = sl * kt_next(x);
}
}
}
static void kt_dequant_shader(const uint32_t * w, uint32_t ncols, uint32_t nrows, float * out) {
const uint32_t blocks_per_row = ncols / QK_K_;
for (uint32_t ib = 0; ib < blocks_per_row * nrows; ++ib) {
const uint32_t row_word = (ib / blocks_per_row) * a_row_words(ncols, KT_BLOCK_W);
const uint32_t blk = row_word + ROW_META_W + (ib % blocks_per_row) * KT_BLOCK_W;
const float d = bits_to_float(w[row_word]);
for (uint32_t ib32 = 0; ib32 < 8; ++ib32) {
kt_emit_subblock(w, blk, ib32, d, out + 256 * ib + 32 * ib32);
}
}
}
static void kt_get_rows_shader(const uint32_t * w, uint32_t ncols, uint32_t nrows, float * out) {
for (uint32_t r = 0; r < nrows; ++r) {
const uint32_t row_word = r * a_row_words(ncols, KT_BLOCK_W);
const float d = bits_to_float(w[row_word]);
for (uint32_t i00 = 0; i00 < ncols; i00 += 2) {
const uint32_t blk = row_word + ROW_META_W + (i00 / QK_K_) * KT_BLOCK_W;
const uint32_t pos = i00 % QK_K_;
float sl;
uint32_t x = kt_group_x(w, blk, pos / 32, (pos % 32) / 4, d, sl);
const uint32_t k0 = pos % 4;
float v0 = 0.0f;
float v1 = 0.0f;
for (uint32_t l = 0; l < 4; ++l) {
const float v = sl * kt_next(x);
if (l == k0 ) v0 = v;
if (l == k0 + 1) v1 = v;
}
out[r * ncols + i00 ] = v0;
out[r * ncols + i00 + 1] = v1;
}
}
}
static void kt_mul_mm_shader(const uint32_t * w, uint32_t ncols, uint32_t nrows, float * out) {
const uint32_t load_vec_a = 4;
for (uint32_t idx = 0; idx < nrows * ncols / load_vec_a; ++idx) {
const uint32_t elem = idx * load_vec_a;
const uint32_t row_word = (elem / ncols) * a_row_words(ncols, KT_BLOCK_W);
const uint32_t acol = elem % ncols;
const uint32_t blk = row_word + ROW_META_W + (acol / QK_K_) * KT_BLOCK_W;
float sl;
uint32_t x = kt_group_x(w, blk, (acol % QK_K_) / 32, (acol % 32) / 4, bits_to_float(w[row_word]), sl);
for (uint32_t k = 0; k < 4; ++k) {
out[elem + k] = sl * kt_next(x);
}
}
}
static void kt_mul_mat_vec_shader(const uint32_t * w, uint32_t ncols, uint32_t nrows, float * out) {
const uint32_t words_per_row = a_row_words(ncols, KT_BLOCK_W);
for (uint32_t i = 0; i < ncols / QK_K_; ++i) {
for (uint32_t ib32 = 0; ib32 < 8; ++ib32) {
uint32_t row_word = 0;
for (uint32_t n = 0; n < nrows; ++n) {
const uint32_t blk = row_word + ROW_META_W + i * KT_BLOCK_W;
kt_emit_subblock(w, blk, ib32, bits_to_float(w[row_word]),
out + n * ncols + i * QK_K_ + 32 * ib32);
row_word += words_per_row;
}
}
}
}
// ---------------------------------------------------------------------------------------
// Copied from QuantizerIQKT<32, 4, 15, false, true>::set_values in ggml/src/iqk/iqk_quantize.cpp.
// It reads the accumulator through int8_t, so it carries the same little-endian assumption
// the original has. Used only to cross-check kt_next() over the whole index range.
static void ref_set_values(uint32_t i, float * result, float scale, int offset) {
uint32_t x = i + offset;
const uint32_t ka = 0xCBAC1FED;
uint32_t s;
const int8_t * i8 = (const int8_t *)&s;
for (int k = 0; k < 4; ++k) {
x = ka*x;
s = x & 0x3f3f3f3f;
result[k] = scale*(i8[0] + i8[1] + i8[2] + i8[3] - 126.f);
}
}
static void check(const char * what, const std::vector<float> & got, const std::vector<float> & ref) {
g_checks++;
size_t bad = 0;
size_t first = 0;
for (size_t i = 0; i < ref.size(); ++i) {
if (got[i] != ref[i]) {
if (bad == 0) {
first = i;
}
bad++;
}
}
if (bad == 0) {
return;
}
g_failures++;
if (g_reported < 12) {
g_reported++;
printf(" FAIL %-42s %zu/%zu differ, first at %zu: got %.9g want %.9g\n",
what, bad, ref.size(), first, got[first], ref[first]);
}
}
// Fills a quantized buffer. payload < 0 gives a reproducible pseudo-random pattern,
// otherwise every payload byte is set to that value. Every byte pattern is a valid encoding
// for both types, so this reaches states a quantizer would never emit. The first eight
// bytes of every block are then overwritten with the given scale bytes.
static void fill_pattern(std::vector<uint32_t> & w, uint32_t ncols, uint32_t nrows, uint32_t block_words,
uint32_t words_per_row, uint32_t seed, int payload, const uint8_t * scales) {
const float row_scales[4] = { 0.0125f, -0.5f, 1.0f, 3.75e-3f };
uint32_t rng = seed * 2654435761u + 1u;
for (uint32_t r = 0; r < nrows; ++r) {
uint32_t bits;
const float d = row_scales[r % 4];
memcpy(&bits, &d, sizeof(bits));
w[r * words_per_row] = bits;
for (uint32_t k = ROW_META_W; k < words_per_row; ++k) {
if (payload >= 0) {
w[r * words_per_row + k] = 0x01010101u * (uint32_t)payload;
} else {
rng ^= rng << 13;
rng ^= rng >> 17;
rng ^= rng << 5;
w[r * words_per_row + k] = rng;
}
}
for (uint32_t b = 0; b < ncols / QK_K_; ++b) {
const uint32_t blk = r * words_per_row + ROW_META_W + b * block_words;
for (uint32_t k = 0; k < 8; ++k) {
const uint32_t word = blk + k / 4;
const uint32_t shift = 8 * (k % 4);
w[word] = (w[word] & ~(0xFFu << shift)) | ((uint32_t)scales[k] << shift);
}
}
}
}
static void run_shape(ggml_type type, uint32_t ncols, uint32_t nrows, uint32_t seed,
int payload, const uint8_t * scales, const char * tag) {
const uint32_t block_words = type == GGML_TYPE_IQ4_KS ? KS_BLOCK_W : KT_BLOCK_W;
// laid out with ggml's own row size, so that a wrong stride in a_row_words() shows up as
// a mismatch rather than shifting the reference along with the mirrors
const uint32_t words_per_row = (uint32_t)ggml_row_size(type, ncols) / sizeof(uint32_t);
std::vector<uint32_t> w(words_per_row * nrows);
fill_pattern(w, ncols, nrows, block_words, words_per_row, seed, payload, scales);
// ggml's own dequantizer, one row at a time
std::vector<float> ref((size_t)ncols * nrows);
ggml_type_traits_t tt = ggml_internal_get_type_traits(type);
for (uint32_t r = 0; r < nrows; ++r) {
tt.to_float(&w[r * words_per_row], &ref[(size_t)r * ncols], ncols);
}
struct mirror {
const char * family;
void (*fn)(const uint32_t *, uint32_t, uint32_t, float *);
};
const mirror ks[4] = {
{ "dequant", ks_dequant_shader },
{ "get_rows", ks_get_rows_shader },
{ "mul_mm", ks_mul_mm_shader },
{ "mul_mat_vec", ks_mul_mat_vec_shader },
};
const mirror kt[4] = {
{ "dequant", kt_dequant_shader },
{ "get_rows", kt_get_rows_shader },
{ "mul_mm", kt_mul_mm_shader },
{ "mul_mat_vec", kt_mul_mat_vec_shader },
};
const mirror * mirrors = type == GGML_TYPE_IQ4_KS ? ks : kt;
std::vector<float> got((size_t)ncols * nrows);
for (int m = 0; m < 4; ++m) {
std::fill(got.begin(), got.end(), 0.0f);
mirrors[m].fn(w.data(), ncols, nrows, got.data());
char name[128];
snprintf(name, sizeof(name), "%s %-11s %ux%u %s",
ggml_type_name(type), mirrors[m].family, nrows, ncols, tag);
check(name, got, ref);
}
}
static void run_trellis_sweep(void) {
const int offsets[2] = { 4096, 4096 + 32768 };
const float scales[3] = { 1.0f, -0.25f, 7.5f };
size_t bad = 0;
for (uint32_t idx = 0; idx < 32768; ++idx) {
for (int o = 0; o < 2; ++o) {
for (int s = 0; s < 3; ++s) {
float ref[4];
ref_set_values(idx, ref, scales[s], offsets[o]);
uint32_t x = (uint32_t)offsets[o] + idx;
for (int k = 0; k < 4; ++k) {
if (scales[s] * kt_next(x) != ref[k]) {
bad++;
}
}
}
}
}
g_checks++;
if (bad != 0) {
g_failures++;
printf(" FAIL iq4_kt trellis sweep: %zu of %d values differ\n", bad, 32768 * 2 * 3 * 4);
}
}
int main(void) {
// scale bytes chosen to hit both codebook halves and both ends of the scale field:
// IQ4_KS uses (b & 254) - 127, which reaches -127 and +127; IQ4_KT uses (b >> 1) - 64,
// which reaches -64 and +63. Bit 0 selects the codebook half / the index offset.
const uint8_t extremes[8] = { 0x00, 0x01, 0xFE, 0xFF, 0x80, 0x81, 0x7E, 0x7F };
const uint8_t mid[8] = { 0x40, 0x41, 0x7A, 0x0B, 0xC0, 0xC1, 0x2A, 0x95 };
// several row and block counts, so the row stride is exercised and not just the
// addressing inside a block
const uint32_t shapes[5][2] = { {256, 1}, {256, 3}, {1024, 5}, {1024, 4}, {4096, 2} };
printf("iq4_kt trellis: all 32768 indices x 2 offsets x 3 scales\n");
run_trellis_sweep();
printf("multi-row buffers against ggml to_float\n");
for (int s = 0; s < 5; ++s) {
run_shape(GGML_TYPE_IQ4_KS, shapes[s][0], shapes[s][1], 1u + s, -1, extremes, "rand/extreme-scales");
run_shape(GGML_TYPE_IQ4_KT, shapes[s][0], shapes[s][1], 1u + s, -1, extremes, "rand/extreme-scales");
run_shape(GGML_TYPE_IQ4_KS, shapes[s][0], shapes[s][1], 9u + s, -1, mid, "rand/mid-scales");
run_shape(GGML_TYPE_IQ4_KT, shapes[s][0], shapes[s][1], 9u + s, -1, mid, "rand/mid-scales");
}
printf("saturated payloads\n");
for (int p = 0; p <= 0xFF; p += 0xFF) {
run_shape(GGML_TYPE_IQ4_KS, 512, 2, 0, p, extremes, p == 0 ? "all-zero" : "all-ones");
run_shape(GGML_TYPE_IQ4_KT, 512, 2, 0, p, extremes, p == 0 ? "all-zero" : "all-ones");
run_shape(GGML_TYPE_IQ4_KS, 512, 2, 0, p, mid, p == 0 ? "all-zero" : "all-ones");
run_shape(GGML_TYPE_IQ4_KT, 512, 2, 0, p, mid, p == 0 ? "all-zero" : "all-ones");
}
printf("%d checks, %d failed\n", g_checks, g_failures);
return g_failures == 0 ? 0 : 1;
}