Skip to content

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

Open
danielhanchen wants to merge 2 commits into
ggml-org:masterfrom
danielhanchen:gdn/fla-l2norm
Open

models: fix GDN normalization from max to rsqrt#28068
danielhanchen wants to merge 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.

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