prefill: STRATA_PF_FUSED_KQ=1 - the fused native-expert path takes UD-Q4_K_XL (gfx11): gate/up Q4_K / Q5_K, down Q5_1 / Q8_0

The formats carry a minimum, so the activation blocks' spare bytes now hold the int8 code sums per 32 values (written by
every path, read by none else). Layers MMQ does not cover on HIP (Q4_K) join the fused layout through MmqPlan::fo.
Opt-in (default output bit-identical). Test pairs plus an int8-rounded model check; QA_SAME probe in the attention harness.
This commit is contained in:
Niko1221 committed 2026-10-04 14:16:58 +00:00
1 parent 65a48f0d57
commit 7440806987
4 files changed
+235 -49

No files matched your search

+11
View File
@@ -97,6 +97,17 @@ int run(int fmt, int64_t ctx, int64_t nq, int reps) { // fmt 1 int8, 0 fp16
std::sort(v.begin(), v.end());
std::copy(v.begin(), v.end(), sel);
}
// QA_SAME=1 (S23 probe): every query reads the same first-w cells - the best case of any K/V sharing between queries
// (all loads hit the caches); QA_SAME=2: query i reads a window of w cells at i / 4 * 64 (neighbours share ~90%)
if (const char* sm = std::getenv("QA_SAME")) {
const int mode = std::atoi(sm);
for (int64_t i = 0; i < nq; ++i) {
const int64_t w = steps[i * k::kStepCount + k::kStepWidth], nkv = ctx - nq + i + 1;
if (w == nkv) continue;
const int64_t base = mode == 1 ? 0 : std::min<int64_t>(i / 4 * 64, nkv - w);
for (int64_t c = 0; c < w; ++c) ids[i * cap + c] = (int32_t) (base + c);
}
}
k::QsaAttnPools pl;
if (fmt == 1) { pl.k_q = up(kq); pl.v_q = up(vq); pl.k_scale = up(ks); pl.v_scale = up(vs); }
else { pl.k_pool = up(kh); pl.v_pool = up(vh); }
+139 -25
View File
@@ -62,15 +62,28 @@ constexpr int GU_ROWS_K = 2560, D_ROWS_K = 640; // K of gate/up (n_embd) and o
// the formats (ggml type ids)
constexpr int T_IQ2_XXS = GGML_TYPE_IQ2_XXS, T_IQ2_XS = GGML_TYPE_IQ2_XS, T_IQ2_S = GGML_TYPE_IQ2_S,
T_IQ3_XXS = GGML_TYPE_IQ3_XXS, T_IQ3_S = GGML_TYPE_IQ3_S, T_IQ4_XS = GGML_TYPE_IQ4_XS,
T_IQ4_NL = GGML_TYPE_IQ4_NL, T_Q2_0 = GGML_TYPE_Q2_0;
T_IQ4_NL = GGML_TYPE_IQ4_NL, T_Q2_0 = GGML_TYPE_Q2_0,
// Aurora (opt-in, HIP gfx11 only): Unsloth UD-Q4_K_XL's experts - gate/up Q4_K (one layer Q5_K), down Q5_1
// (five layers Q8_0). Their weights are w = d_w q + m_w (a minimum), so a sub-block's dot is
// sum w x = d_x (d_w sum q a + m_w sum a):
// the activation blocks carry sum a per 32 values in their eight spare bytes (quant_act_nat_kernel and
// the H epilogue write it for every path; no other format reads it)
T_Q4_K = GGML_TYPE_Q4_K, T_Q5_K = GGML_TYPE_Q5_K, T_Q5_1 = GGML_TYPE_Q5_1, T_Q8_0 = GGML_TYPE_Q8_0;
static_assert(sizeof(block_iq2_xxs) == 66 && sizeof(block_iq2_xs) == 74 && sizeof(block_iq2_s) == 82 &&
sizeof(block_iq3_xxs) == 98 && sizeof(block_iq3_s) == 110 && sizeof(block_iq4_xs) == 136 &&
sizeof(block_iq4_nl) == 18 && sizeof(block_q2_0) == 18, "the block layouts this file decodes");
sizeof(block_iq4_nl) == 18 && sizeof(block_q2_0) == 18 && sizeof(block_q4_K) == 144 &&
sizeof(block_q5_K) == 176 && sizeof(block_q5_1) == 24 && sizeof(block_q8_0) == 34,
"the block layouts this file decodes");
// block bytes, scale per 16 values, codebook bytes in shared memory
__host__ __device__ constexpr int block_bytes(int t) {
return t == T_IQ2_XXS ? 66 : t == T_IQ2_XS ? 74 : t == T_IQ2_S ? 82 : t == T_IQ3_XXS ? 98 : t == T_IQ3_S ? 110
: t == T_IQ4_XS ? 136 : 18;
: t == T_IQ4_XS ? 136 : t == T_Q4_K ? 144 : t == T_Q5_K ? 176 : t == T_Q5_1 ? 24 : t == T_Q8_0 ? 34 : 18;
}
// the formats with a minimum (w = d q + m), and the raw words a sub-block's load stage holds
__host__ __device__ constexpr bool has_min(int t) { return t == T_Q4_K || t == T_Q5_K || t == T_Q5_1; }
__host__ __device__ constexpr int raw_words(int t) {
return t == T_Q4_K ? 10 : t == T_Q5_K ? 18 : t == T_Q5_1 ? 6 : t == T_Q8_0 ? 9 : 5;
}
__host__ __device__ constexpr bool per16(int t) { return t == T_IQ2_XS || t == T_IQ2_S; }
__host__ __device__ constexpr int grid_bytes(int t) {
@@ -107,9 +120,16 @@ __global__ void quant_act_nat_kernel(const float* __restrict__ x, int64_t nblk,
a1 = fmaxf(a1, __shfl_xor_sync(0xffffffffu, a1, o));
}
uint8_t* out = xa + w * AB;
out[lane] = (uint8_t) (int8_t) (a0 > 0.0f ? __float2int_rn(v0 * (127.0f / a0)) : 0);
out[32 + lane] = (uint8_t) (int8_t) (a1 > 0.0f ? __float2int_rn(v1 * (127.0f / a1)) : 0);
if (lane == 0) *(float4*) (out + 64) = make_float4(a0 / 127.0f, a1 / 127.0f, 0.0f, 0.0f);
const int c0 = a0 > 0.0f ? __float2int_rn(v0 * (127.0f / a0)) : 0, c1 = a1 > 0.0f ? __float2int_rn(v1 * (127.0f / a1)) : 0;
out[lane] = (uint8_t) (int8_t) c0;
out[32 + lane] = (uint8_t) (int8_t) c1;
int s0 = c0, s1 = c1; // the codes' sums per 32 values (the formats with a minimum read them)
#pragma unroll
for (int o = 16; o > 0; o >>= 1) {
s0 += __shfl_xor_sync(0xffffffffu, s0, o);
s1 += __shfl_xor_sync(0xffffffffu, s1, o);
}
if (lane == 0) *(float4*) (out + 64) = make_float4(a0 / 127.0f, a1 / 127.0f, (float) s0, (float) s1);
}
#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 800
@@ -180,7 +200,12 @@ __device__ __forceinline__ void table16(uint32_t q4, const uint32_t (&t)[4], uin
// Raw bytes of sub-block `ib` of the block at `bp` (IQ: the 256-value super-block, ib 0..7; Q2_0: the 64-value block,
// ib 0..1; IQ4_NL: the 32-value block).
template <int T> __device__ __forceinline__ void load_unit(const uint8_t* bp, int ib, uint32_t (&w)[5]) {
// 32 bits at a 2-byte aligned address (one load when it is 4-byte aligned)
__device__ __forceinline__ uint32_t ld32a(const uint8_t* p) {
return ((uintptr_t) p & 3) == 0 ? *(const uint32_t*) p : ld32(p);
}
template <int T, int NW> __device__ __forceinline__ void load_unit(const uint8_t* bp, int ib, uint32_t (&w)[NW]) {
static_assert(NW >= raw_words(T), "raw words");
if constexpr (T == T_IQ2_XXS) {
w[0] = ld32(bp + 2 + 8 * ib); w[1] = ld32(bp + 6 + 8 * ib); w[2] = ld16(bp);
} else if constexpr (T == T_IQ2_XS) {
@@ -202,16 +227,69 @@ template <int T> __device__ __forceinline__ void load_unit(const uint8_t* bp, in
#pragma unroll
for (int k = 0; k < 4; ++k) w[k] = ld32(bp + 2 + 4 * k);
w[4] = ld16(bp);
} else if constexpr (T == T_Q4_K || T == T_Q5_K) {
// qs: 32 bytes of the sub-block pair (ib / 2), the nibble chosen in convert; w[8] = d | dmin << 16; w[9]: the
// scale/min bytes (get_scale_min_k4's: ib < 4: sc[ib], sc[ib + 4]; else sc[ib + 4], sc[ib - 4], sc[ib]), ib << 24
constexpr int QS = T == T_Q4_K ? 16 : 48;
const uint8_t* q = bp + QS + 32 * (ib >> 1);
#pragma unroll
for (int k = 0; k < 8; ++k) w[k] = ld32a(q + 4 * k);
w[8] = ld32a(bp);
const uint8_t* sc = bp + 4;
w[9] = ib < 4 ? (uint32_t) sc[ib] | ((uint32_t) sc[ib + 4] << 8)
: (uint32_t) sc[ib + 4] | ((uint32_t) sc[ib - 4] << 8) | ((uint32_t) sc[ib] << 16);
w[9] |= (uint32_t) ib << 24;
if constexpr (T == T_Q5_K) {
#pragma unroll
for (int k = 0; k < 8; ++k) w[10 + k] = ld32a(bp + 16 + 4 * k); // qh
}
} else if constexpr (T == T_Q5_1) { // d, m, qh, qs[16]
#pragma unroll
for (int k = 0; k < 4; ++k) w[k] = ld32a(bp + 8 + 4 * k);
w[4] = ld32a(bp + 4);
w[5] = ld32a(bp);
} else if constexpr (T == T_Q8_0) { // d, qs[32]
#pragma unroll
for (int k = 0; k < 8; ++k) w[k] = ld32(bp + 2 + 4 * k);
w[8] = ld16(bp);
} else { // Q2_0
w[0] = ld32(bp + 2 + 8 * ib); w[1] = ld32(bp + 6 + 8 * ib); w[2] = ld16(bp);
}
}
// The sub-block as 32 int8 (q[0..7], natural order) and its scales (s0: values 0-15, s1: 16-31).
template <int T>
__device__ __forceinline__ void convert(const uint32_t (&w)[5], const uint8_t* grid, const uint32_t (&kv)[4],
template <int T, int NW>
__device__ __forceinline__ void convert(const uint32_t (&w)[NW], const uint8_t* grid, const uint32_t (&kv)[4],
uint32_t (&q)[8], float& s0, float& s1) {
if constexpr (T == T_IQ2_XXS) {
if constexpr (T == T_Q4_K || T == T_Q5_K) {
const int ib = (int) (w[9] >> 24), sh = 4 * (ib & 1);
uint32_t sc, mn;
if (ib < 4) { sc = w[9] & 63; mn = (w[9] >> 8) & 63; }
else {
sc = ((w[9] & 0xF) | (((w[9] >> 8) & 0xC0) >> 2)); // sc[ib + 4] & 15 | (sc[ib - 4] >> 6) << 4
mn = (((w[9]) >> 4) & 0xF) | (((w[9] >> 16) & 0xC0) >> 2); // sc[ib + 4] >> 4 | (sc[ib] >> 6) << 4
}
#pragma unroll
for (int k = 0; k < 8; ++k) {
q[k] = (w[k] >> sh) & 0x0F0F0F0Fu;
if constexpr (T == T_Q5_K) q[k] |= ((w[10 + k] >> ib) & 0x01010101u) << 4;
}
s0 = half_at(w[8]) * (float) sc;
s1 = -(half_at(w[8] >> 16) * (float) mn);
} else if constexpr (T == T_Q5_1) {
#pragma unroll
for (int k = 0; k < 4; ++k) {
const uint32_t lo = ((w[4] >> (4 * k)) & 0xF) * 0x00204081u, hi = ((w[4] >> (16 + 4 * k)) & 0xF) * 0x00204081u;
q[k] = (w[k] & 0x0F0F0F0Fu) | ((lo & 0x01010101u) << 4);
q[4 + k] = ((w[k] >> 4) & 0x0F0F0F0Fu) | ((hi & 0x01010101u) << 4);
}
s0 = half_at(w[5]);
s1 = half_at(w[5] >> 16);
} else if constexpr (T == T_Q8_0) {
#pragma unroll
for (int k = 0; k < 8; ++k) q[k] = w[k];
s0 = s1 = half_at(w[8]);
} else if constexpr (T == T_IQ2_XXS) {
const uint2* g = (const uint2*) grid;
#pragma unroll
for (int l = 0; l < 4; ++l) {
@@ -581,10 +659,10 @@ native_w11_kernel(const Batch b, const NativeGeom geo, const Tables tb, const ui
: blob + geo.down_off + (size_t) (rbase + ur) * geo.d_row;
auto unit = [&](int s) -> const uint8_t* {
if (GU) return wrow + (s >> 2) * BS;
return WT == T_IQ4_NL ? wrow + (2 * s + uj) * BS : wrow + s * BS;
return (WT == T_IQ4_NL || WT == T_Q5_1 || WT == T_Q8_0) ? wrow + (2 * s + uj) * BS : wrow + s * BS;
};
auto sub = [&](int s) { return GU ? 2 * (s & 3) + uj : uj; };
auto put = [&](const uint32_t (&raw)[5], int buf) {
auto put = [&](const uint32_t (&raw)[raw_words(WT)], int buf) {
uint32_t q[8];
float s0, s1;
convert<WT>(raw, sgrid, kv, q, s0, s1);
@@ -598,7 +676,7 @@ native_w11_kernel(const Batch b, const NativeGeom geo, const Tables tb, const ui
const int r = row0 + min(tid, nrows - 1); // rows past the tile's end repeat its last one
srow[tid] = GU ? src[r] : r;
}
uint32_t raw[5] = {0, 0, 0, 0, 0};
uint32_t raw[raw_words(WT)] = {};
load_unit<WT>(unit(0), sub(0), raw);
put(raw, 0);
if (NS > 1) load_unit<WT>(unit(1), sub(1), raw);
@@ -615,21 +693,23 @@ native_w11_kernel(const Batch b, const NativeGeom geo, const Tables tb, const ui
#pragma unroll
for (int i = 0; i < 8; ++i) acc[mt][nt][i] = 0.0f;
uint4 bq[2][4];
float2 bx[2];
auto fetch_b = [&](int s, uint4 (&q)[2][4], float2 (&x)[2]) {
float2 bx[2], bsum[2]; // the activation scales, and (the formats with a minimum) the codes' sums per 32 values
auto fetch_b = [&](int s, uint4 (&q)[2][4], float2 (&x)[2], float2 (&sm)[2]) {
#pragma unroll
for (int nt = 0; nt < 2; ++nt) {
const uint4* p = reinterpret_cast<const uint4*>(brow[nt] + s * AB);
q[nt][0] = p[0]; q[nt][1] = p[1]; q[nt][2] = p[2]; q[nt][3] = p[3];
x[nt] = *reinterpret_cast<const float2*>(brow[nt] + s * AB + 64);
if constexpr (has_min(WT)) sm[nt] = *reinterpret_cast<const float2*>(brow[nt] + s * AB + 72);
else sm[nt] = make_float2(0.0f, 0.0f);
}
};
if (on) fetch_b(0, bq, bx);
if (on) fetch_b(0, bq, bx, bsum);
for (int s = 0; s < NS; ++s) {
__syncthreads(); // stage s's weights are in buffer s & 1
uint4 nbq[2][4];
float2 nbx[2];
if (on && s + 1 < NS) fetch_b(s + 1, nbq, nbx);
float2 nbx[2], nbsum[2];
if (on && s + 1 < NS) fetch_b(s + 1, nbq, nbx, nbsum);
if (on) {
const int bf = s & 1;
#pragma unroll
@@ -660,7 +740,10 @@ native_w11_kernel(const Batch b, const NativeGeom geo, const Tables tb, const ui
} else {
const nw_i8 d = nw_wmma(A1, B1, nw_wmma(A0, B0, m));
#pragma unroll
for (int i = 0; i < 8; ++i) acc[mt][nt][i] = fmaf(w0[i] * dx, dotf(d[i]), acc[mt][nt][i]);
for (int i = 0; i < 8; ++i) {
acc[mt][nt][i] = fmaf(w0[i] * dx, dotf(d[i]), acc[mt][nt][i]);
if constexpr (has_min(WT)) acc[mt][nt][i] = fmaf(w1[i] * dx, h ? bsum[nt].y : bsum[nt].x, acc[mt][nt][i]);
}
}
}
}
@@ -673,6 +756,7 @@ native_w11_kernel(const Batch b, const NativeGeom geo, const Tables tb, const ui
#pragma unroll
for (int nt = 0; nt < 2; ++nt) {
bx[nt] = nbx[nt];
bsum[nt] = nbsum[nt];
#pragma unroll
for (int j = 0; j < 4; ++j) bq[nt][j] = nbq[nt][j];
}
@@ -703,13 +787,18 @@ native_w11_kernel(const Batch b, const NativeGeom geo, const Tables tb, const ui
const float inv = am > 0.0f ? 127.0f / am : 0.0f;
uint8_t* o = out + (size_t) (row0 + r) * (10 * AB) + (size_t) fb * AB;
uint32_t wd[8] = {};
int csum = 0;
#pragma unroll
for (int j = 0; j < 32; ++j)
wd[j >> 2] |= (uint32_t) (uint8_t) (int8_t) __float2int_rn(hs[r][32 * hh + j] * inv) << (8 * (j & 3));
for (int j = 0; j < 32; ++j) {
const int c = __float2int_rn(hs[r][32 * hh + j] * inv);
csum += c;
wd[j >> 2] |= (uint32_t) (uint8_t) (int8_t) c << (8 * (j & 3));
}
uint4* o4 = reinterpret_cast<uint4*>(o + 32 * hh);
o4[0] = make_uint4(wd[0], wd[1], wd[2], wd[3]);
o4[1] = make_uint4(wd[4], wd[5], wd[6], wd[7]);
*reinterpret_cast<float*>(o + 64 + 4 * hh) = am / 127.0f;
*reinterpret_cast<float*>(o + 72 + 4 * hh) = (float) csum; // (the formats with a minimum)
}
}
} else {
@@ -815,10 +904,29 @@ const DevInfo& dev_info() {
#endif
}
// opt-in on top of STRATA_PF_FUSED=1: STRATA_PF_FUSED_KQ=1 takes UD-Q4_K_XL's formats (the output differs from the FP16
// expert path's at rounding level; KL-gated, see aurora_s23.md)
bool kq_on() {
static const bool on = [] {
const char* v = std::getenv("STRATA_PF_FUSED_KQ");
return v != nullptr && v[0] == '1';
}();
return on;
}
bool gu_covered(int t) {
return t == T_IQ2_XXS || t == T_IQ2_XS || t == T_IQ2_S || t == T_IQ3_XXS || t == T_IQ3_S || t == T_IQ4_XS;
return t == T_IQ2_XXS || t == T_IQ2_XS || t == T_IQ2_S || t == T_IQ3_XXS || t == T_IQ3_S || t == T_IQ4_XS
#if defined(__HIPCC__)
|| ((t == T_Q4_K || t == T_Q5_K) && kq_on()) // the gfx11 kernels only
#endif
;
}
bool d_covered(int t) {
return t == T_Q2_0 || t == T_IQ4_NL
#if defined(__HIPCC__)
|| ((t == T_Q5_1 || t == T_Q8_0) && kq_on())
#endif
;
}
bool d_covered(int t) { return t == T_Q2_0 || t == T_IQ4_NL; }
template <int T, bool GU>
void launch(int ww, unsigned grid, const Batch& b, const NativeGeom& g, const Tables& tb, const void* act,
@@ -886,11 +994,17 @@ void experts_native(const Batch& b, const NativeGeom& g, int n_expert, int64_t n
case T_IQ2_S: STRATA_NW_GU(T_IQ2_S); break;
case T_IQ3_XXS: STRATA_NW_GU(T_IQ3_XXS); break;
case T_IQ3_S: STRATA_NW_GU(T_IQ3_S); break;
case T_Q4_K: STRATA_NW_GU(T_Q4_K); break;
case T_Q5_K: STRATA_NW_GU(T_Q5_K); break;
default: STRATA_NW_GU(T_IQ4_XS); break;
}
#undef STRATA_NW_GU
if (g.d_type == T_Q2_0) native_w11_kernel<T_Q2_0, false><<<g_d, NW_THREADS, 0, s>>>(b, g, tb, ha8, src, nullptr, dm);
else native_w11_kernel<T_IQ4_NL, false><<<g_d, NW_THREADS, 0, s>>>(b, g, tb, ha8, src, nullptr, dm);
#define STRATA_NW_D(T) native_w11_kernel<T, false><<<g_d, NW_THREADS, 0, s>>>(b, g, tb, ha8, src, nullptr, dm)
if (g.d_type == T_Q2_0) STRATA_NW_D(T_Q2_0);
else if (g.d_type == T_Q5_1) STRATA_NW_D(T_Q5_1);
else if (g.d_type == T_Q8_0) STRATA_NW_D(T_Q8_0);
else STRATA_NW_D(T_IQ4_NL);
#undef STRATA_NW_D
ck(cudaGetLastError(), "experts_native");
return;
}
+11 -2
View File
@@ -559,6 +559,8 @@ constexpr size_t MMQ_TAIL = 4096;
struct MmqPlan {
bool any = false, fallback = true;
std::vector<char> layer; // per layer: MMQ
std::vector<char> fo; // per layer: no MMQ here but the native fused kernels cover its formats
// (gfx11: UD-Q4_K_XL's Q4_K / Q5_K and Q5_1 / Q8_0 experts, STRATA_PF_FUSED=1)
size_t gu_max = 0, d_max = 0;
};
const MmqPlan& mmq_plan() {
@@ -569,11 +571,17 @@ const MmqPlan& mmq_plan() {
const bool on = mmq::built() && (env == nullptr || std::atoi(env) != 0);
const int64_t layers = lay.native ? (int64_t) lay.fmt.size() : lay.n_layers;
p.layer.assign((size_t) std::max<int64_t>(layers, 0), 0);
p.fo.assign(p.layer.size(), 0);
p.fallback = !on || layers <= 0;
for (int64_t l = 0; on && l < layers; ++l) {
const int gt = lay.native ? lay.fmt[(size_t) l].gu_type : 42, dt = lay.native ? lay.fmt[(size_t) l].d_type : 42;
// #420: a tile on every GPU for these shapes (gate+up: 1280 rows, down: N rows), else the FP16 path
if (!mmq::fits(gt, 1280) || !mmq::fits(dt, N)) { p.fallback = true; continue; }
if (!mmq::fits(gt, 1280) || !mmq::fits(dt, N)) {
p.fallback = true;
// the fused path's buffers (Xq, H) exist for it; a chunk it does not take (a small one) keeps the FP16 path
if (on && lay.native && fused::native_supported(gt, dt)) { p.fo[(size_t) l] = 1; p.any = true; }
continue;
}
p.layer[(size_t) l] = 1;
p.any = true;
p.gu_max = std::max(p.gu_max, mmq::matrix_bytes(gt, 1280, N));
@@ -1758,7 +1766,8 @@ bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& e
const bool use_mmq = mmq_plan().any && mmq_plan().layer[(size_t) l];
const int mmq_gt = lay.native ? lay.fmt[(size_t) l].gu_type : 42;
const int mmq_dt = lay.native ? lay.fmt[(size_t) l].d_type : 42;
const bool fused_nat = use_mmq && stream_all && lay.native && fused::native_supported(mmq_gt, mmq_dt);
const bool fused_only = mmq_plan().any && mmq_plan().fo[(size_t) l];
const bool fused_nat = (use_mmq || fused_only) && stream_all && lay.native && fused::native_supported(mmq_gt, mmq_dt);
const bool fused_l = (use_mmq && stream_all && !lay.native && fused::enabled()) || fused_nat;
size_t n_order = 0; // the routed experts (the debug report; unknown when fused)
if (fused_l) {
+74 -22
View File
@@ -57,6 +57,8 @@ struct Pair {
ggml_type gu, d;
const char* name;
};
// MMQ covers this pair (the HIP build has no Q4_K / Q5_K / Q5_1 MMQ: those pairs are checked against the reference only)
bool mmq_ok(const Pair& p) { return mmq::supported((int) p.gu) && mmq::supported((int) p.d); }
struct Geo {
size_t gu_row, d_row, up_off, down_off, bytes;
@@ -80,13 +82,18 @@ struct Geo {
void fill_matrix(uint8_t* m, ggml_type t, int64_t rows, int64_t cols, std::mt19937& rng) {
const size_t bs = ggml_type_size(t), nb = (size_t) rows * (size_t) (cols / ggml_blck_size(t));
std::uniform_int_distribution<int> byte(0, 255);
const float lo = t == GGML_TYPE_IQ3_S ? 0.0004f : t == GGML_TYPE_IQ4_XS ? 0.00005f
const float lo = t == GGML_TYPE_Q4_K || t == GGML_TYPE_Q5_K ? 0.00004f : t == GGML_TYPE_Q5_1 ? 0.0006f
: t == GGML_TYPE_Q8_0 ? 0.0001f : t == GGML_TYPE_IQ3_S ? 0.0004f : t == GGML_TYPE_IQ4_XS ? 0.00005f
: t == GGML_TYPE_IQ4_NL ? 0.0002f : t == GGML_TYPE_Q2_0 ? 0.004f : 0.0008f;
std::uniform_real_distribution<float> sc(lo, 6.0f * lo);
for (size_t i = 0; i < nb * bs; ++i) m[i] = (uint8_t) byte(rng);
for (size_t b = 0; b < nb; ++b) {
const ggml_fp16_t h = ggml_fp32_to_fp16(sc(rng));
std::memcpy(m + b * bs, &h, 2);
if (t == GGML_TYPE_Q4_K || t == GGML_TYPE_Q5_K || t == GGML_TYPE_Q5_1) { // the minimum's scale, either sign
const ggml_fp16_t h2 = ggml_fp32_to_fp16(sc(rng) * ((rng() & 1) ? 1.0f : -1.0f) * (t == GGML_TYPE_Q5_1 ? 4.0f : 1.0f));
std::memcpy(m + b * bs + 2, &h2, 2);
}
}
}
std::vector<uint8_t> make_blob(const Pair& p, const Geo& g, std::mt19937& rng) {
@@ -271,6 +278,16 @@ Err compare(const std::vector<float>& a, RA row_a, const std::vector<float>& b,
return out;
}
// the kernels' int8 rounding per 32 values (quant_act_nat_kernel / the H epilogue), as floats: codes * (amax / 127)
void quant32(const float* in, float* out, int n) {
for (int b = 0; b < n; b += 32) {
float am = 0.0f;
for (int j = 0; j < 32; ++j) am = std::max(am, std::fabs(in[b + j]));
const float inv = am > 0.0f ? 127.0f / am : 0.0f, d = am / 127.0f;
for (int j = 0; j < 32; ++j) out[b + j] = (float) std::nearbyint(in[b + j] * inv) * d;
}
}
std::vector<float> download(const Dev& d, size_t n) {
std::vector<float> h(n);
ck(cudaMemcpy(h.data(), d.p, n * 4, cudaMemcpyDeviceToHost), "download");
@@ -310,27 +327,30 @@ void reference_part(const Pair& p, cudaStream_t s) {
// (the uploads above are on the legacy stream and the work below on a non-blocking one: a cudaMemcpy from pageable
// memory may return before its DMA lands - on Windows it can wait in the queue until the next synchronous call)
ck(cudaDeviceSynchronize(), "uploads");
const bool mq = mmq_ok(p);
mmq::Context ctx;
MmqBufs mb(p, rows, E);
run_mmq(p, geo, ctx, mb, r, x_dev.as<float>(), src_dev.as<int32_t>(), blob, rows, s);
MmqBufs* mbp = mq ? new MmqBufs(p, rows, E) : nullptr;
if (mq) run_mmq(p, geo, ctx, *mbp, r, x_dev.as<float>(), src_dev.as<int32_t>(), blob, rows, s);
FusedBufs fb(T, rows, E);
ck(cudaMemcpy(fb.ids.p, ids.data(), ids.size() * 4, cudaMemcpyHostToDevice), "ids");
ck(cudaMemset(fb.dm.p, 0xff, (size_t) rows * N * 4), "sentinel"); // an unwritten row is NaN
ck(cudaDeviceSynchronize(), "uploads");
run_fused(p, geo, fb, x_dev.as<float>(), T, E, {0, 1, 20, E}, blob, s);
ck(cudaStreamSynchronize(s), "sync");
const std::vector<float> y_mmq = download(mb.dm, (size_t) rows * N), y_f = download(fb.dm, (size_t) rows * N);
const std::vector<float> y_mmq = mq ? download(mbp->dm, (size_t) rows * N) : std::vector<float>((size_t) rows * N, 0.0f),
y_f = download(fb.dm, (size_t) rows * N);
delete mbp;
const std::vector<int32_t> slot = download_i(fb.slot, (size_t) rows), fsrc = download_i(fb.src, (size_t) rows);
for (int64_t i = 0; i < rows; ++i)
if (fsrc[(size_t) slot[(size_t) i]] != (int32_t) (i / K)) throw std::runtime_error("group: slot/src disagree");
// the reference, an expert at a time on all cores
std::vector<float> ref((size_t) rows * N);
std::vector<float> ref((size_t) rows * N), ref2((size_t) rows * N); // ref2: the same with the kernels' int8 rounding
std::vector<std::thread> th;
const int nth = std::max(1, std::min(16, (int) std::thread::hardware_concurrency()));
for (int w = 0; w < nth; ++w)
th.emplace_back([&, w] {
std::vector<float> gu, dn, h(FF);
std::vector<float> gu, dn, h(FF), h2(FF), hq(FF), xq(N);
for (int e = w; e < E; e += nth) {
if (r.cnt[(size_t) e] == 0) continue;
dequant(p, geo, host[(size_t) e], gu, dn);
@@ -349,6 +369,20 @@ void reference_part(const Pair& p, cudaStream_t s) {
for (int f = 0; f < FF; ++f) a += (double) wd[f] * h[(size_t) f];
ref[(size_t) i * N + o] = (float) a;
}
quant32(xr, xq.data(), N);
for (int f = 0; f < FF; ++f) {
double gt = 0, up = 0;
const float *wg = gu.data() + (size_t) f * N, *wu = gu.data() + (size_t) (FF + f) * N;
for (int k = 0; k < N; ++k) { gt += (double) wg[k] * xq[(size_t) k]; up += (double) wu[k] * xq[(size_t) k]; }
h2[(size_t) f] = (float) (gt / (1.0 + std::exp(-gt)) * up);
}
quant32(h2.data(), hq.data(), FF);
for (int o = 0; o < N; ++o) {
double a = 0;
const float* wd = dn.data() + (size_t) o * FF;
for (int f = 0; f < FF; ++f) a += (double) wd[f] * hq[(size_t) f];
ref2[(size_t) i * N + o] = (float) a;
}
}
}
});
@@ -357,17 +391,27 @@ void reference_part(const Pair& p, cudaStream_t s) {
auto pair = [](size_t i) { return (int64_t) i; };
auto mrow = [&](size_t i) { return (int64_t) r.row_of[i]; };
auto frow = [&](size_t i) { return (int64_t) slot[i]; };
const Err em = compare(y_mmq, mrow, ref, pair, (size_t) rows);
const Err em = mq ? compare(y_mmq, mrow, ref, pair, (size_t) rows) : Err{};
const Err ef = compare(y_f, frow, ref, pair, (size_t) rows);
const Err efm = compare(y_f, frow, y_mmq, mrow, (size_t) rows);
std::printf(" MMQ vs FP32 reference: rel RMS %.3e worst row max rel %.3e\n", em.rms, em.worst);
const Err efm = mq ? compare(y_f, frow, y_mmq, mrow, (size_t) rows) : Err{};
const Err e2 = compare(y_f, frow, ref2, pair, (size_t) rows);
std::printf(" fused vs the int8-rounded model (same activation / H rounding, double sums): rel RMS %.3e worst %.3e\n",
e2.rms, e2.worst);
if (e2.rms > 2e-3 || e2.worst > 3e-2)
throw std::runtime_error(std::string(p.name) + ": the fused path differs from its own arithmetic's model");
if (mq) std::printf(" MMQ vs FP32 reference: rel RMS %.3e worst row max rel %.3e\n", em.rms, em.worst);
std::printf(" fused vs FP32 reference: rel RMS %.3e worst row max rel %.3e\n", ef.rms, ef.worst);
std::printf(" fused vs MMQ : rel RMS %.3e worst row max rel %.3e\n", efm.rms, efm.worst);
if (mq) std::printf(" fused vs MMQ : rel RMS %.3e worst row max rel %.3e\n", efm.rms, efm.worst);
double zmax = 0;
for (int k = 0; k < K; ++k)
for (int o = 0; o < N; ++o)
zmax = std::max(zmax, (double) std::fabs(y_f[(size_t) slot[(size_t) (ZERO * K + k)] * N + o]));
if (zmax != 0) throw std::runtime_error(std::string(p.name) + ": the all-zero token's outputs are not zero");
if (!mq) { // no MMQ here: the int8 activations' own error (the IQ pairs' fused path sits at ~1e-2 rel RMS)
if (ef.rms > 0.03 || ef.worst > 0.25)
throw std::runtime_error(std::string(p.name) + ": the fused path's error against the reference is too large");
return;
}
if (ef.rms > 1.5 * em.rms || ef.worst > 2.0 * em.worst)
throw std::runtime_error(std::string(p.name) + ": the fused path's error is not comparable to MMQ's");
}
@@ -401,8 +445,10 @@ void timing_part(const Pair& p, int T, cudaStream_t s) {
Dev x_dev(x.size() * 4), src_dev((size_t) rows * 4);
ck(cudaMemcpy(x_dev.p, x.data(), x.size() * 4, cudaMemcpyHostToDevice), "x");
ck(cudaMemcpy(src_dev.p, r.src.data(), r.src.size() * 4, cudaMemcpyHostToDevice), "src");
const bool mq = mmq_ok(p);
mmq::Context ctx;
MmqBufs mb(p, rows, E);
MmqBufs* mbp = mq ? new MmqBufs(p, rows, E) : nullptr;
MmqBufs& mb = *mbp; // (only used when mq)
FusedBufs fb(T, rows, E);
ck(cudaMemcpy(fb.ids.p, ids.data(), ids.size() * 4, cudaMemcpyHostToDevice), "ids");
ck(cudaDeviceSynchronize(), "uploads"); // (see reference_part)
@@ -420,7 +466,7 @@ void timing_part(const Pair& p, int T, cudaStream_t s) {
};
auto mmq_run = [&] { run_mmq(p, geo, ctx, mb, r, x_dev.as<float>(), src_dev.as<int32_t>(), blob, rows, s); };
auto fused_run = [&] { run_fused(p, geo, fb, x_dev.as<float>(), T, E, {0, E}, blob, s); };
const bool all_routed = std::count(r.cnt.begin(), r.cnt.end(), 0) == 0;
const bool all_routed = mq && std::count(r.cnt.begin(), r.cnt.end(), 0) == 0;
auto direct_run = [&] {
run_mmq(p, geo, ctx, mb, r, x_dev.as<float>(), src_dev.as<int32_t>(), blob, rows, s, true, stride);
};
@@ -446,7 +492,7 @@ void timing_part(const Pair& p, int T, cudaStream_t s) {
}
}
// ~1 s of both first (the clocks ramp up), then alternating rounds; the medians
const bool no_mmq = std::getenv("S20_NOMMQ") != nullptr; // (debug: the fused runs alone)
const bool no_mmq = !mq || std::getenv("S20_NOMMQ") != nullptr; // (debug: the fused runs alone)
for (float spent = 0; spent < 1000.0f;) spent += (no_mmq ? 0.0f : once(mmq_run)) + once(fused_run);
std::vector<float> tm, tf, td;
for (int rep = 0; rep < 8; ++rep) {
@@ -467,10 +513,11 @@ void timing_part(const Pair& p, int T, cudaStream_t s) {
std::sort(tf2.begin(), tf2.end());
std::printf(" (fused back to back: median %.3f ms, min %.3f; alternating min %.3f; MMQ min %.3f)\n", tf2[4],
tf2[0], tf[0], tm[0]);
const std::vector<float> y_mmq = download(mb.dm, (size_t) rows * N), y_f = download(fb.dm, (size_t) rows * N);
const std::vector<float> y_mmq = mq ? download(mb.dm, (size_t) rows * N) : std::vector<float>((size_t) rows * N, 0.0f),
y_f = download(fb.dm, (size_t) rows * N);
const std::vector<int32_t> slot = download_i(fb.slot, (size_t) rows);
const Err efm = compare(y_f, [&](size_t i) { return (int64_t) slot[i]; }, y_mmq,
[&](size_t i) { return (int64_t) r.row_of[i]; }, (size_t) rows);
const Err efm = mq ? compare(y_f, [&](size_t i) { return (int64_t) slot[i]; }, y_mmq,
[&](size_t i) { return (int64_t) r.row_of[i]; }, (size_t) rows) : Err{};
std::printf("%s - timing part: %d tokens x top %d over %d experts, one layer: MMQ path %.3f ms, fused %.3f ms "
"(%.2fx); fused vs MMQ rel RMS %.3e worst row %.3e\n",
p.name, T, K, E, t_mmq, t_f, t_mmq / t_f, efm.rms, efm.worst);
@@ -560,18 +607,19 @@ void timing_part(const Pair& p, int T, cudaStream_t s) {
for (int f = 0; f < FF; ++f) a += (double) wd[f] * h[(size_t) f];
ref[j * N + o] = (float) a;
}
std::copy_n(y_mmq.begin() + (ptrdiff_t) ((size_t) r.row_of[i] * N), N, ym.begin() + (ptrdiff_t) (j * N));
if (mq) std::copy_n(y_mmq.begin() + (ptrdiff_t) ((size_t) r.row_of[i] * N), N, ym.begin() + (ptrdiff_t) (j * N));
std::copy_n(y_f.begin() + (ptrdiff_t) ((size_t) slot[i] * N), N, yf.begin() + (ptrdiff_t) (j * N));
}
auto id = [](size_t j) { return (int64_t) j; };
const Err em = compare(ym, id, ref, id, pick.size()), ef = compare(yf, id, ref, id, pick.size());
const Err em = mq ? compare(ym, id, ref, id, pick.size()) : Err{}, ef = compare(yf, id, ref, id, pick.size());
std::printf(" sample of %zu pairs vs FP32 reference: MMQ rel RMS %.3e worst %.3e, fused rel RMS %.3e worst "
"%.3e\n", pick.size(), em.rms, em.worst, ef.rms, ef.worst);
if (ef.rms > 1.5 * em.rms || ef.worst > 2.0 * em.worst)
if (!mq ? (ef.rms > 0.03 || ef.worst > 0.25) : (ef.rms > 1.5 * em.rms || ef.worst > 2.0 * em.worst))
throw std::runtime_error(std::string(p.name) + ": the fused path's error is not comparable to MMQ's (timing)");
}
cudaEventDestroy(a);
cudaEventDestroy(b);
delete mbp;
if (efm.rms > 0.05) throw std::runtime_error(std::string(p.name) + ": the paths disagree at the real shape");
}
} // namespace
@@ -582,6 +630,7 @@ int main(int argc, char** argv) {
_putenv_s("STRATA_PF_FUSED", "1");
#else
setenv("STRATA_PF_FUSED", "1", 1);
setenv("STRATA_PF_FUSED_KQ", "1", 1); // (the HIP build's Q4_K / Q5_K / Q5_1 / Q8_0 pairs)
#endif
int n = 0;
if (cudaGetDeviceCount(&n) != cudaSuccess || n == 0) { std::printf("no CUDA device: skipped\n"); return 77; }
@@ -590,7 +639,10 @@ int main(int argc, char** argv) {
const std::vector<Pair> pairs = {
{GGML_TYPE_IQ2_S, GGML_TYPE_Q2_0, "IQ2_S / Q2_0"}, {GGML_TYPE_IQ2_XXS, GGML_TYPE_Q2_0, "IQ2_XXS / Q2_0"},
{GGML_TYPE_IQ2_XS, GGML_TYPE_IQ4_NL, "IQ2_XS / IQ4_NL"}, {GGML_TYPE_IQ3_XXS, GGML_TYPE_IQ4_NL, "IQ3_XXS / IQ4_NL"},
{GGML_TYPE_IQ3_S, GGML_TYPE_IQ4_NL, "IQ3_S / IQ4_NL"}, {GGML_TYPE_IQ4_XS, GGML_TYPE_Q2_0, "IQ4_XS / Q2_0"}};
{GGML_TYPE_IQ3_S, GGML_TYPE_IQ4_NL, "IQ3_S / IQ4_NL"}, {GGML_TYPE_IQ4_XS, GGML_TYPE_Q2_0, "IQ4_XS / Q2_0"},
// (HIP only) UD-Q4_K_XL's: 47 layers Q4_K / Q5_1, 4 Q4_K / Q8_0, and the one Q5_K layer
{GGML_TYPE_Q4_K, GGML_TYPE_Q5_1, "Q4_K / Q5_1"}, {GGML_TYPE_Q4_K, GGML_TYPE_Q8_0, "Q4_K / Q8_0"},
{GGML_TYPE_Q5_K, GGML_TYPE_Q5_1, "Q5_K / Q5_1"}, {GGML_TYPE_Q5_K, GGML_TYPE_Q8_0, "Q5_K / Q8_0"}};
const char* only = nullptr; // --only=NAME: the pairs whose name contains NAME
for (int i = 1; i < argc; ++i)
if (std::strncmp(argv[i], "--only=", 7) == 0) only = argv[i] + 7;
@@ -611,14 +663,14 @@ int main(int argc, char** argv) {
cudaStream_t s = nullptr;
ck(cudaStreamCreateWithFlags(&s, cudaStreamNonBlocking), "stream");
for (const Pair& p : pairs) {
if (only && std::string(p.name).find(only) == std::string::npos) continue;
if (only ? std::string(p.name).find(only) == std::string::npos : p.gu == GGML_TYPE_Q4_K || p.gu == GGML_TYPE_Q5_K) continue;
if (!fused::native_supported((int) p.gu, (int) p.d))
throw std::runtime_error(std::string(p.name) + ": not covered by the native kernels");
if (ref) reference_part(p, s);
}
if (timing) {
// the IQ2_XS pack's layers (34 of 48 IQ2_S / Q2_0, 11 IQ2_XXS / Q2_0), the IQ3_S pack's most common, IQ3_XXS's
for (const Pair& p : {pairs[0], pairs[1], pairs[4], pairs[3]}) {
for (const Pair& p : only ? pairs : std::vector<Pair>{pairs[0], pairs[1], pairs[4], pairs[3]}) {
if (only && std::string(p.name).find(only) == std::string::npos) continue;
for (int T : chunks) timing_part(p, T, s);
}