Repository navigation
npu: the 2-bit ternary stream (t2s) -- half the weight bytes, expanded on the core - #821
Conversation
ed9c730 to
fe539f1
Compare
PR Reviewer Guide 🔍(Review updated until commit 9538d31)Here are some key observations to aid the review process:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at fe539f1 (tprs-ready applied)
Re-derived the headline claims on the real images, not only the synthetic fixture.
python3 tests/q4nx_t2s_test.py→ 12/12 PASS, 0 failures (expand matches the pb16i code area byte for byte,
both refusals, the side table, the 0.472222x ratio, 2.1250 b/w).- the real tensor in your body,
blk_0_ffn_gate_weight.pb16i(K=5120, N=17408), read here:
tiles 10880 (both sides) stream 23,674,880 B image 50,135,040 B ratio 0.47222 (2.1250 b/w)
expand(st) vs the image's int4 code area, block for block: 0 of 44,564,480 bytes differ
decode(st) codes vs _split(image) codes: identical
multipliers at 128-K vs the image's 32-K repeats: identical
My first comparison said "40 million differ" because I compared the expanded output against img[:K*N/2] — wrong
on my side: the image is [tiles][4096 code + 512 ms], so the code bytes are img.reshape(-1,4608)[:, :4096].
With the correct slice it is 0 differ, which is the number your body claims.
- the 431-image figure is arithmetic, not a new measurement: 431
.pb16ifiles total 14,435,942,400 B = 14.44 GB,
and since the per-tensor ratio is exactly 2176/4608, the stream total is 6.82 GB — the same 0.47222x. tools/copyright.py --checkrc 0, andq4nx_t2sis registered as a ctest next toq4nx_t2
(CMakeLists.txt:458), so CI runs it on every build rather than only by hand.
Not verified by me, stated in the body: the arena int32 replay (0 of 278,528, 3/3 seeds) — private harness, and the
NPU side is frozen; and any device GB/s, which the body already says is blocked. Nothing in the diff changes an
existing path: one new tool, one new test, one add_test, one docs section.
…d on the core
A ternary model is 2.125 bits per weight; the pb16i image streams 4.5 (int4 nibbles plus one int16
multiplier per 32-K group). tools/q4nx_t2s.py derives the stream form the XDNA decode kernel reads:
2,176 B per k=256 x n=32 tile, 2.125 b/w, 27B decode 14.44 GB -> 6.82 GB (0.47222x)
[0, 2048) 2-bit codes, n-major blocks (nb, kb) at (nb*16 + kb)*64; inside a block, byte r*4 + j
holds the four codes for k = kb*16 + r, n = nb*16 + 4j + {0,1,2,3}
[2048, 2176) int16 multiplier per 128-K block per column ([nb 2][gb 2][16 c])
Two properties make it exact rather than a requantisation:
* pb16i's multiplier is already per-128-K -- q4nx_pb16i.pack derives one value per 128-K block and then
repeats it four times to reach the image's 32-K granularity -- so the compaction drops only duplicated
bytes, and from_pb16i_image() CHECKS that they are duplicates (named refusal, no rounding).
* the n-major block order makes the kernel's expansion to the mmul's int4 B operand a byte-local 1->2
map (f(v) = v - 1 mod 16 = {15,0,1}, four masks/shifts per output vector, no lane shuffling, no table
in DM), and makes one nb (1,024 B) one contiguous DMA run. The 2,176-B tile is streamed as a single
contiguous run, so the shorter stream does not shorten the runs (>= 1 KiB runs is what the measured
43-50 GB/s needs).
Gate, byte-exact: expand() must reproduce the pb16i image's int4 code bytes block for block, decode()
must return the image's codes and multipliers, and the two entry points must agree byte for byte.
tests/q4nx_t2s_test.py checks all of it (12 checks) plus both refusals, synthetically so CI needs only
numpy. Run by hand on the real Bonsai-2 images: all 10,880 tiles of blk_0_ffn_gate_weight.pb16i, 0 of
44,564,480 expanded code bytes differing, codes and multipliers identical, 50,135,040 B -> 23,674,880 B;
the 431-image set reports 14.44 GB -> 6.82 GB, the same figure the t2 chunk kind reports for its own
container format (two independent decodings of the same information).
The device side (the ELF family that consumes this stream) stays in npu-kernels; its measurement is
blocked on the NPU freeze and will be added here when the array is released.
fe539f1 to
8f5c176
Compare
|
Persistent review updated to latest commit 8f5c176 |
|
Persistent review updated to latest commit 9538d31 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 9538d3155 (head after #822 merged; tprs-ready already on)
9538d31 merges main 3702f78 (#822). Content unchanged, re-checked at it rather than carried over:
- own diff is still the four files (
tools/q4nx_t2s.py+177,tests/q4nx_t2s_test.py+125,CMakeLists.txt+4,
docs/npu.md+24);tools/q4nx_t2s.pyandtests/q4nx_t2s_test.pyare byte-identical to the head I reviewed
(fe539f1), and the merge kept both this PR'sadd_test(NAME q4nx_t2s …)and #820's
mtp_draft_test … npu/mtp/mtp_head.cppinCMakeLists.txt; python3 tests/q4nx_t2s_test.py→ 12/12 checks, 0 failures;- real-image claim re-derived again at this head,
blk_0_ffn_gate_weight.pb16i(K=5120, N=17408): 10,880 tiles,
stream 23,674,880 B against the image's 50,135,040 B (ratio 0.47222), andexpand()→ 0 of 44,564,480
expanded code bytes differ from the pb16i image's int4 code area, block for block; - CI on this head:
build,CodeQL, fourAnalyze,pr_agentsuccess (331 s),hw_gateskipped;
mergeable_state=clean.
Unchanged caveats from my first verdict, neither a gate: the arena int32 replay and any device GB/s are the author's
(and the NPU side is frozen), and the PR changes no existing path — new tool, new test, one add_test, one docs
section.
launch path, kept out of #821 on purpose so that PR stayed a tool + a test. A container declares it with `"q4nx_weight_layout": "t2s"` and `"q4nx_weight_layout_version": 5` (pb16i is 4, and each layout refuses the other's version). The images are pb16i's own sidecar set -- `blk_<layer>_<tag>_weight.t2s` plus the **unchanged** `.ref` (e_ref int16[N] + mode byte, since the compaction changes neither the magnitudes nor e_ref) -- in `q4nx_t2s_dir` / `Q4NX_T2S_DIR`, and the decode kernels carry `_t2s_m16`. Everything downstream of the load is pb16i's path (same int32/int64 accumulation, same per-column 2^e_ref, same single host rounding, same loud refusals); only the file's size and extension differ, and `q4nx_t2s.h` is that difference: 2,176 B per k=256 x n=32 tile (pb16i: 4,608) 2.125 bits/weight 27B 12.93 -> 6.11 GB/token `I8Ctx` grows a `t2s` flag next to `pb16i` and derives the image's size, extension and geometry test from it, so a 4,608-B pb16i image can never be read as a 2,176-B stream (and vice versa): a size mismatch is a named refusal, not a wrong number. plan.json accepts `kernels.tag: "_t2s"` and requires `container.layout: "t2s"` + `container.pb16i: true` to agree with it; tools/npu_plan.py writes both. Device-free gates (all green on this box): - `tests/npu_t2s_layout_test.cpp` (new, `ctest npu_t2s_layout`): the tile is 2,048 + 128 B, i.e. 2.125 bits/weight; for every 27B role the stream is exactly 2,176/4,608 of the pb16i image; and the two measured byte counts of the real gate/up tensor are pinned -- 50,135,040 B of pb16i -> 23,674,880 B of stream, the figures tools/q4nx_t2s.py produced on `blk_0_ffn_gate_weight.pb16i`. - `tests/npu_plan_test.cpp`: a t2s plan loads as the ternary family with its own tag and no sealed ELF; `_t2s` on a row-major container, or on one that does not claim `pb16i`, is refused by name. - `onebit_npu_forward` builds; `ctest -R "q4nx_t2s|npu_plan|npu_pb16i_layout|npu_t2s_layout"` 5/5. No device number is claimed: the t2s kernel's first launch (arena `tasks/GU16T2S`, built, never launched) is blocked on the orchestrator's NPU freeze and a per-run OK for the new ELF family, per the lane's state file. When it passes, the arena ELF is installed as `full_i8_GU_K5120_N17408_t2s_m16.elf` and the in-loop s/token follows in a second PR body.
…2.125 b/w) (#826) * npu: the t2s launch path -- a container can stream the 2-bit stream launch path, kept out of #821 on purpose so that PR stayed a tool + a test. A container declares it with `"q4nx_weight_layout": "t2s"` and `"q4nx_weight_layout_version": 5` (pb16i is 4, and each layout refuses the other's version). The images are pb16i's own sidecar set -- `blk_<layer>_<tag>_weight.t2s` plus the **unchanged** `.ref` (e_ref int16[N] + mode byte, since the compaction changes neither the magnitudes nor e_ref) -- in `q4nx_t2s_dir` / `Q4NX_T2S_DIR`, and the decode kernels carry `_t2s_m16`. Everything downstream of the load is pb16i's path (same int32/int64 accumulation, same per-column 2^e_ref, same single host rounding, same loud refusals); only the file's size and extension differ, and `q4nx_t2s.h` is that difference: 2,176 B per k=256 x n=32 tile (pb16i: 4,608) 2.125 bits/weight 27B 12.93 -> 6.11 GB/token `I8Ctx` grows a `t2s` flag next to `pb16i` and derives the image's size, extension and geometry test from it, so a 4,608-B pb16i image can never be read as a 2,176-B stream (and vice versa): a size mismatch is a named refusal, not a wrong number. plan.json accepts `kernels.tag: "_t2s"` and requires `container.layout: "t2s"` + `container.pb16i: true` to agree with it; tools/npu_plan.py writes both. Device-free gates (all green on this box): - `tests/npu_t2s_layout_test.cpp` (new, `ctest npu_t2s_layout`): the tile is 2,048 + 128 B, i.e. 2.125 bits/weight; for every 27B role the stream is exactly 2,176/4,608 of the pb16i image; and the two measured byte counts of the real gate/up tensor are pinned -- 50,135,040 B of pb16i -> 23,674,880 B of stream, the figures tools/q4nx_t2s.py produced on `blk_0_ffn_gate_weight.pb16i`. - `tests/npu_plan_test.cpp`: a t2s plan loads as the ternary family with its own tag and no sealed ELF; `_t2s` on a row-major container, or on one that does not claim `pb16i`, is refused by name. - `onebit_npu_forward` builds; `ctest -R "q4nx_t2s|npu_plan|npu_pb16i_layout|npu_t2s_layout"` 5/5. No device number is claimed: the t2s kernel's first launch (arena `tasks/GU16T2S`, built, never launched) is blocked on the orchestrator's NPU freeze and a per-run OK for the new ELF family, per the lane's state file. When it passes, the arena ELF is installed as `full_i8_GU_K5120_N17408_t2s_m16.elf` and the in-loop s/token follows in a second PR body. * npu: check the sidecar directory once, where it is read (CodeQL path-injection on #826) Every weight image is opened under the layout's sidecar directory, which comes from the container's config or from Q4NX_PB16I_DIR / Q4NX_T2S_DIR, and file_exists() is the fopen that CodeQL reports. One check at the point the path is read instead of trusting it at each open: absolute, no ".." component, no NUL, bounded length -- which is what a converter writes anyway (a relative directory would silently resolve against the serve's cwd). Both layouts go through it; the deployed pb16i container's /home/bcloud/models/npu/bonsai2-pb16i-ternary passes. --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
P3 of the NPU plan (#802), host side only — the device measurement is blocked by the orchestrator's NPU freeze
(two firmware TDRs at 09:48-09:49 in the release gate's NPU row); this lane took no ticket and no launch today.
What this is
A ternary model is 2.125 bits per weight. The pb16i image streams 4.5 (int4 nibbles plus one int16 multiplier
per 32-K group).
tools/q4nx_t2s.pyderives the stream form the XDNA decode kernel reads, so the codes can travel at2 bits and be expanded to the mmul's int4 nibbles inside the core, between the DMA and the
mmul:Why it is exact, not a requantisation
q4nx_pb16i.packderives onemsper 128-K block and then repeats it fourtimes to reach the image's 32-K granularity, so putting it back at 128-K granularity drops only duplicated bytes. The
derivation checks that they are duplicates and refuses with a named error otherwise (
NotRepresentable) — norounding, ever. (The t2 chunk kind's own gate measured that property at 100 % of blocks for Bonsai-2.)
e = r*16 + cof a 16x16 block in byte
e/2; P2's t2 order (byter*64 + c/4) would make the expansion a cross-lane transpose.With four consecutive-n codes in one wire byte, the expansion is a byte-local 1->2 map — four masks/shifts per output
vector, no shuffling, no table in DM — and one
nb(1,024 B) becomes a single contiguous DMA run.measured 43-50 GB/s needs >= 1 KiB runs, so 2-bit must not buy bytes by shortening runs.
Gate (byte-exact, device-free)
ctest q4nx_t2s(new, 12 checks, numpy only — synthetic weights through the engine's own pb16i packer): size and b/w,expand()== the image's int4 code bytes block for block,decode()== the image's codes and multipliers,stream()==from_pb16i_image()byte for byte, the side table identical, and both refusals (a non-shared 128-Kmultiplier; a reserved 2-bit pattern).
Run by hand on the real Bonsai-2 images (too big for CI):
blk_0_ffn_gate_weight.pb16i, all 10,880 tiles,0 of 44,564,480 expanded code bytes differ, codes and multipliers identical, 50,135,040 B -> 23,674,880 B; the whole
431-image set 14.44 GB -> 6.82 GB, the same figure the t2 chunk kind reports for its container format.
Also run in the NPU kernel arena against the frozen int32 harness (K=5120, N=17408, seeds 1/2/3): replaying the kernel's
integer arithmetic (
int8 x int4 -> int32per 32-K group,t = P*msint16 x int16 -> acc32, int32 / acc64) from the t2sstream reproduces
expectedwith 0 of 278,528 int32 differing, 3/3 seeds.What is not here
The ELF family that consumes the stream (private
npu-kernels; built and DM-checked, unlaunched) and its measured GB/s /s-token — those arrive as an arena entry and a second PR once the freeze lifts, with numbers in the body. Nothing in this
PR changes an existing path: new tool, new test, one
add_test, one docs section.