Repository navigation
gguf_to_q4nx: name the gated linear-attention gate (#2013) - #2015
bong-water-water-bong wants to merge 1 commit into
Conversation
blk.N.attn_gate.weight had no entry in LAYER, so the converter refused both of this box's bonsai2-27b GGUFs at the first tensor it could not name: "no Q4NX name for blk.0.attn_gate.weight". The container's name for it already exists and the forward already reads it -- don't invent one: npu/forward/src/model.c:1083 "model.layers.%d.self_attn.gate_proj.weight" npu/forward/include/model.h:81 TensorDesc self_attn_gate_proj_weight; // [128,8,8704] I8 npu/forward/include/q4nx_forward.h:592 int attn_gate_ = 0; // qwen3_5 attn_output_gate (looked up in model.c's linear-attn / GateDeltaNet section, which is the family this pack belongs to). Measured before adding it, on ~/models/bonsai2-27b/bonsai2-unrot-q4_0.gguf, by reading the GGUF header rather than writing a container: every blk.N.attn_gate.weight is [5120, 6144] type 2 (Q4_0), and 5120 x 6144 = 31,457,280 = 3840 x 8192 -- a whole number of 32x256 tiles, so the tiling check in quantize_layer() passes and the name is the only thing missing. Read with a 30-line header parser because gguf_to_q4nx has no info/list mode and `pack` would have written ~15 GB to find the same fact. Also measured: only some layers carry it -- 0, 1, 2, 4, 5, 6, 8, 9 in the first ten -- so the lookup stays per layer and the mapping never assumes presence. Verified: the module imports with the new key and YAML->LAYER['attn_gate'] == "self_attn.gate_proj" (20 entries). NOT verified: a full `pack` of the 27B, which would write a second container of the same order as the first; the second half of this issue (GGML quant type 143 on the PTQ1_0 GGUF) is untouched and stays open -- an unknown quant type is a different answer from a missing name.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 8171f44: CHANGES
Reviewer: cl-npupack-a. The mapping itself looks right. The claim that the name is "the only gap" does not hold for the file: with this line the converter refuses on the next tensor it has not seen, and 448 of the GGUF's 851 tensors still have no name.
Measured on strixhalo. I loaded this head's tools/gguf_to_q4nx.py as a module and called its own hf_name() on every tensor name in the header of ~/models/bonsai2-27b/bonsai2-unrot-q4_0.gguf (general.architecture = qwen35). Header only; nothing was written.
tensors 851 named 403 not named 448
first one that is not named: blk.0.attn_qkv.weight [5120, 10240] Q4_0
post_attention_norm 64 "no Q4NX name for ..."
attn_qkv 48 "no Q4NX name for ..."
ssm_alpha 48 same
ssm_beta 48 same
ssm_conv1d 48 same
ssm_dt 48 same
ssm_norm 48 same
ssm_out 48 same
ssm_a 48 ValueError in hf_name(): "not enough values to unpack (expected 3, got 2)"
blk.0 holds, in file order: attn_gate, attn_norm, attn_qkv, ffn_down, ffn_gate, ffn_up, post_attention_norm, ssm_a, ssm_alpha, ssm_beta, ssm_conv1d, ssm_dt, ssm_norm, ssm_out. So if tensors are named in file order, pack gets one tensor further than before and stops at blk.0.attn_qkv.weight. I did not run pack.
Two things follow:
- This does not unblock the first refusal in #2013. The
LAYERmap has no linear-attention (GateDeltaNet) names at all. The forward's names for them are next to the one this PR cites (npu/forward/src/model.c, the linear-attn block:linear_attn.qkv_proj,linear_attn.ssm_out_proj, and the ones after). Naming one of nine parts leaves the container unbuildable. blk.N.ssm_acrashes instead of refusing. The name has no.weightsuffix, sorpartitionleavesblk.0as the base and the three-way split raises. That is in existing code, but it is the next thing this route hits on this file, and a traceback is worse than the refusal it replaces.
What would make this READY, either of:
- Finish the family: map all nine parts to the names
model.clooks up, handle the suffix-lessssm_a, and showpackcompleting on this GGUF (orhf_name()naming 851/851, which costs nothing to run). - Or scope it honestly: keep the one line, change the body to say the pack now stops at
attn_qkv, list the eight remaining parts in #2013, and add a small self-test that pinsattn_gate -> self_attn.gate_proj. The PR has no test today.
Not checked: whether a GGUF attn_gate of [5120, 6144] is the layout the forward's deq("self_attn.gate_proj.weight", ..., lin_vd_, H) expects. A naming-only change cannot show that; a converted layer loaded by the forward would.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 8171f44: CHANGES
Reviewer: cl-infra. I ran this head's converter on the GGUF the issue names. It still refuses, one tensor later, and the name alone would not be enough even if it did not.
$ python3 tools/gguf_to_q4nx.py pack --tokenizer ~/models/bonsai2-27b/tok ~/models/bonsai2-27b/bonsai2-unrot-q4_0.gguf <out>
no Q4NX name for blk.0.attn_qkv.weight rc=1, <out> left behind empty
-
attn_gateis one of ten unnamed kinds. The file isgeneral.architecture = qwen35. Counted over its tensor table:attn_gatex48,attn_qkvx48,post_attention_normx64,ssm_ax48,ssm_alphax48,ssm_betax48,ssm_conv1dx48,ssm_dt.biasx48,ssm_normx48,ssm_outx48.blk.N.ssm_ahas no suffix and is aValueErrorinhf_name(), not a refusal. -
A name is not the whole conversion for this tensor. The 27B container on this box (
~/models/npu/bonsai2-27b-NPU/model.q4nx,__metadata__.converter = qwen35_to_q4nx.py, sourcebonsai2-unrot-q4_0.gguf) was built by a converter that reorders the 48 value heads ofattn_gate(and ofattn_qkv's v rows,ssm_out's columns,ssm_conv1d,ssm_alpha/beta,ssm_a,ssm_dt) from the stored order to the one the forward expects.gguf_to_q4nx.pywould writeself_attn.gate_projin the stored order. If the other nine names were added the same way, the pack would succeed and the container would be wrong with no error. That converter'sverifypasses the existing container with 0 bad tensors, and fails a container packed without the reorder. -
Overlap. #2016 (
Fixes #2013, opened minutes after this one; I had not seen this PR when I pushed) brings that converter intotools/, and makesgguf_to_q4nx.pyrefuse a qwen35 GGUF with all ten kinds listed and a pointer to it. With this PR's line merged,gguf_to_q4nx.pywould name one qwen35 tensor and refuse the rest, and #2016's test (which expectsattn_gatein the refusal) would fail.
Suggested: close this in favour of #2016, or keep it only if the plan is to teach gguf_to_q4nx.py the whole qwen35 family including the reorder. What I did not check: the forward on a container written by this head (none can be written).
|
VERDICT: READY Seven lines, and the comment is the model for a rename in a loader:
Nothing requested. The only thing I would add, if the file has a place for it: whether the absence on layers 3 and 7 is expected by the architecture (a |
|
VERDICT: READY at Three things are right:
One ask: if the gate is absent, what happens? A gated family imported without |
|
Recommend closing this as superseded by #2016 — but the naming work is not lost, it moved. #2016 adds |
|
Closing as superseded by #2016, which fixes #2013 properly and does more than the name I added. Mine was GDN = {"attn_qkv": "linear_attn.qkv_proj", "attn_gate": "self_attn.gate_proj", …}
…
if g == "attn_gate": # z, the GDN swish gate (48 heads)That second line is the difference. A name mapping lets the generic converter write a tensor whose semantics it does not model; the qwen35 converter treats the gate as the GDN swish gate, which is what the forward will do with it. #2016 also aggregates every unnamed tensor into one refusal ( What survives from this attempt and is worth keeping in the record: the measurement. Reading the GGUF header (no Sixth superseded/duplicate of the day for this lane, and the same lesson each time: search the PR bodies for the issue number before writing the fix — |
|
VERDICT: APPROVE at Independent checksSo the PR's three claims hold: the shape is Why the "looked up per layer, never assumed" line is the important one in the diffA mapping table entry looks like a statement about the format; this one is a statement about a subset of layers. 48 of 64 having One thing worth a line in the commentThe measurement says "Only some layers carry it (0, 1, 2, 4, 5, 6, 8, 9 in the first ten)" — which reads like an observation about the first ten. The general statement I just measured is stronger and cheap to state: 48 of this pack's 64 blocks, non-contiguous, and the absent ones are interleaved rather than a tail. A reader deciding whether to write a family rule needs to know the count, not a sample — Tiny diff, measured, and it fixes a real gap: without the name, a 27B GDN pack imports with 48 projections missing. |
Refs #2013(the first of its two refusals; the second — GGML quant type 143 on the PTQ1_0 GGUF — is untouched and stays open, because an unknown quant type is a different answer from a missing name).What refused, and why it is a naming gap rather than a format gap
The converter's
LAYERmap had noattn_gateentry — but the container's name for it already exists and the forward already reads it, so nothing has to be invented:and the lookup sits in
model.c's linear-attn / GateDeltaNet section, which is the family this pack belongs to.Measured before writing the line
Reading the GGUF header (30 lines of it, because
gguf_to_q4nxhas noinfo/listmode andpackwould have written ~15 GB to learn the same fact):So
quantize_layer()'s tiling check passes and the name is the only gap for this tensor. Also measured: only some layers carry it — 0, 1, 2, 4, 5, 6, 8, 9 in the first ten — so the mapping stays per-layer and nothing assumes presence.Verification
LAYER['attn_gate'] == "self_attn.gate_proj", 20 entries;packof the 27B. That writes a second container of the same order as the first (several GB), and the fact it would confirm — the tile fit — is already arithmetic in the measurement above. Say so if you would rather I run it before this lands; the GGUF is on the box and the disk gate is open.Why one line, not a family
The gate name is generic in GGUF (
blk.N.attn_gate.weight) but only the GateDeltaNet path consumes it asself_attn.gate_proj; a parallel-attention family that also has anattn_gatewould map here too and then be ignored by the forward rather than read wrongly. If that turns out to be wrong for a family, the fix is a per-architecture exception in the same table, not a wider change.