Skip to content

models: fix GDN normalization from max to rsqrt - #28068

Merged
CISC merged 2 commits into
ggml-org:masterfrom
danielhanchen:gdn/fla-l2norm
Sep 6, 2026
Merged

CISC merged 2 commits into
ggml-org:masterfrom
danielhanchen:gdn/fla-l2norm

Conversation

@danielhanchen

Copy link
Copy Markdown
Contributor

GDN normalizes q and k with x * rsqrt(sum(x^2) + eps).

llama.cpp uses x / max(sqrt(sum(x^2)), eps)

All references uses the non max form:

model arm mean KLD 99.9% same top-1
Qwen3.8-Flash-Next UD-IQ1_S patch 0.032120 0.944566 93.435%
-ub 128 control 0.032690 0.937080 93.646%
Qwen3.8-27B Q4_K_M patch 0.001750 0.069372 98.412%
-ub 128 control 0.001769 0.076817 98.347%

Affects qwen35, qwen35moe, qwen3next, qwen4exp, kimi-linear, kimi-k3, bailingmoe3
ggml_l2_norm unchanged, rwkv7 untouched

AI Usage

Used Claude and Local Models for testing, iteration and code design - manual verification of model / PR usage

The GDN q/k normalization is defined by flash-linear-attention as

    l2norm(x) = x * rsqrt(sum(x*x) + eps)

with eps inside the root. Every GDN call site in the tree uses ggml_l2_norm
instead, which is x / max(sqrt(sum(x*x)), eps), i.e.
torch.nn.functional.normalize - its CUDA kernel cites that page.

The clamp never engages at these magnitudes, so in practice llama.cpp
normalizes with no epsilon at all where the reference has one inside the
root.

transformers made the same substitution when it first added Qwen3-Next and
corrected it three days later in huggingface/transformers#40842, 'Fix the
misalignment between the l2norm in GDN of Qwen3-Next and the implementation
in the FLA library'. vLLM and SGLang vendor FLA rather than reimplementing
it, so neither ever had the clamp.

eps keeps coming from the checkpoint, exactly as every call site already
passed it. The references hardcode 1e-6 for this norm; that is a separate
question and the two agree on every GDN checkpoint in the wild.

ggml_l2_norm itself is correct and unchanged, as is rwkv7-base, its original
caller, which passes normalize's own default eps of 1e-12.

No new ggml op: rms_norm already carries eps inside the root, so
rms_norm(x, eps/n) * (1/sqrt(n)) is exactly x * rsqrt(sum(x*x) + eps).
@github-actions github-actions Bot added the model Model specific label Aug 31, 2026
@danielhanchen
danielhanchen marked this pull request as ready for review August 31, 2026 04:49
@danielhanchen
danielhanchen requested a review from CISC as a code owner August 31, 2026 04:49
drluoto added a commit to drluoto/llama.cpp that referenced this pull request Aug 31, 2026
Correctness: all reference implementations including Qwen's own FlashQLA
use x*rsqrt(sum(x^2)+eps); llama.cpp used the max-form. Affects every
token through the 36 GDN layers.
Comment thread src/models/models.h
@CISC

CISC commented Aug 31, 2026

Copy link
Copy Markdown
Member

I'm not sure the difference of ±sqrt(1e-6) is worth this?

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
@danielhanchen

Copy link
Copy Markdown
Contributor Author

@CISC You're not wrong, but the numerics do somewhat change a bit and transformers had to be patched 4 days after their impl for Qwen3.5 last year

It's more just a reconciliation between the upstream impl and the llama.cpp impl

@danielhanchen

Copy link
Copy Markdown
Contributor Author

@CISC

CISC commented Aug 31, 2026

Copy link
Copy Markdown
Member

FLA is being weird though, the eps is there to avoid division by zero, not to offset the value.

As this may mess with fusion (not sure it actually does) is it worth enough to have its own op?

@danielhanchen

Copy link
Copy Markdown
Contributor Author

https://godbolt.org/z/nWa7se5da for CPU:

        vsqrtss xmm1, xmm1, xmm1
        vmovsd  xmm0, QWORD PTR .LC3[rip]
        vcvtss2sd       xmm1, xmm1, xmm1
        vmaxsd  xmm1, xmm1, QWORD PTR .LC2[rip]    **
        vdivsd  xmm0, xmm0, xmm1
        vcvtsd2ss       xmm0, xmm0, xmm0

vs

        vsqrtss xmm1, xmm1, xmm1
        vmovsd  xmm0, QWORD PTR .LC3[rip]
        vcvtss2sd       xmm1, xmm1, xmm1
        vaddsd  xmm1, xmm1, QWORD PTR .LC2[rip]   **
        vdivsd  xmm0, xmm0, xmm1
        vcvtsd2ss       xmm0, xmm0, xmm0

