Commit 5fdfa6282 for llama.cpp

commit 5fdfa6282936576d2f352d4b97f397a109f207a6
Author: Daniel Han <danielhanchen@gmail.com>
Date:   Sun Sep 6 09:46:21 2026 -0700

    models : fix GDN normalization from `max` to `rsqrt` (#28068)

    * 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>

diff --git a/src/models/bailingmoe3.cpp b/src/models/bailingmoe3.cpp
index 1f2592cfa..e208c7d5a 100644
--- a/src/models/bailingmoe3.cpp
+++ b/src/models/bailingmoe3.cpp
@@ -280,8 +280,8 @@ llama_model_bailingmoe3::graph::graph(const llama_model & model, const llm_graph
             ggml_tensor * beta = ggml_mul_mat(ctx0, layer.ssm_beta, cur);
             beta = ggml_sigmoid(ctx0, ggml_reshape_4d(ctx0, beta, 1, n_head, n_seq_tokens, n_seqs));

-            q = ggml_l2_norm(ctx0, q, hparams.f_norm_rms_eps);
-            k = ggml_l2_norm(ctx0, k, hparams.f_norm_rms_eps);
+            q = build_gdn_l2_norm(ctx0, q, hparams.f_norm_rms_eps);
+            k = build_gdn_l2_norm(ctx0, k, hparams.f_norm_rms_eps);

             ggml_tensor * states_all = mctx_cur->get_s_l(il);
             ggml_tensor * state = build_rs(inp_rs, states_all, hparams.n_embd_s(), n_seqs);
diff --git a/src/models/kimi-k3.cpp b/src/models/kimi-k3.cpp
index b061093eb..b7604cbf2 100644
--- a/src/models/kimi-k3.cpp
+++ b/src/models/kimi-k3.cpp
@@ -441,9 +441,9 @@ ggml_tensor * llama_model_kimi_k3::graph::build_kda_layer(
     ggml_tensor * state = build_rs(inp_rs, ssm_states_all, hparams.n_embd_s(), n_seqs);
     state = ggml_reshape_4d(ctx0, state, head_dim, head_dim, n_head_kda, n_seqs);

-    const float eps = hparams.f_norm_rms_eps;
-    Qcur = ggml_l2_norm(ctx0, Qcur, eps);
-    Kcur = ggml_l2_norm(ctx0, Kcur, eps);
+    const float eps_norm = hparams.f_norm_rms_eps;
+    Qcur = build_gdn_l2_norm(ctx0, Qcur, eps_norm);
+    Kcur = build_gdn_l2_norm(ctx0, Kcur, eps_norm);

     auto attn_out = build_delta_net(Qcur, Kcur, Vcur, g1, beta, state, il);

diff --git a/src/models/kimi-linear.cpp b/src/models/kimi-linear.cpp
index 601d1d9be..f391f5f50 100644
--- a/src/models/kimi-linear.cpp
+++ b/src/models/kimi-linear.cpp
@@ -331,10 +331,11 @@ llama_model_kimi_linear::graph::graph(const llama_model & model, const llm_graph
             ggml_tensor * state = build_rs(inp_rs, ssm_states_all, hparams.n_embd_s(), n_seqs);
             state = ggml_reshape_4d(ctx0, state, head_dim, head_dim, n_head, n_seqs);

+
             const float eps_norm = hparams.f_norm_rms_eps;

-            Qcur = ggml_l2_norm(ctx0, Qcur, eps_norm);
-            Kcur = ggml_l2_norm(ctx0, Kcur, eps_norm);
+            Qcur = build_gdn_l2_norm(ctx0, Qcur, eps_norm);
+            Kcur = build_gdn_l2_norm(ctx0, Kcur, eps_norm);

             // Choose between build_delta_net_chunking and build_delta_net_recurrent based on n_tokens
             auto attn_out = build_delta_net(Qcur, Kcur, Vcur, g1, beta, state, il);
diff --git a/src/models/models.h b/src/models/models.h
index 50e9a235c..87195fddd 100644
--- a/src/models/models.h
+++ b/src/models/models.h
@@ -10,6 +10,13 @@

 class llama_memory_hybrid_idx_context;

+// ref: https://github.com/ggml-org/llama.cpp/pull/28068
+static inline ggml_tensor * build_gdn_l2_norm(ggml_context * ctx, ggml_tensor * x, float eps) {
+    const float n = x->ne[0];
+
+    return ggml_scale(ctx, ggml_rms_norm(ctx, x, eps/n), 1.0f/sqrtf(n));
+}
+
 //
 // base classes
 //
diff --git a/src/models/qwen35.cpp b/src/models/qwen35.cpp
index 0b9210981..478f9ebea 100644
--- a/src/models/qwen35.cpp
+++ b/src/models/qwen35.cpp
@@ -423,10 +423,11 @@ ggml_tensor * llama_model_qwen35::graph::build_layer_attn_linear(
     cb(k_conv, "k_conv", il);
     cb(v_conv, "v_conv", il);

+
     const float eps_norm = hparams.f_norm_rms_eps;

-    q_conv = ggml_l2_norm(ctx0, q_conv, eps_norm);
-    k_conv = ggml_l2_norm(ctx0, k_conv, eps_norm);
+    q_conv = build_gdn_l2_norm(ctx0, q_conv, eps_norm);
+    k_conv = build_gdn_l2_norm(ctx0, k_conv, eps_norm);

     //q_conv = ggml_cont_4d(ctx0, q_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
     //k_conv = ggml_cont_4d(ctx0, k_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
diff --git a/src/models/qwen35moe.cpp b/src/models/qwen35moe.cpp
index ed4083f12..488c7d357 100644
--- a/src/models/qwen35moe.cpp
+++ b/src/models/qwen35moe.cpp
@@ -447,10 +447,11 @@ ggml_tensor * llama_model_qwen35moe::graph::build_layer_attn_linear(
     cb(k_conv, "k_conv", il);
     cb(v_conv, "v_conv", il);

+
     const float eps_norm = hparams.f_norm_rms_eps;

-    q_conv = ggml_l2_norm(ctx0, q_conv, eps_norm);
-    k_conv = ggml_l2_norm(ctx0, k_conv, eps_norm);
+    q_conv = build_gdn_l2_norm(ctx0, q_conv, eps_norm);
+    k_conv = build_gdn_l2_norm(ctx0, k_conv, eps_norm);

     //q_conv = ggml_cont_4d(ctx0, q_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
     //k_conv = ggml_cont_4d(ctx0, k_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
diff --git a/src/models/qwen3next.cpp b/src/models/qwen3next.cpp
index eb823b8ea..222c0acf0 100644
--- a/src/models/qwen3next.cpp
+++ b/src/models/qwen3next.cpp
@@ -503,10 +503,11 @@ ggml_tensor * llama_model_qwen3next::graph::build_layer_attn_linear(
     cb(k_conv, "k_conv", il);
     cb(v_conv, "v_conv", il);

+
     const float eps_norm = hparams.f_norm_rms_eps;

-    q_conv = ggml_l2_norm(ctx0, q_conv, eps_norm);
-    k_conv = ggml_l2_norm(ctx0, k_conv, eps_norm);
+    q_conv = build_gdn_l2_norm(ctx0, q_conv, eps_norm);
+    k_conv = build_gdn_l2_norm(ctx0, k_conv, eps_norm);

     //q_conv = ggml_cont_4d(ctx0, q_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
     //k_conv = ggml_cont_4d(ctx0, k_conv, head_k_dim, num_k_heads, n_seq_tokens, n_seqs);
diff --git a/src/models/qwen4exp.cpp b/src/models/qwen4exp.cpp
index 1484c9b07..8ace95f73 100644
--- a/src/models/qwen4exp.cpp
+++ b/src/models/qwen4exp.cpp
@@ -936,10 +936,11 @@ ggml_tensor * llama_model_qwen4exp::graph::build_layer_attn_linear(
     cb(k_conv, "k_conv", il);
     cb(v_conv, "v_conv", il);

+
     const float eps_norm = hparams.f_norm_rms_eps;

-    q_conv = ggml_l2_norm(ctx0, q_conv, eps_norm);
-    k_conv = ggml_l2_norm(ctx0, k_conv, eps_norm);
+    q_conv = build_gdn_l2_norm(ctx0, q_conv, eps_norm);
+    k_conv = build_gdn_l2_norm(ctx0, k_conv, eps_norm);

     // repeat to match shapes when head keys != value keys; unneeded with the fused GDN
     if (num_k_heads != num_v_heads && (!cparams.fused_gdn_ar || !cparams.fused_gdn_ch)) {