Skip to content

npu: a pack that declares rope_type "llama3" is served plain RoPE — 17 of 32 dim pairs run up to 32x fast (Llama-3.x) #1048

Closed

Description

@bong-water-water-bong

npu: a pack that declares rope_type: "llama3" is served plain RoPE — 17 of 32 dim pairs run up to 32x fast

Every Llama-3.x Ryzen AI pack declares RoPE scaling and gets none of it. rope_scaling is parsed by neither the
forward nor the semantics layer, so the model is served with inv_freq = theta^(-i/half) while the checkpoint it was
quantised from used the llama3 low/high-frequency interpolation.

Measured on Llama-3.2-1B-Instruct_rai_1.8.0_npu_4K (imported container, ctr-llama), config
{rope_theta 500000, rope_scaling {rope_type llama3, factor 32, high_freq_factor 4, low_freq_factor 1, original_max_position_embeddings 8192}}. The importer carries that block into the container's config.json
verbatim, so the information reaches us and is then dropped:

  • npu/forward/src/q4nx_forward.cpp — the rope table is 1.0 / pow(theta, i/half); the file reads LongRoPE
    short_factor and freq_factors and has no llama3 branch.
  • npu/forward/include/q4nx_semantics.h — rope_theta, rotary_dim, rope_type_named, per_layer_rope,
    rope_theta_sliding; no scaling parameters of any kind.

Size of the defect

rope_gap.py (in the lane's evidence dir). The formula was ported and checked against transformers' own
inv_freq before it was used: max|diff| 4.2e-8 (vs 9.8e-4 if plain RoPE is used instead), so the numbers below
come from the right formula.

  • 17 of 32 dim pairs are scaled, by up to 32x — i.e. those frequencies advance 32x too fast.
  • max absolute angle error: 0.126 rad at position 128, 1.008 at 1024, 2.017 at 2048 (10 dim pairs over 0.1 rad
    at 2048).
  • logits, same prompt, scaled vs plain RoPE on the same bf16 checkpoint:
    KL 0.0058 at ctx 104, 0.0107 at 754, 0.0252 at 1497 (corr 0.999 / 0.995 / 0.992; argmax unchanged in all three).

So it is small at short context and grows with position; it will dominate at this pack's real range (context_length
32768 in the pack's genai_config.json, max_position_embeddings 131072). It is not the cause of the single
per-prompt coin flips tracked in #1044 — those are 1.9-3.2 logit gaps at positions 5-12, where a 0.01 rad angle
error cannot act — and that is exactly why this is its own issue.

What a fix needs

  1. Read rope_scaling (or rope_parameters) at load, i.e. in npu/pack_import.cpp / npu/model.cpp / the forward's
    config path, into the semantics struct.

  2. Build the table with the three-branch rule (transformations' _compute_llama3_parameters):
    wavelen = 2*pi/inv_freq; original_max/low_freq_factor and original_max/high_freq_factor bound the band;
    outside it divide by factor, inside it interpolate by smooth = (original_max/wavelen - low)/(high - low).

    out = where(wavelen > orig_max/low, inv/factor, inv)
    smooth = (orig_max/wavelen - low)/(high - low)
    out = where((wavelen >= orig_max/high) & (wavelen <= orig_max/low),
                (1 - smooth)*out/factor + smooth*out, out)
  3. A test that fails on the old table: the llama3 inv_freq for (d=64, theta=500000, factor 32, high 4, low 1,
    orig_max 8192) is fixed data — 17 of 32 entries differ from plain, the ratios are integers up to 32, and the
    three entries inside the band must be interpolated, not switched.

  4. Prove it on the container rather than on the table: a long-context prompt (ctx ~1500) through
    1bit forward-serve --dump-logits against this checkpoint's reference logits, before and after. The before value
    is KL 0.0252 at 1497 above.

Related

Activity

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

    @bong-water-water-bong
    CollaboratorAuthor

    Fixed in #1054 (597ab16). The rule is one function over the frequency in npu/forward/include/q4nx_rope.h, applied where inv is built, gated on the config declaring the type so every other model is untouched.

    Evidence, in the order it should be read:

    1. known answer, not a snapshot — expected tables from transformers' own implementation, reproduced by an independent port to 4.2e-8; Llama-3.2-1B is 15 unchanged / 14 divided by exactly 32 / 3 interpolated at i=15..17, Llama-3.1-8B (factor 8) is 29 / 29 / 6 at i=29..34;
    2. fails on the old behaviour — the same test against return inv gives 17 failures, rc 1;
    3. moves our logits the way the reference says — on the container at 1127 tokens our before→after change has rms 0.2975 vs the reference's 0.3022, corr +0.74, and the ground-truth ordering flips (before: closer to plain 0.0955 than scaled 0.1038; after: closer to scaled 0.1325 than plain 0.1565). Honest caveat: the reference difference (KL 0.0167) is below our 4-bit floor at that context, so this is a change-vector argument, not an absolute-KL win — and it grows with context (0.0058@104 → 0.0252@1497);
    4. non-regression, byte-exact — a Qwen2.5-0.5B container gives the identical logit row before and after (62fa6780964858047ef207e5).
  2. bong-water-water-bong commented on Nov 5, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Scope hand-off: the forward was not the only place in this tree that builds plain RoPE

    #1054 fixes npu/forward's in-loop table, which is what the OGA pack path uses (the imported ctr-llama container
    carries no rope tensor — 147 tensors, no cos/sin — so the forward computes it, and that is the path I measured). Two
    other builders exist and neither knows about scaling:

    1. npu/pack.cpp:175 fill_rope() — theta^(-2j/128) for j < 64, no LongRoPE, no llama3, plus a
      fast-path constant kInvFreq1e6 for theta 1e6. It is the table npu/pack.cpp:146 writes into a packed container
      and that npu/lane.cpp:128 re-fills per position on the lane path. (It is also hard-wired to 64 pairs / head_dim
      128, so it could not have served this pack, whose head_dim is 64 — but Llama-3.1-8B is head_dim 128 with the same
      llama3 block.)
    2. tools/gguf_to_q4nx.py:197 — reads rope.freq_base into rope_theta and nothing else, so converting a
      Llama-3.x GGUF yields a container whose config says rope_theta 500000 and carries no scaling: whichever of the
      two builders above then runs produces an unscaled table. llama.cpp's own metadata
      (llama.rope.scaling.type = llama3, factor, yarn_log_multiplier-style companions) carries the information, so
      this is a converter gap, not a format gap.

    Not filed as a separate issue because it is the same rule and the same test data as this one — but it is a separate
    fix site, and #1054 does not touch either. Whoever closes this should decide whether the two are in scope: if they
    are, q4nx_rope_llama3_inv_freq() is header-only (npu/forward/include/q4nx_rope.h) and usable from both, and the
    known-answer table in tests/npu_rope_llama3_test.c is the test.

    One thing that does not need it: the ONNX/ORT route. npu/ort/onebit_npu_gemm.cpp is the GEMM half only; for an
    exported ONNX graph the rope angles arrive as graph constants, so the rule lives in whoever exported it (transformers
    applies it), not in the EP.

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