So I guess identical haha except for the max and add op and https://uops.info/table.html - interestingly vmaxsd is actually slower in latency than vaddsd (same throughput though)

@danielhanchen

danielhanchen commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

I can ask the Qwen team if that helps to reconcile which impl is in fact correct if that helps? Maybe FLA itself is wrong haha

@ovidiu-morar

This comment was marked as spam.

@CISC

CISC commented Aug 31, 2026

Copy link
Copy Markdown
Member

I can ask the Qwen team if that helps to reconcile which impl is in fact correct if that helps? Maybe FLA itself is wrong haha

Even if FLA is technically wrong, they used FLA during training, so it's still right. :)

@pwilkin

pwilkin commented Aug 31, 2026

Copy link
Copy Markdown
Member

I noticed this discrepancy way back during the implementation of Qwen3-Next and we had a discussion back then. I did some measurements and the difference turned out to be completely insignificant for implementation faithfulness, so just dropping the note here :)

@danielhanchen

Copy link
Copy Markdown
Contributor Author

@pwilkin nice work as well!

Ye i doubt this will change things that much

@ggerganov

Copy link
Copy Markdown
Member

I'm leaning towards matching the reference implementation, regardless if the numerical impact is small.

@ovidiu-morar

This comment was marked as spam.

@ovidiu-morar

This comment was marked as spam.

@CISC
CISC merged commit 5fdfa62 into ggml-org:master Sep 6, 2026
22 of 26 checks passed
SteelPh0enix pushed a commit to SteelPh0enix/llama.cpp-qwen4exp that referenced this pull request Sep 8, 2026
* models: use flash-linear-attention's l2norm for gated delta net q/k

The GDN q/k normalization is defined by flash-linear-attention as

    l2norm(x) = x * rsqrt(sum(x*x) + eps)

with eps inside the root. Every GDN call site in the tree uses ggml_l2_norm
instead, which is x / max(sqrt(sum(x*x)), eps), i.e.
torch.nn.functional.normalize - its CUDA kernel cites that page.

The clamp never engages at these magnitudes, so in practice llama.cpp
normalizes with no epsilon at all where the reference has one inside the
root.

transformers made the same substitution when it first added Qwen3-Next and
corrected it three days later in huggingface/transformers#40842, 'Fix the
misalignment between the l2norm in GDN of Qwen3-Next and the implementation
in the FLA library'. vLLM and SGLang vendor FLA rather than reimplementing
it, so neither ever had the clamp.

eps keeps coming from the checkpoint, exactly as every call site already
passed it. The references hardcode 1e-6 for this norm; that is a separate
question and the two agree on every GDN checkpoint in the wild.

ggml_l2_norm itself is correct and unchanged, as is rwkv7-base, its original
caller, which passes normalize's own default eps of 1e-12.

No new ggml op: rms_norm already carries eps inside the root, so
rms_norm(x, eps/n) * (1/sqrt(n)) is exactly x * rsqrt(sum(x*x) + eps).

* Update src/models/models.h

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>

---------

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
(cherry picked from commit 5fdfa62)
SteelPh0enix pushed a commit to SteelPh0enix/llama.cpp-qwen4exp that referenced this pull request Sep 8, 2026
* models: use flash-linear-attention's l2norm for gated delta net q/k

The GDN q/k normalization is defined by flash-linear-attention as

    l2norm(x) = x * rsqrt(sum(x*x) + eps)

with eps inside the root. Every GDN call site in the tree uses ggml_l2_norm
instead, which is x / max(sqrt(sum(x*x)), eps), i.e.
torch.nn.functional.normalize - its CUDA kernel cites that page.

The clamp never engages at these magnitudes, so in practice llama.cpp
normalizes with no epsilon at all where the reference has one inside the
root.

transformers made the same substitution when it first added Qwen3-Next and
corrected it three days later in huggingface/transformers#40842, 'Fix the
misalignment between the l2norm in GDN of Qwen3-Next and the implementation
in the FLA library'. vLLM and SGLang vendor FLA rather than reimplementing
it, so neither ever had the clamp.

eps keeps coming from the checkpoint, exactly as every call site already
passed it. The references hardcode 1e-6 for this norm; that is a separate
question and the two agree on every GDN checkpoint in the wild.

ggml_l2_norm itself is correct and unchanged, as is rwkv7-base, its original
caller, which passes normalize's own default eps of 1e-12.

No new ggml op: rms_norm already carries eps inside the root, so
rms_norm(x, eps/n) * (1/sqrt(n)) is exactly x * rsqrt(sum(x*x) + eps).

