Skip to content

npu: the Llama-3.2-1B pack imports whole and then drifts immediately — its first EOS is served as text #1044

Closed

Description

@bong-water-water-bong

npu: the Llama-3.2-1B pack imports whole and then drifts immediately — its first EOS is served as text

Now that #844 lands, the Llama family is the second pack that converts whole (147 tensors, 150,848 q4_1 tiles,
1280/1280 attention tiles, 2560/2560 MLP groups, 32/32 norm anchors, model.norm.weight bit-identical to the
pack's own tail bytes). Its forward is not right yet, and this issue is the measurement rather than a guess.

Instrument: ~/ogaload/oga13/gate_llama.sh — the upstream meta-llama/Llama-3.2-1B-Instruct checkpoint in bf16 as
the reference, our container fed the reference's own prompt ids, 16 greedy steps, NPU_INFER_HOST_GEMM=1 (no
device, no ticket).

prompt ours mean KL median first argmax split
capital of France The capital of France is Paris.<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\nI'm sorry, 2.49 0.00012 step 12
first three primes The first three prime numbers are:\n\n1. 2\n2. 3 7.30 3.72 step 6
why the sky is blue The sky appears blue because it is primarily composed of a mixture of gases, including 8.29 10.10 step 5

Two different defects are visible in that table, and they should not be conflated:

  1. A stop-token defect that is ours, not the pack's. The capital prompt is token-for-token right through the
    answer (The capital of France is Paris.) and the next model token is <|eot_id|> — after which the reference
    stops and we keep generating, emitting the control token as text and inventing a second turn. That is our
    serving path not treating this family's EOS as a stop (Llama-3.2 uses 128009 <|eot_id|> and 128001
    <|end_of_text|>), and it is easy to confuse with a model defect because the text looks broken.
  2. A genuine drift from a correct prefix on the other two prompts (5 and 6 steps, KL ~7-8 at the split), which
    is a weights/layout question like engine#1003 was for the Qwen3 packs — not a near-tie: the KL at the split is an
    order of magnitude above the 0.03-0.4 the Qwen packs show at their coin flips.

What is established, so the next step starts from evidence: the container reads the pack (147 tensors, 16 layers,
H=2048 NL=16 NH=32 NKV=8 HD=64 IM=8192 NV=128256, qk_norm=0, rope_theta=500000 from the pack's own config),
the import re-encodes every tile and group to the pack's bytes, the final norm is bit-identical to the pack's tail,
and the four array designs are named by the forward (QKV K=2048 N=3072, O K=2048 N=2048, GU K=2048 N=16384,
D K=8192 N=2048; the b8 ELFs for them exist in ~/ogaload/oga13/kernels-llama-b8).

What is not established, and is the experiment that decides it: whether a Llama block's projections in the
container are the checkpoint's tensors under the layout the importer assumes (the check §7.25 ran for Qwen3 layer 0-13
— dequantise a container tensor, correlate against the checkpoint after the per-input-channel fold). Llama's section
geometry differs from Qwen2.5 in at least the ways already recorded in FACTS §7.24 (the k/q/v run order is k, q, v)
and its eps is 1e-5 rather than 1e-6, so "it worked for Qwen" is not an argument. Candidates to exclude in order:
attention payload split, MLP gate/up order, and the o_proj K geometry (o_proj_k is recorded as 2048 for this pack,
i.e. K == hidden, which the 4B Qwen3 pack does not satisfy).

