From 971814e641bd49f63ec9dcdc5d1cae665400caf5 Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Thu, 24 Sep 2026 06:08:31 -0300 Subject: [PATCH 1/3] NPU: read all three Q4NX chunk kinds (q4_1, Q4_K, Q8), and repack Q4_K into q4_1 npu/q4nx.{h,cpp}: the chunk kind of a tensor (its shape's last dimension), tile and tensor decoders for q4_1 (5120 B), Q4_K (4736 B) and Q8 (8704 B), and a Q4_K -> q4_1 repack that is exact apart from rounding S*scale and M*min to bf16, so the lane and dx (which read q4_1) run Q4_K weights unchanged. Tensor::tiles() understands the 3-d [tile rows, tile cols, chunk] shape Qwen3.5-4B uses. The layouts are 1bit-MONSTER's (npu-infer/src/q4nx_dequant.c, verified there against the checkpoint's bf16 twin and a CPU forward). tests/npu_q4nx_test.cpp checks ours bit for bit against its Python decoder (npu-infer/tools/q4nx_dequant.py at 35fafde8f) on one real tile of each kind (tests/golden/q4nx: Qwen3.5-4B qkv_proj and lm_head, Qwen3-0.6B q_proj; the expected SHA-256 of each 32 x 256 f32 output), and the repack against its bound: max error 0.00032, 0.92 of the bf16 rounding bound. docs/npu.md, "Q4NX chunks". Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 6 +- docs/npu.md | 20 +++++- npu/model.cpp | 2 + npu/q4nx.cpp | 115 +++++++++++++++++++++++++++++++ npu/q4nx.h | 61 ++++++++++++++++ tests/golden/q4nx/q4_1.bin | Bin 0 -> 5120 bytes tests/golden/q4nx/q4k.bin | Bin 0 -> 4736 bytes tests/golden/q4nx/q8.bin | Bin 0 -> 8704 bytes tests/npu_q4nx_test.cpp | 138 +++++++++++++++++++++++++++++++++++++ 9 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 npu/q4nx.cpp create mode 100644 npu/q4nx.h create mode 100644 tests/golden/q4nx/q4_1.bin create mode 100644 tests/golden/q4nx/q4k.bin create mode 100644 tests/golden/q4nx/q8.bin create mode 100644 tests/npu_q4nx_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 36494422..b9d7f8a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,7 +161,7 @@ option(ONEBIT_NPU_HOST "Build the NPU host library and its tests" ${_onebit_npu_ if(ONEBIT_NPU_HOST) find_package(PkgConfig REQUIRED) pkg_check_modules(PCRE2 REQUIRED IMPORTED_TARGET libpcre2-8) -add_library(onebit_npu STATIC npu/full_elf.cpp npu/model.cpp npu/pack.cpp npu/tokenizer.cpp) +add_library(onebit_npu STATIC npu/full_elf.cpp npu/model.cpp npu/pack.cpp npu/q4nx.cpp npu/tokenizer.cpp) target_include_directories(onebit_npu PUBLIC npu) target_link_libraries(onebit_npu PUBLIC nlohmann_json::nlohmann_json PRIVATE PkgConfig::PCRE2) @@ -177,6 +177,10 @@ if(ONEBIT_NPU_CAPTURED AND ONEBIT_NPU_PDI AND ONEBIT_NPU_GOLDEN) COMMAND npu_full_elf_test ${ONEBIT_NPU_CAPTURED} ${ONEBIT_NPU_PDI} ${ONEBIT_NPU_GOLDEN}) endif() +# Q4NX chunk decoders against 1bit-MONSTER's verified decoders, on committed real tiles +add_executable(npu_q4nx_test tests/npu_q4nx_test.cpp) +target_link_libraries(npu_q4nx_test PRIVATE onebit_npu) +add_test(NAME npu_q4nx COMMAND npu_q4nx_test ${CMAKE_SOURCE_DIR}/tests/golden/q4nx) add_executable(npu_pack_test tests/npu_pack_test.cpp) target_link_libraries(npu_pack_test PRIVATE onebit_npu) set(ONEBIT_NPU_MODEL "" CACHE PATH "Q4NX model directory (model.q4nx + config.json)") diff --git a/docs/npu.md b/docs/npu.md index 19bd735a..28edd439 100644 --- a/docs/npu.md +++ b/docs/npu.md @@ -34,7 +34,7 @@ Step 3 lands in three parts: ``` / - model.q4nx weights (safetensors layout, I8 tiles of 5120 B) + model.q4nx weights (safetensors layout, Q4NX tiles: "Q4NX chunks" below) config.json Hugging Face config: dimensions, rope_theta, eos_token_id tokenizer.json Hugging Face tokenizer npu/layer_ctx1.elf layer kernel, instruction ELFs for context lengths 1, 2, 17 @@ -44,6 +44,24 @@ Step 3 lands in three parts: npu/layer.pdi the design both kernels run on ``` +### Q4NX chunks + +A Q4NX weight is a grid of 32-row x 256-column tiles, one chunk each, row-major over +the grid. The chunk kind is the last dimension of the tensor's shape: `[tiles, 5120]`, +or `[tile rows, tile cols, chunk bytes]` (`npu/q4nx.h` has the byte layouts). + +| Chunk | Kind | Weight | Seen in | +|---|---|---|---| +| 5120 B | q4_1: bf16 scale and zero per 32 columns of a row | `code * scale + zero` | Qwen3, Llama, Gemma, Phi models | +| 4736 B | Q4_K: u8 scale and min per 32 columns, bf16 `S`, `M` per row | `S * scale * code + M * min` | Qwen3.5-4B projections | +| 8704 B | Q8: bf16 scale per 32 columns, int8 codes | `d * code` | Qwen3.5-4B `lm_head` and embedding | + +`npu/q4nx.h` decodes all three, and repacks Q4_K into q4_1 (exact apart from rounding +`S * scale` and `M * min` to bf16), so the lane and dx, which read q4_1, run Q4_K +weights unchanged. Q8 does not fit q4_1. `tests/npu_q4nx_test.cpp` checks the decoders +bit for bit against 1bit-MONSTER's verified decoders on real Qwen3.5-4B and Qwen3-0.6B +tiles (committed in `tests/golden/q4nx`), and the repack against its rounding bound. + `1bit serve -m ` serves such a directory on the NPU behind the OpenAI-compatible API ([serve.md](serve.md)); inside Lemonade, that is what its onebit backend runs (`--onebit npu`). diff --git a/npu/model.cpp b/npu/model.cpp index e0950499..20150427 100644 --- a/npu/model.cpp +++ b/npu/model.cpp @@ -28,6 +28,8 @@ namespace onebit::npu { int Tensor::tiles() const { + // [tile rows, tile cols, chunk bytes] (Q4NX with Q4_K or Q8 chunks, npu/q4nx.h) + if (shape.size() == 3 && shape[0] > 0 && shape[1] > 0) return int(shape[0] * shape[1]); if (shape.size() != 2 || shape[0] <= 0) return 0; // A row is one tile only when it is 5120 bytes wide; narrower rows pack // several to a tile. diff --git a/npu/q4nx.cpp b/npu/q4nx.cpp new file mode 100644 index 00000000..af636eec --- /dev/null +++ b/npu/q4nx.cpp @@ -0,0 +1,115 @@ +// Copyright 2026 bong-water-water-bong +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include "q4nx.h" + +#include +#include +#include + +namespace onebit::npu::q4nx { + +namespace { + +float bf16(const uint8_t* p) { + const uint32_t u = uint32_t(p[0] | (p[1] << 8)) << 16; + float f; + std::memcpy(&f, &u, 4); + return f; +} + +uint16_t to_bf16(float f) { // round to nearest even + uint32_t u; + std::memcpy(&u, &f, 4); + return uint16_t((u + 0x7FFF + ((u >> 16) & 1)) >> 16); +} + +void put16(uint8_t* p, uint16_t v) { + p[0] = uint8_t(v); + p[1] = uint8_t(v >> 8); +} + +// the q4_1 code byte and nibble of row r, column c +size_t q41_code_at(int r, int c) { return 1024 + size_t((r / 16) * 8 + c / kGroup) * 256 + 8 * (c % kGroup) + (r % 16) / 2; } + +} // namespace + +Chunk chunk_of(const Tensor& t) { + const int64_t last = t.shape.empty() ? 0 : t.shape.back(); + if ((t.shape.size() == 2 || t.shape.size() == 3) && + (last == int64_t(Chunk::Q4_1) || last == int64_t(Chunk::Q4K) || last == int64_t(Chunk::Q8))) + return Chunk(last); + throw std::runtime_error("not a Q4NX chunked tensor (last dimension " + std::to_string(last) + ")"); +} + +void dequant_tile(Chunk kind, const uint8_t* t, float* out, int ld) { + for (int r = 0; r < kRows; r++) { + for (int c = 0; c < kCols; c++) { + const int g = c / kGroup, i = g * kRows + r; + float w; + if (kind == Chunk::Q4_1) { + const uint8_t b = t[q41_code_at(r, c)]; + const int code = (r % 2 == 0) ? (b & 15) : (b >> 4); + w = float(code) * bf16(t + 2 * i) + bf16(t + 512 + 2 * i); + } else if (kind == Chunk::Q4K) { + const uint8_t b = t[512 + c * 16 + r / 2]; + const int code = (r % 2 == 0) ? (b & 15) : (b >> 4); + w = bf16(t + 4608 + 2 * r) * float(t[i]) * float(code) + bf16(t + 4672 + 2 * r) * float(t[256 + i]); + } else { + w = bf16(t + 2 * i) * float(int8_t(t[512 + c * kRows + r])); + } + out[size_t(r) * ld + c] = w; + } + } +} + +void dequant(const Tensor& t, int rows, int cols, float* out) { + const Chunk kind = chunk_of(t); + if (rows % kRows || cols % kCols) throw std::runtime_error("Q4NX tensor size is not whole tiles"); + const int tr = rows / kRows, tc = cols / kCols; + if (t.bytes < uint64_t(tr) * tc * uint64_t(kind)) throw std::runtime_error("Q4NX tensor is shorter than its tiles"); + for (int i = 0; i < tr; i++) + for (int j = 0; j < tc; j++) + dequant_tile(kind, t.data + (size_t(i) * tc + j) * size_t(kind), out + size_t(i) * kRows * cols + size_t(j) * kCols, + cols); +} + +void q4k_to_q4_1(const uint8_t* k, uint8_t* q) { + std::memset(q, 0, size_t(Chunk::Q4_1)); + for (int r = 0; r < kRows; r++) { + const float S = bf16(k + 4608 + 2 * r), M = bf16(k + 4672 + 2 * r); + for (int g = 0; g < kCols / kGroup; g++) { + const int i = g * kRows + r; + put16(q + 2 * i, to_bf16(S * float(k[i]))); + put16(q + 512 + 2 * i, to_bf16(M * float(k[256 + i]))); + } + for (int c = 0; c < kCols; c++) { + const uint8_t b = k[512 + c * 16 + r / 2]; + const uint8_t code = (r % 2 == 0) ? (b & 15) : (b >> 4); + q[q41_code_at(r, c)] |= (r % 2 == 0) ? code : uint8_t(code << 4); + } + } +} + +std::vector as_q4_1(const Tensor& t) { + const Chunk kind = chunk_of(t); + const size_t n = size_t(t.bytes / uint64_t(kind)); + if (kind == Chunk::Q4_1) return std::vector(t.data, t.data + n * size_t(kind)); + if (kind == Chunk::Q8) throw std::runtime_error("a Q8 tensor does not fit q4_1 (8-bit codes)"); + std::vector out(n * size_t(Chunk::Q4_1)); + for (size_t i = 0; i < n; i++) q4k_to_q4_1(t.data + i * size_t(kind), out.data() + i * size_t(Chunk::Q4_1)); + return out; +} + +} // namespace onebit::npu::q4nx diff --git a/npu/q4nx.h b/npu/q4nx.h new file mode 100644 index 00000000..a53cac64 --- /dev/null +++ b/npu/q4nx.h @@ -0,0 +1,61 @@ +// Copyright 2026 bong-water-water-bong +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// The three Q4NX chunk kinds, decoded. A Q4NX weight is a grid of 32-row x 256-column +// tiles, each one chunk; a tensor's chunk kind is the last dimension of its shape +// ([tiles, 5120] or [tile rows, tile cols, chunk bytes]), and tiles are stored row-major +// over the grid. Index g*32 + r means column group g (32 columns) of row r. +// +// q4_1 5120 B bf16 scale[g*32+r] @0, bf16 zero[g*32+r] @512, then 4-bit codes in 16 +// regions of 256 B: region h*8+g (h = r/16), byte 8*j + (r%16)/2, even +// row in the low nibble. w = code * scale + zero +// Q4_K 4736 B u8 scale[g*32+r] @0, u8 min[g*32+r] @256, 4-bit codes @512 at byte +// k*16 + r/2 (even row low), bf16 S[r] @4608, bf16 M[r] @4672. +// w = S[r]*scale*code + M[r]*min +// Q8 8704 B bf16 d[g*32+r] @0, int8 codes @512 at byte k*32 + r. w = d * code +// +// Checked bit for bit against 1bit-MONSTER's verified decoders (npu-infer/tools/ +// q4nx_dequant.py) on real Qwen3.5-4B and Qwen3-0.6B tiles: tests/npu_q4nx_test.cpp. +#pragma once + +#include "model.h" + +#include +#include + +namespace onebit::npu::q4nx { + +constexpr int kRows = 32, kCols = 256, kGroup = 32; + +enum class Chunk : uint32_t { Q4_1 = 5120, Q4K = 4736, Q8 = 8704 }; + +// The chunk kind of a quantized tensor; throws if its shape names none of the three. +Chunk chunk_of(const Tensor& t); + +// One tile to f32: out[r * ld + c] for r < 32, c < 256. +void dequant_tile(Chunk kind, const uint8_t* tile, float* out, int ld); + +// A whole rows x cols tensor to f32 (rows % 32 == 0, cols % 256 == 0). +void dequant(const Tensor& t, int rows, int cols, float* out); + +// One Q4_K tile as a q4_1 tile, so the lane and dx run it unchanged. Exact apart from +// rounding the products S[r]*scale and M[r]*min to bf16 (nearest even). +void q4k_to_q4_1(const uint8_t* q4k, uint8_t* q41); + +// The tensor's tiles as q4_1: a copy for q4_1, converted for Q4_K. Throws for Q8, whose +// 8-bit codes do not fit q4_1. +std::vector as_q4_1(const Tensor& t); + +} // namespace onebit::npu::q4nx diff --git a/tests/golden/q4nx/q4_1.bin b/tests/golden/q4nx/q4_1.bin new file mode 100644 index 0000000000000000000000000000000000000000..5ec58318cb818d7767af90485b5b3b7593f1c4a4 GIT binary patch literal 5120 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 g#0uZxxvl6nEZUT7L9GS^3ezN|D*}f4E!JOaK4? literal 0 HcmV?d00001 diff --git a/tests/golden/q4nx/q4k.bin b/tests/golden/q4nx/q4k.bin new file mode 100644 index 0000000000000000000000000000000000000000..99756b0979f60a3040ecd40186b886553916eaf0 GIT binary patch literal 4736 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 literal 0 HcmV?d00001 diff --git a/tests/golden/q4nx/q8.bin b/tests/golden/q4nx/q8.bin new file mode 100644 index 0000000000000000000000000000000000000000..b6f2178f02d36aa3cd6f936332c8e380796e60c4 GIT binary patch literal 8704 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 jZX$+k#H%Jii#lpBs2yu$m+FsNdo^9x#d_k literal 0 HcmV?d00001 diff --git a/tests/npu_q4nx_test.cpp b/tests/npu_q4nx_test.cpp new file mode 100644 index 00000000..924c9437 --- /dev/null +++ b/tests/npu_q4nx_test.cpp @@ -0,0 +1,138 @@ +// Copyright 2026 bong-water-water-bong +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// npu_q4nx_test +// +// The Q4NX chunk decoders (npu/q4nx.h) against 1bit-MONSTER's verified decoders +// (npu-infer/tools/q4nx_dequant.py at 35fafde8f, itself checked against the checkpoint's +// bf16 twin): one real tile of each kind, from Qwen3.5-4B (layer 0 qkv_proj tile 0, Q4_K; +// lm_head tile 0, Q8) and Qwen3-0.6B (layer 0 q_proj tile 0, q4_1). The expected output is +// the SHA-256 of the reference's 32 x 256 f32, so any bit that differs fails. +// Then Q4_K -> q4_1: the converted tile must decode within the bf16 rounding of its two +// scale products (half an ulp of S*scale times the largest code, plus half an ulp of M*min). +#include "../npu/q4nx.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using onebit::npu::q4nx::Chunk; + +std::vector slurp(const std::string& p) { + std::ifstream f(p, std::ios::binary); + return {std::istreambuf_iterator(f), {}}; +} + +// SHA-256 (FIPS 180-4), enough for one 32 KiB buffer +std::string sha256(const uint8_t* data, size_t len) { + static const uint32_t K[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, + 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, + 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2}; + uint32_t h[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; + std::vector m(data, data + len); + m.push_back(0x80); + while (m.size() % 64 != 56) m.push_back(0); + for (int i = 7; i >= 0; i--) m.push_back(uint8_t((uint64_t(len) * 8) >> (8 * i))); + auto rotr = [](uint32_t x, int n) { return (x >> n) | (x << (32 - n)); }; + for (size_t off = 0; off < m.size(); off += 64) { + uint32_t w[64]; + for (int i = 0; i < 16; i++) + w[i] = uint32_t(m[off + 4 * i]) << 24 | uint32_t(m[off + 4 * i + 1]) << 16 | uint32_t(m[off + 4 * i + 2]) << 8 | m[off + 4 * i + 3]; + for (int i = 16; i < 64; i++) { + const uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3); + const uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7]; + for (int i = 0; i < 64; i++) { + const uint32_t t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]; + const uint32_t t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c)); + hh = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2; + } + h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; h[5] += f; h[6] += g; h[7] += hh; + } + char hex[65]; + for (int i = 0; i < 8; i++) std::snprintf(hex + 8 * i, 9, "%08x", h[i]); + return hex; +} + +float bf16(const uint8_t* p) { + const uint32_t u = uint32_t(p[0] | (p[1] << 8)) << 16; + float f; + std::memcpy(&f, &u, 4); + return f; +} + +} // namespace + +int main(int argc, char** argv) { + if (argc < 2) { std::fprintf(stderr, "usage: %s \n", argv[0]); return 2; } + const std::string dir = argv[1]; + struct Case { const char* file; Chunk kind; const char* want; }; + const Case cases[] = { + {"q4k.bin", Chunk::Q4K, "d2fc854e3b61bc0c77004724fefd441af57408519e8113bde87b63fd2ee5c4f5"}, + {"q8.bin", Chunk::Q8, "09ad9e7a61cad612ffcb9cc2a1d7dfd27b6813a6d184329404045d4691c5fdf2"}, + {"q4_1.bin", Chunk::Q4_1, "9f58d4201c4844abc937204c2b7f8d364ff44bc574b0bb87292928c54447d2c8"}, + }; + bool ok = true; + std::vector w(32 * 256), v(32 * 256); + std::vector q4k; + for (const Case& c : cases) { + const auto tile = slurp(dir + "/" + c.file); + if (tile.size() != size_t(c.kind)) { std::printf("FAIL: %s is %zu bytes\n", c.file, tile.size()); return 1; } + onebit::npu::q4nx::dequant_tile(c.kind, tile.data(), w.data(), 256); + const std::string got = sha256(reinterpret_cast(w.data()), w.size() * 4); + const bool same = got == c.want; + ok &= same; + std::printf("%s %-8s w[0][0] %.7g, w[31][255] %.7g\n", same ? "ok " : "FAIL", c.file, w[0], w[32 * 256 - 1]); + if (!same) std::printf(" sha256 %s, want %s\n", got.c_str(), c.want); + if (c.kind == Chunk::Q4K) q4k = tile; + } + + // Q4_K -> q4_1 + std::vector q41(size_t(Chunk::Q4_1)); + onebit::npu::q4nx::q4k_to_q4_1(q4k.data(), q41.data()); + onebit::npu::q4nx::dequant_tile(Chunk::Q4K, q4k.data(), w.data(), 256); + onebit::npu::q4nx::dequant_tile(Chunk::Q4_1, q41.data(), v.data(), 256); + double worst = 0, worst_err = 0; + for (int r = 0; r < 32; r++) { + const float S = bf16(q4k.data() + 4608 + 2 * r), M = bf16(q4k.data() + 4672 + 2 * r); + for (int c = 0; c < 256; c++) { + const int i = (c / 32) * 32 + r; + // half an ulp of each bf16 product: 2^(exponent - 8) + auto half_ulp = [](double x) { return x == 0 ? 0.0 : std::ldexp(1.0, std::ilogb(std::fabs(x)) - 8); }; + const double bound = 15 * half_ulp(double(S) * q4k[i]) + half_ulp(double(M) * q4k[256 + i]) + 1e-12; + const double err = std::fabs(double(v[r * 256 + c]) - w[r * 256 + c]); + worst = std::fmax(worst, err / bound); + worst_err = std::fmax(worst_err, err); + } + } + const bool conv = worst <= 1.0 + 1e-6; + ok &= conv; + std::printf("%s Q4_K -> q4_1: max |error| %.3g, at most %.2f of the bf16 rounding bound\n", conv ? "ok " : "FAIL", worst_err, worst); + std::printf("%s\n", ok ? "PASS" : "FAIL"); + return ok ? 0 : 1; +} From 7d4577c0edaddd32af133fa4c654f06fc4cb9e74 Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Thu, 24 Sep 2026 06:09:41 -0300 Subject: [PATCH 2/3] copyright: exempt binary golden data (tests/golden/**/*.bin), which cannot carry a comment Co-Authored-By: Claude Opus 5.5 --- tools/copyright.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/copyright.py b/tools/copyright.py index 20ef39ea..379df349 100644 --- a/tools/copyright.py +++ b/tools/copyright.py @@ -36,7 +36,7 @@ ] BLOCK_STYLES = [(("*.md", "*.html", "*.svg"), ("")), (("*.css", "*.js"), ("/*", "*/"))] -EXEMPT = ["third_party/*", "patches/*", "LICENSE", "NOTICE", "*.gguf", "*.json", "*.tsv", "tests/golden/*/tokens.txt", "*/Cargo.lock", "*.png", "*.ico"] +EXEMPT = ["third_party/*", "patches/*", "LICENSE", "NOTICE", "*.gguf", "tests/golden/*.bin", "tests/golden/*/*.bin", "*.json", "*.tsv", "tests/golden/*/tokens.txt", "*/Cargo.lock", "*.png", "*.ico"] def _match(name, patterns): From be7383598615c6b569c54a028f9d203a64f495f5 Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Thu, 24 Sep 2026 06:11:38 -0300 Subject: [PATCH 3/3] ci: build npu_q4nx_test Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8546c451..cc062e68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,6 @@ jobs: - name: Configure run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release - name: Build - run: cmake --build build --target onebit npu_full_elf_test npu_pack_test tokenizer_test -j + run: cmake --build build --target onebit npu_full_elf_test npu_pack_test npu_q4nx_test tokenizer_test -j - name: Test run: ctest --test-dir build --output-on-failure