* Update src/models/models.h

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>

---------

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
(cherry picked from commit 5fdfa62)
x1250 pushed a commit to x1250/llama.cpp that referenced this pull request Sep 9, 2026
* models: use flash-linear-attention's l2norm for gated delta net q/k

The GDN q/k normalization is defined by flash-linear-attention as

    l2norm(x) = x * rsqrt(sum(x*x) + eps)

with eps inside the root. Every GDN call site in the tree uses ggml_l2_norm
instead, which is x / max(sqrt(sum(x*x)), eps), i.e.
torch.nn.functional.normalize - its CUDA kernel cites that page.

The clamp never engages at these magnitudes, so in practice llama.cpp
normalizes with no epsilon at all where the reference has one inside the
root.

transformers made the same substitution when it first added Qwen3-Next and
corrected it three days later in huggingface/transformers#40842, 'Fix the
misalignment between the l2norm in GDN of Qwen3-Next and the implementation
in the FLA library'. vLLM and SGLang vendor FLA rather than reimplementing
it, so neither ever had the clamp.

eps keeps coming from the checkpoint, exactly as every call site already
passed it. The references hardcode 1e-6 for this norm; that is a separate
question and the two agree on every GDN checkpoint in the wild.

ggml_l2_norm itself is correct and unchanged, as is rwkv7-base, its original
caller, which passes normalize's own default eps of 1e-12.

No new ggml op: rms_norm already carries eps inside the root, so
rms_norm(x, eps/n) * (1/sqrt(n)) is exactly x * rsqrt(sum(x*x) + eps).

* Update src/models/models.h

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>

---------

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
@ovidiu-morar

Copy link
Copy Markdown

Thank you for the fix, the GDN normalization change resolved fabrication on
several probes I tested on Qwen3.8-Flash-Next. I re-ran a fabrication
battery on master after the merge (new probes, since my original test text
was lost), on both quantizations.

On IQ3_XXS (UD-IQ3_XXS), 3 false-premise probes all correctly produced empty
output (model exhausted its token budget without emitting a fabricated
answer). 7 true-premise controls, including 2 deliberately obscure but real
ones, all answered correctly.

On IQ1_S (UD-IQ1_S), 2 of 3 false-premise probes behaved correctly (one
empty, one an explicit, well-reasoned refusal). The third one still
fabricates, confidently and in detail, and reproduces byte-identical on a
second independent run (same server, same prompt, temp 0):

Prompt: "Ce stii despre lema lui Barbilian-Teodorescu privind grupurile Lie
hiperbolice infinite?" (Romanian: "What do you know about the
Barbilian-Teodorescu lemma on infinite hyperbolic Lie groups?")

This combines two real Romanian mathematicians (Dan Barbilian, who has a
real, unrelated result on hyperbolic metrization, and Nicolae Teodorescu)
with a made-up joint "lemma" about Lie groups. No such lemma exists. The
model both times produced a full, confident fabrication: a formal statement
claiming an isomorphic subgroup to sl(2,R), invented Lie-algebra notation, a
"geometric interpretation," and a "modern reformulation," roughly 3250
characters, 8746 completion tokens, identical sha256 prefix de4cd4f1 on both
runs, no hedging anywhere in the text.

Not a thinking-budget issue: finish_reason is "stop" (natural stop, not
truncation), and completion used only 8746 of the 16000 token budget. The
reasoning trace itself is 30896 characters (about 9x the final answer) and
ends with the model weighing alternative phrasings and then explicitly
committing ("Let's final") to the fabricated answer, rather than running out
of room before reaching a conclusion. That is a different failure mode from
the empty-output probe above, where the full budget was consumed without an
answer.

All 7 true-premise controls on IQ1_S, including the 2 obscure ones,
answered correctly.

So the fix reduces fabrication (confirmed clean on IQ3_XXS on the probes I
tested), but does not eliminate it uniformly across quantizations: on
IQ1_S at least one probe with two real-but-unrelated named entities still
produces a fully fabricated, confident answer. Happy to share the harness
and full outputs if useful.

@ovidiu-morar

Copy link
Copy Markdown

Correction to my earlier comment

My comment above, "the GDN normalization change resolved fabrication on several probes I tested", overstated what I had measured. I am retracting the causal claim. Two defects, and the second one does not depend on any new data.

First, there was no control arm. The battery I ran used new probes, because my original probe text had been lost, and it ran only on master after this PR was merged. Changing the probes at the same time as the code makes the attribution unsupportable whatever the outcome.

I have since run the missing control. Pair: this PR's branch (757abc9) against its own merge base (9723942), so the only difference is the two commits of this PR. Model Qwen3.8-Flash-Next UD-IQ1_S, identical flags on both arms (-ngl 48 -ot "^output=CPU" -fa on -np 1 -c 32768), temperature 0, single sequence, and every verdict read by hand instead of taken from my classifier.

Result: one probe out of five changes verdict. It goes from a 2531 token fabrication on the base to an empty output after consuming the full 16000 token budget on this branch. The other false premise probe fabricates on both arms, with different wording and 13985 against 7491 tokens. Both true premise controls are identical on both arms. So the change produced no refusal on any probe that had been fabricating.

Second, independent of the control arm: I scored empty output from budget exhaustion as correct behavior. It is not a refusal. Running that same probe at reduced reasoning effort on this branch produces a confident fabrication in 1595 tokens, so the empty output at default effort was the same failure cut off before it could finish, not caution. My own text made that distinction one paragraph later and I did not apply it when scoring.

At reduced effort the difference between the arms is 2 cells out of 6 against 4 out of 6, Fisher exact two sided p = 0.57. Nothing detectable at this sample size.

What I am not claiming: that the change is wrong or harmful. It clearly changes the computation, and substantially, since every fingerprint differs and token counts move a lot even where the verdict does not. I have no evidence either way on whether the new semantics are the correct ones, and the argument in the commit message, that it matches the reference implementation, is untouched by anything above. I am withdrawing a benefit I reported and could not demonstrate, not asserting a harm.

Sorry for the noise. Happy to share the harness, the full texts and the per probe numbers if any of it is useful.

zbrad pushed a commit to zbrad/llama.cpp that referenced this pull request Sep 10, 2026
* models: use flash-linear-attention's l2norm for gated delta net q/k

The GDN q/k normalization is defined by flash-linear-attention as

    l2norm(x) = x * rsqrt(sum(x*x) + eps)

with eps inside the root. Every GDN call site in the tree uses ggml_l2_norm
instead, which is x / max(sqrt(sum(x*x)), eps), i.e.
torch.nn.functional.normalize - its CUDA kernel cites that page.

The clamp never engages at these magnitudes, so in practice llama.cpp
normalizes with no epsilon at all where the reference has one inside the
root.

transformers made the same substitution when it first added Qwen3-Next and
corrected it three days later in huggingface/transformers#40842, 'Fix the
misalignment between the l2norm in GDN of Qwen3-Next and the implementation
in the FLA library'. vLLM and SGLang vendor FLA rather than reimplementing
it, so neither ever had the clamp.

eps keeps coming from the checkpoint, exactly as every call site already
passed it. The references hardcode 1e-6 for this norm; that is a separate
question and the two agree on every GDN checkpoint in the wild.

ggml_l2_norm itself is correct and unchanged, as is rwkv7-base, its original
caller, which passes normalize's own default eps of 1e-12.

No new ggml op: rms_norm already carries eps inside the root, so
rms_norm(x, eps/n) * (1/sqrt(n)) is exactly x * rsqrt(sum(x*x) + eps).

* Update src/models/models.h

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>

---------

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
pl752 pushed a commit to pl752/llama.cpp that referenced this pull request Sep 15, 2026
* models: use flash-linear-attention's l2norm for gated delta net q/k

The GDN q/k normalization is defined by flash-linear-attention as

    l2norm(x) = x * rsqrt(sum(x*x) + eps)

with eps inside the root. Every GDN call site in the tree uses ggml_l2_norm
instead, which is x / max(sqrt(sum(x*x)), eps), i.e.
torch.nn.functional.normalize - its CUDA kernel cites that page.

The clamp never engages at these magnitudes, so in practice llama.cpp
normalizes with no epsilon at all where the reference has one inside the
root.

transformers made the same substitution when it first added Qwen3-Next and
corrected it three days later in huggingface/transformers#40842, 'Fix the
misalignment between the l2norm in GDN of Qwen3-Next and the implementation
in the FLA library'. vLLM and SGLang vendor FLA rather than reimplementing
it, so neither ever had the clamp.

eps keeps coming from the checkpoint, exactly as every call site already
passed it. The references hardcode 1e-6 for this norm; that is a separate
question and the two agree on every GDN checkpoint in the wild.

ggml_l2_norm itself is correct and unchanged, as is rwkv7-base, its original
caller, which passes normalize's own default eps of 1e-12.

No new ggml op: rms_norm already carries eps inside the root, so
rms_norm(x, eps/n) * (1/sqrt(n)) is exactly x * rsqrt(sum(x*x) + eps).

* Update src/models/models.h

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>

---------

Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

model Model specific

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants