ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
kv_quant.h
Go to the documentation of this file.
1#pragma once
2// Quantised KV cache for the IR interpreter (EMBER_KV=f32|f16|q8|q4; research item R2,
3// experiments/kv_quant_r2_2026-10-28). f32 is the default and leaves every existing path as it was.
4//
5// A cache row is one position's [kv_heads][head_dim] floats. The formats, per row:
6// f16 IEEE half per element, round to nearest even (2 bytes per element).
7// q8 blocks of 32 consecutive elements (whole blocks inside a head: head_dim % 32 == 0), llama.cpp's
8// q8_0 rule: d = half(max |x| / 127), q = clamp(rint(x / d), -127, 127), x' = q * d
9// (1 byte per element + a half scale per block: 1.0625 bytes per element).
10// q4 blocks of 32, asymmetric (llama.cpp's q4_1 form): d = half((max - min) / 15), m = half(min),
11// q = clamp(rint((x - m) / d), 0, 15), x' = fma(q, d, m); element 2i in the low nibble of byte i,
12// 2i + 1 in the high one; (d, m) a half pair per block (0.625 bytes per element).
13// Every rounding is explicit (division, product and fma rounded once each, rint half to even), so the
14// CPU form below and the CUDA kernels (kv_quant.cu) quantise a given float to the same code.
15//
16// CPU: the interpreter keeps its f32 cache and replaces each written row by its dequantised value
17// (roundtrip_rows): the numerics of a quantised cache, not its memory. CUDA stores the codes and
18// dequantises in the attention kernels' load path: the tiled split-K decode kernel's form
19// (decode_attn_tiled.cuh) reading codes, and prefill through a dequantised f32 copy of the rows
20// [0, start_pos + T) of the layer (one copy shared by the layers) read by the existing prefill kernel.
21#include <cmath>
22#include <cstddef>
23#include <cstdint>
24#include <cstring>
25#include <stdexcept>
26#include <string>
27#ifdef HAS_CUDA
28#include <cuda_runtime.h>
29#endif
30
31namespace ember_kvq {
32
33enum Format : int { kF32 = 0, kF16 = 1, kQ8 = 2, kQ4 = 3 };
34constexpr int kBlock = 32;
35
36// EMBER_KV's value: "" or "f32" -> kF32, "f16", "q8", "q4"; anything else throws.
37inline Format parse_format(const char* s) {
38 const std::string v = s ? s : "";
39 if (v.empty() || v == "f32") return kF32;
40 if (v == "f16") return kF16;
41 if (v == "q8") return kQ8;
42 if (v == "q4") return kQ4;
43 throw std::runtime_error("EMBER_KV=" + v + ": expected f32, f16, q8 or q4");
44}
45inline const char* format_name(int f) { return f == kF16 ? "f16" : f == kQ8 ? "q8" : f == kQ4 ? "q4" : "f32"; }
46
47// Bytes of one row's codes and of its scales (row = kv_heads * head_dim elements).
48inline size_t data_bytes(int f, size_t row) {
49 return f == kF16 ? row * 2 : f == kQ8 ? row : f == kQ4 ? row / 2 : row * 4;
50}
51inline size_t scale_bytes(int f, size_t row) {
52 return f == kQ8 ? row / kBlock * 2 : f == kQ4 ? row / kBlock * 4 : 0;
53}
54// Why a shape cannot take format f ("" when it can).
55inline std::string shape_error(int f, int head_dim) {
56 if (f == kF32) return "";
57 if (head_dim % 4 != 0) return "head_dim % 4 != 0";
58 if ((f == kQ8 || f == kQ4) && head_dim % kBlock != 0) return "head_dim % 32 != 0 (q8 and q4 blocks sit inside a head)";
59 return "";
60}
61
62// IEEE binary16 conversions, round to nearest even (CUDA's __float2half_rn), subnormals and
63// infinities included; NaN stays NaN.
64inline uint16_t f32_to_f16(float x) {
65 uint32_t u;
66 std::memcpy(&u, &x, 4);
67 const uint32_t sign = (u >> 16) & 0x8000u;
68 const uint32_t ax = u & 0x7fffffffu;
69 if (ax >= 0x7f800000u) return static_cast<uint16_t>(sign | (ax > 0x7f800000u ? 0x7e00u : 0x7c00u));
70 if (ax >= 0x477ff000u) return static_cast<uint16_t>(sign | 0x7c00u); // rounds past 65504: infinity
71 if (ax < 0x38800000u) { // below 2^-14: a half subnormal (or zero)
72 if (ax < 0x33000000u) return static_cast<uint16_t>(sign); // below half the smallest subnormal: 0
73 const uint32_t e = ax >> 23;
74 const uint32_t m = (ax & 0x7fffffu) | 0x800000u;
75 const uint32_t shift = 126 - e; // r = m * 2^(e - 126) units of 2^-24, m with its implicit bit
76 const uint32_t base = m >> shift;
77 const uint32_t rem = m & ((1u << shift) - 1u);
78 const uint32_t half_ulp = 1u << (shift - 1);
79 uint32_t r = base;
80 if (rem > half_ulp || (rem == half_ulp && (base & 1u))) ++r;
81 return static_cast<uint16_t>(sign | r);
82 }
83 uint32_t r = ((ax >> 13) - (112u << 10)); // rebias the exponent, keep 10 mantissa bits
84 const uint32_t rem = ax & 0x1fffu;
85 if (rem > 0x1000u || (rem == 0x1000u && (r & 1u))) ++r; // a carry into the exponent is correct
86 return static_cast<uint16_t>(sign | r);
87}
88inline float f16_to_f32(uint16_t h) {
89 const uint32_t sign = static_cast<uint32_t>(h & 0x8000u) << 16;
90 const uint32_t e = (h >> 10) & 0x1fu, m = h & 0x3ffu;
91 uint32_t u;
92 if (e == 0) {
93 if (m == 0) {
94 u = sign;
95 } else { // subnormal: m * 2^-24, exact in float
96 const float v = static_cast<float>(m) * 5.9604644775390625e-8f;
97 std::memcpy(&u, &v, 4);
98 u |= sign;
99 }
100 } else if (e == 31) {
101 u = sign | 0x7f800000u | (m << 13);
102 } else {
103 u = sign | ((e + 112u) << 23) | (m << 13);
104 }
105 float f;
106 std::memcpy(&f, &u, 4);
107 return f;
108}
109
110// One block of 32 floats quantised and dequantised in place (q8, q4) or each element through half (f16).
111inline void roundtrip_block(int f, float* x) {
112 if (f == kQ8) {
113 float amax = 0.0f;
114 for (int i = 0; i < kBlock; ++i) amax = std::fmax(amax, std::fabs(x[i]));
115 const float d = f16_to_f32(f32_to_f16(amax / 127.0f));
116 for (int i = 0; i < kBlock; ++i) {
117 float q = d != 0.0f ? std::nearbyint(x[i] / d) : 0.0f;
118 q = std::fmin(127.0f, std::fmax(-127.0f, q));
119 x[i] = q * d;
120 }
121 } else if (f == kQ4) {
122 float mn = x[0], mx = x[0];
123 for (int i = 1; i < kBlock; ++i) {
124 mn = std::fmin(mn, x[i]);
125 mx = std::fmax(mx, x[i]);
126 }
127 const float d = f16_to_f32(f32_to_f16((mx - mn) / 15.0f));
128 const float m = f16_to_f32(f32_to_f16(mn));
129 for (int i = 0; i < kBlock; ++i) {
130 float q = d != 0.0f ? std::nearbyint((x[i] - m) / d) : 0.0f;
131 q = std::fmin(15.0f, std::fmax(0.0f, q));
132 x[i] = std::fma(q, d, m);
133 }
134 }
135}
136
137// rows rows of `row` floats each, in place: what the cache gives back after storing them in format f.
138inline void roundtrip_rows(int f, float* x, size_t rows, size_t row) {
139 if (f == kF32) return;
140 const size_t n = rows * row;
141 if (f == kF16) {
142 for (size_t i = 0; i < n; ++i) x[i] = f16_to_f32(f32_to_f16(x[i]));
143 return;
144 }
145 for (size_t b = 0; b + kBlock <= n; b += kBlock) roundtrip_block(f, x + b);
146}
147
148#ifdef HAS_CUDA
149// The rows of k, v [T][row] stored in format f at positions start_pos + t (or *dpos + t when dpos is
150// given: the captured decode step). Kd/Vd: codes [max_len][data_bytes(row)]; Ks/Vs: scales
151// [max_len][scale_bytes(row)] (null for f16).
152void write_cuda(int f, void* Kd, void* Ks, void* Vd, void* Vs, const float* k, const float* v, int T, int row,
153 int start_pos, cudaStream_t stream, const int* dpos = nullptr);
154// One query row per head (q [hq][hd]) over the L = seq_len (or *dpos + 1) cached positions: the tiled
155// split-K decode attention (decode_attn_tiled.cuh's chunking, lane layout and merge), codes dequantised as
156// they are loaded. part: partial_floats(hq, hd, max_len) floats of scratch.
157size_t partial_floats(int hq, int hd, int max_len);
158void decode_cuda(int f, const float* q, const void* Kd, const void* Ks, const void* Vd, const void* Vs, float* out,
159 int hq, int hkv, int hd, float scale, int max_len, int seq_len, float* part, cudaStream_t stream,
160 const int* dpos = nullptr);
161// Rows [0, n) dequantised to f32 [n][row] (for the prefill kernel).
162void dequant_rows_cuda(int f, const void* Kd, const void* Ks, const void* Vd, const void* Vs, float* K, float* V, int n,
163 int row, cudaStream_t stream);
164#endif
165
166} // namespace ember_kvq
int n
std::vector< float > v
node out
node scale
size_t j
const char * format_name(int f)
Definition kv_quant.h:45
void write_cuda(int f, void *Kd, void *Ks, void *Vd, void *Vs, const float *k, const float *v, int T, int row, int start_pos, cudaStream_t st, const int *dpos)
Definition kv_quant.cu:260
size_t partial_floats(int hq, int hd, int max_len)
Definition kv_quant.cu:284
void decode_cuda(int f, const float *q, const void *Kd, const void *Ks, const void *Vd, const void *Vs, float *out, int hq, int hkv, int hd, float scale, int max_len, int seq_len, float *part, cudaStream_t st, const int *dpos)
Definition kv_quant.cu:288
uint16_t f32_to_f16(float x)
Definition kv_quant.h:64
size_t data_bytes(int f, size_t row)
Definition kv_quant.h:48
void dequant_rows_cuda(int f, const void *Kd, const void *Ks, const void *Vd, const void *Vs, float *K, float *V, int n, int row, cudaStream_t st)
Definition kv_quant.cu:303
void roundtrip_block(int f, float *x)
Definition kv_quant.h:111
std::string shape_error(int f, int head_dim)
Definition kv_quant.h:55
size_t scale_bytes(int f, size_t row)
Definition kv_quant.h:51
constexpr int kBlock
Definition kv_quant.h:34
Format parse_format(const char *s)
Definition kv_quant.h:37
float f16_to_f32(uint16_t h)
Definition kv_quant.h:88
void roundtrip_rows(int f, float *x, size_t rows, size_t row)
Definition kv_quant.h:138