Activity

  1. bong-water-water-bong commented on Nov 5, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Answering this issue's "not established" question, and correcting one of its readings. All device-free
    (NPU_INFER_HOST_GEMM=1, no NPU ticket, nothing on :18097).

    1. The container does hold the checkpoint's tensors

    The experiment the issue asks for, run on Llama (not carried over from Qwen): dequantise a container tensor with
    the q4_1 tile rule, correlate against meta-llama/Llama-3.2-1B-Instruct bf16. corrF fits out the one
    per-input-channel scale the packer folds into a projection. corr.T ≈ 0 everywhere rules out a transposed read.

    tensor corr corr.T corrF tensor corr corr.T corrF
    L0 q_proj +0.8780 −0.001 +0.9869 L8 q_proj +0.9914 +0.001 +0.9941
    L0 k_proj +0.8816 −0.001 +0.9866 L8 gate_proj +0.9888 +0.000 +0.9941
    L0 v_proj +0.9561 −0.001 +0.9864 L8 down_proj +0.9930 −0.000 +0.9943
    L0 o_proj +0.9819 +0.001 +0.9934 L15 q_proj +0.9929 −0.000 +0.9938
    L0 gate_proj +0.9943 +0.000 +0.9949 L15 o_proj +0.9410 +0.001 +0.9937
    L0 up_proj +0.9902 +0.000 +0.9905 L15 down_proj +0.9608 −0.000 +0.9936
    L0 down_proj +0.9859 −0.001 +0.9942 L15 gate_proj +0.9935 +0.000 +0.9948

    lm_head dequant vs the tied checkpoint embedding: corr +0.9942. model.norm.weight: 2048/2048 bit-exact,
    while the per-layer norms match 0-22 of 2048 channels — exactly the channels whose AWQ smoothing factor is 1, which
    is the fold, seen from the other side.

    Two tensors read low on corrF (L15 v_proj +0.9479, L15 up_proj +0.9693) and they are not defects:
    per-output-row correlation is +0.979 min / +0.993 median for v_proj (0 of 512 rows below 0.8) and +0.976 / +0.994 for
    up_proj (0 of 8192). The packer applies a second, per-output-row fold to the tensor that precedes an AWQ scaling
    op — the container's own algo_config says o_proj.prev_op = self_attn.v_proj and down_proj.prev_op = mlp.up_proj
    — and a per-column-only fit cannot recover that. So: the attention/MLP split, the gate/up order and o_proj's K
    geometry named in the issue are all excluded.

    2. The "drift" is one coin flip per prompt, not drift

    The greedy table above can't say that, because after the first split the two sides generate from different token
    histories
    — every later row compares different contexts, and that is where the KL 7-8/worst-case rows come from
    (the per-position KL at the split is 1.115 / 0.978 / 1.385, not 7-8).

    Re-measured on identical context: serve once, then for each position k send prompt = ref_prompt_ids + ref_out_ids[:k] with max_tokens 1 and keep the dumped logits. Every row now shares the reference's prefix, and
    rows below the split reproduce the greedy numbers to the digit (instrument control).

    prompt identical-context rows the one split its size
    capital of France 15 of 16, KL mean 0.093 / median 0.00012 step 12 ref 60704 21.50 vs ours 40 19.79 (ref's own #2 is 40 19.625)
    first three primes 15 of 16, KL ≤0.0006 outside it step 6 identical top-5 set {25,220,512,1473,33080}, ours takes 1473 26.21 vs ref 220 24.13
    why the sky is blue 15 of 16, KL ≤0.06 outside it step 5 ref 315 19.38 vs ours 433 17.37 (we rank 315 second at 16.12)

    Agreement resumes right after each split (p2 rows 7-15: KL ≤0.0006, top-1 on all nine). So the forward is
    per-position correct and flips once per prompt, on a different position each time, among the same handful of
    candidates — the Qwen3 near-tie signature, not a layout error.

    3. A separate defect this family exposes: rope_type: "llama3" is not implemented

    The pack declares rope_scaling {llama3, factor 32, high_freq_factor 4, low_freq_factor 1, original_max_position 8192} with rope_theta 500000; the importer carries it into the container verbatim, and the
    forward builds RoPE as theta^(-i/half) and nothing else (origin/main npu/forward/src/q4nx_forward.cpp handles
    LongRoPE short_factor and freq_factors, no llama3 branch; q4nx_semantics.h has no scaling fields).

    Measured against transformers as the control, my port of the formula reproduces its inv_freq to 4.2e-8:

    • 17 of 32 dim pairs are scaled, up to 32x — served 32x too fast;
    • max angle error 0.126 rad at pos 128, 1.008 at 1024, 2.017 at 2048 (10 dims > 0.1 rad there);
    • same prompt, scaled vs plain RoPE on this checkpoint: KL 0.0058 at ctx 104, 0.0107 at 754, 0.0252 at 1497.

    Not the cause of §2's flips (0.01 rad at position 5-12 cannot move a logit by 1.9-3.2), but it grows with context and
    will dominate at this pack's real 4096-32768 range. Filed separately rather than folded in here.

    4. Defect 1 (EOS served as text) — root cause found, fix in flight

    app/forward_serve.cpp read eos_token_id with json_int(), which answers its default for anything that is not an
    integer, so this pack's list [128001, 128008, 128009] gave eos = -1 and the serve's only stop id was whatever
    <|endoftext|> resolved to by name. Reproducer (device-free): finish_reason: length, text
    The capital of France is Paris.<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\nI'm sorry,.
    npu/model.cpp and npu/pack_import.cpp already parse the array form — only the serve path was scalar. PR to follow.

  2. bong-water-water-bong commented on Nov 5, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Defect 1 fixed in #1052 (f50f600). Root cause was one line: app/forward_serve.cpp read eos_token_id with json_int(), which answers its default for a list, so this pack's [128001, 128008, 128009] gave no stop ids and <|eot_id|> 128009 was served as text. Same reproducer, rebuilt:

    before  finish_reason: length   "The capital of France is Paris.<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\nI'm sorry,"
    after   finish_reason: stop     "The capital of France is Paris."
    

    Defect 2 stays open and is measured in the comment above (15 of 16 positions per prompt agree on identical context, one flip each); it is not an eos problem. The rope_type: llama3 gap found while measuring it is #1048.

  3. bong-water-water-bong commented on Nov 5, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Defect 2 is attributed, and it is not our forward

    §7.30 above answered how many positions disagree (15 of 16 per prompt on identical context, one flip each). This
    answers why, with an experiment that separates the only two candidates — the pack's 4-bit weights, or our forward —
    which the checkpoint comparison cannot do, because both are present in it.

    Method (device-free, ~10 min of CPU, no AMD software): load the upstream checkpoint in fp32 in transformers,
    swap in the container's own dequantised tensors (146: 112 q4_1 projections, 34 bf16 norms/embedding, plus the
    pack's separately quantised lm_head; the vectorised q4_1 dequant was checked bitwise against the reference loop
    first), then step it greedily on the same prompt ids. That is a reference implementation of the pack's model: it
    shares our data and none of our code.

    prompt upstream checkpoint (bf16) reference impl on the container's weights our engine
    capital of France splits step 12 → 60704 splits step 12 → 40 splits step 12 → 40
    first three primes splits step 6 → 220 splits step 6 → 1473 splits step 6 → 1473
    why the sky is blue splits step 5 → 315 splits step 5 → 433 splits step 5 → 433

    Every flip reproduced, at the same position, with the same token. And our engine against that reference
    implementation: 48 rows, zero argmax splits, corr 0.999-1.000, KL mean 0.00085 / 0.00005 / 0.00035 (max 0.00938 /
    0.00075 / 0.00203) — the residual is fp32 reduction order.

    So: the flips are entirely the pack's 4-bit weight quantisation. The container reads the pack correctly (#1052,
    §7.28), the forward executes the pack's model correctly, and what remains is the distance between a 4-bit pack and the
    bf16 checkpoint it was made from — which any correct engine would show.

    That also means the framing in this issue's body needs one correction: a single comparison against the upstream
    checkpoint measures two different things at once, and they should be reported separately — engine fidelity (our
    logits vs a reference implementation on the pack's weights: KL ~1e-4, no ranking changes) and the pack's own
    distance from its upstream checkpoint
    (KL mean 0.093 / 0.052 / 0.036 plus one flip per prompt). The second number is
    not a defect in anything; it is the price of 4-bit.

    Recorded as npu-re FACTS §7.31 (5037b09, branch ogaload/oga-pack-layout-20261031), evidence
    evidence/oga/llama-family-2026-11-04/attr_flips.py.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions