Q5_0 expert dot: the -16 offset takes the sum of the quantized activations (CUDA/HIP and SYCL), like Q5_1 and Q4_0

vec_dot_q5_0_q8_1 used the q8_1 block's ds.y (the sum of the original activations). With the SwiGLU outputs (not zero-mean) the whole-expert error with Q5_0 down was 2.1e-2 .. 3.1e-2 against 1.2e-2 for every other down type. Now 1.2e-2, and the GPU agrees with the CPU path. The Q5_0 parity limit is tightened to 1.8e-2.
This commit is contained in:
Niko1221 committed 2026-11-05 09:07:47 +01:00
1 parent edb7c4819b
commit aa678f10d0
4 files changed
+23 -14

No files matched your search

+9 -6
View File
@@ -435,13 +435,16 @@ __device__ __forceinline__ float vec_dot_q5_K_q8_1(const void* __restrict__ vbq,
// of the ORIGINAL activations. That is ggml-cpu's convention (its q8_1 `s` is d * sum(q)) and the one the K-quant
// mins above use; the scaled and the min term then see the same activation (eddoursul/Strata measured 1.1-1.2%
// against 1.9% relative error per expert). Result: sumi * (d5 * d8) + sumu * (m5 * d8).
// Q5_0: llama.cpp's integer chain verbatim (vecdotq.cuh vec_dot_q5_0_q8_1_impl) -- symmetric (one delta, no
// learned min), so unlike Q5_1 there is no min-term rounding choice to make: the constant -16 per-weight offset
// is accounted for via the q8_1 block's `ds.y` (sum of the ORIGINAL activations), exactly as upstream.
// Q5_0: llama.cpp's integer chain (vecdotq.cuh vec_dot_q5_0_q8_1_impl), but the constant -16 per-weight offset multiplies
// the sum of the QUANTIZED activations (dp4a with 0x01010101, times d8), like the Q5_1 and Q4_0 paths and ggml-cpu's q8_0
// dot, instead of the q8_1 block's `ds.y` (the sum of the ORIGINAL activations, upstream CUDA). Round 0214 (owner's yes,
// 11-05): the original sum does not see the activations' rounding, which the experts' SwiGLU outputs (not zero-mean)
// make large: the whole-expert error with Q5_0 down was 2.1e-2 .. 3.1e-2 against 1.2e-2 for every other down type, and the
// GPU now agrees with the CPU path (UD-IQ4_XS, UD-Q3_K_XL). Result: d5 * d8 * (sumi - 16 * sumu).
__device__ __forceinline__ float vec_dot_q5_0_q8_1(const void* __restrict__ vbq, const block_q8_1* __restrict__ bq8_1,
const int& kbx, const int& iqs) {
const block_q5_0* bq5_0 = (const block_q5_0*) vbq + kbx;
int sumi = 0;
int sumi = 0, sumu = 0;
#pragma unroll
for (int i = 0; i < VDR_Q5_0; ++i) {
const int vl = get_int_b2(bq5_0->qs, iqs + i);
@@ -459,11 +462,11 @@ __device__ __forceinline__ float vec_dot_q5_0_q8_1(const void* __restrict__ vbq,
vi1 |= (vh << 2) & 0x00100000;
vi1 |= (vh << 9) & 0x10000000;
sumi = ggml_cuda_dp4a(vi1, u1, sumi);
sumu = ggml_cuda_dp4a(0x01010101, u1, ggml_cuda_dp4a(0x01010101, u0, sumu));
}
const float d5 = __half2float(bq5_0->d);
const float d8 = __low2float(bq8_1->ds);
const float s8 = __high2float(bq8_1->ds);
return d5 * (sumi * d8 - (16.0f * VDR_Q5_0 / QI5_0) * s8);
return d5 * d8 * (float) (sumi - 16 * sumu);
}
// Q4_0 (plain llama-quantize Q4_0 files, e.g. bartowski's: gate/up and most down projections): the codes are centred
// (q - 8 as signed bytes) before the dot, the exact integer form ggml-cpu's q4_0 x q8_0 dot uses, rather than
+3 -1
View File
@@ -505,7 +505,9 @@ int check_blob(const cpu::NativeFmt& f, const std::vector<uint8_t>& blob, int se
cudaFree(dstart); cudaFree(dn); cudaFree(ddst); cudaFree(dtok);
}
const double ec = rel(got_c, ref), eg = rel(got_g, ref), ecg = rel(got_c, got_g);
const bool ok = ec < 3e-2 && eg < 3e-2 && std::isfinite(ec) && std::isfinite(eg);
// Q5_0 down used to sit at 2.1e-2 .. 3.1e-2 (the offset from the original activation sum); with the quantized sum it is as accurate as the rest
const double gpu_limit = f.d_type == 6 ? 1.8e-2 : 3e-2;
const bool ok = ec < 3e-2 && eg < gpu_limit && std::isfinite(ec) && std::isfinite(eg);
std::printf("%-9s %-8s/%-7s blob %8zu cpu rel %.2e gpu rel %.2e cpu-gpu %.2e %s\n", label.c_str(),
ggml_type_name((ggml_type) f.gu_type), ggml_type_name((ggml_type) f.d_type), f.bytes, ec, eg, ecg,
ok ? "ok" : "FAIL");
+9 -6
View File
@@ -638,14 +638,17 @@ __dpct_inline__ float vec_dot_q5_K_q8_1(const void *__restrict__ vbq,
// of the ORIGINAL activations. That is ggml-cpu's convention (its q8_1 `s` is d * sum(q)) and the one the K-quant
// mins above use; the scaled and the min term then see the same activation (eddoursul/Strata measured 1.1-1.2%
// against 1.9% relative error per expert). Result: sumi * (d5 * d8) + sumu * (m5 * d8).
// Q5_0: llama.cpp's integer chain verbatim (vecdotq.cuh vec_dot_q5_0_q8_1_impl) -- symmetric (one delta, no
// learned min), so unlike Q5_1 there is no min-term rounding choice to make: the constant -16 per-weight offset
// is accounted for via the q8_1 block's `ds.y` (sum of the ORIGINAL activations), exactly as upstream.
// Q5_0: llama.cpp's integer chain (vecdotq.cuh vec_dot_q5_0_q8_1_impl), but the constant -16 per-weight offset multiplies
// the sum of the QUANTIZED activations (dp4a with 0x01010101, times d8), like the Q5_1 and Q4_0 paths and ggml-cpu's q8_0
// dot, instead of the q8_1 block's `ds.y` (the sum of the ORIGINAL activations, upstream CUDA). Round 0214 (owner's yes,
// 11-05): the original sum does not see the activations' rounding, which the experts' SwiGLU outputs (not zero-mean)
// make large: the whole-expert error with Q5_0 down was 2.1e-2 .. 3.1e-2 against 1.2e-2 for every other down type, and the
// GPU now agrees with the CPU path (UD-IQ4_XS, UD-Q3_K_XL). Result: d5 * d8 * (sumi - 16 * sumu).
__dpct_inline__ float vec_dot_q5_0_q8_1(const void *__restrict__ vbq,
const block_q8_1 *__restrict__ bq8_1,
const int &kbx, const int &iqs) {
const block_q5_0* bq5_0 = (const block_q5_0*) vbq + kbx;
int sumi = 0;
int sumi = 0, sumu = 0;
#pragma unroll
for (int i = 0; i < VDR_Q5_0; ++i) {
const int vl = get_int_b2(bq5_0->qs, iqs + i);
@@ -663,12 +666,12 @@ __dpct_inline__ float vec_dot_q5_0_q8_1(const void *__restrict__ vbq,
vi1 |= (vh << 2) & 0x00100000;
vi1 |= (vh << 9) & 0x10000000;
sumi = ggml_cuda_dp4a(vi1, u1, sumi);
sumu = ggml_cuda_dp4a(0x01010101, u1, ggml_cuda_dp4a(0x01010101, u0, sumu));
}
const float d5 = sycl::vec<sycl::half, 1>(bq5_0->d)
.convert<float, sycl::rounding_mode::automatic>()[0];
const float d8 = bq8_1->ds[0];
const float s8 = bq8_1->ds[1];
return d5 * (sumi * d8 - (16.0f * VDR_Q5_0 / QI5_0) * s8);
return d5 * d8 * (float) (sumi - 16 * sumu);
}
// Q4_0 (plain llama-quantize Q4_0 files, e.g. bartowski's: gate/up and most down projections): the codes are centred
// (q - 8 as signed bytes) before the dot, the exact integer form ggml-cpu's q4_0 x q8_0 dot uses, rather than
+2 -1
View File
@@ -347,7 +347,8 @@ int main(int argc, char** argv) {
sycl::free(dstart, *s); sycl::free(dn, *s); sycl::free(ddst, *s); sycl::free(dtok, *s);
}
const double ec = rel(got_c, ref), eg = rel(got_g, ref), ecg = rel(got_c, got_g);
const bool ok = ec < 3e-2 && eg < 3e-2 && std::isfinite(ec) && std::isfinite(eg);
const double gpu_limit = f.d_type == 6 ? 1.8e-2 : 3e-2; // Q5_0 down: the quantized-sum offset (round 0214) makes it as accurate as the rest
const bool ok = ec < 3e-2 && eg < gpu_limit && std::isfinite(ec) && std::isfinite(eg);
std::printf("%s %2d %-8s/%-7s blob %8zu cpu rel %.2e gpu rel %.2e cpu-gpu %.2e %s\n", synthetic ? "pair " : "layer", l,
ggml_type_name((ggml_type) f.gu_type), ggml_type_name((ggml_type) f.d_type), f.bytes, ec, eg, ecg,
ok ? "ok" : "FAIL");