Skip to content

Commit ca5c7ad

Browse files
committed
perf(laguna): byte-exact attention-glue fusions on NVFP4 decode (VT_LAGUNA_GLUE_FUSED)
Fold the tiny under-occupied glue kernels the decode CUDA graph does NOT hide into fewer, larger nodes on the Laguna-NVFP4 resident decode — BYTE-EXACT, default-ON, VT_LAGUNA_GLUE_FUSED=0 opts back out for a same-binary A/B. - L1: fold the per-head softplus out-gate into the attention-combine store (DecodeAttnCombineKernel + the SPLIT<=1 DecodeAttnGqa{,G}Kernel fallbacks). The combine stored acc*inv (f32, lossless) and a separate SoftplusHeadGate pass reloaded it x softplus(g); folding computes the same (acc*inv)*softplus(g) in registers. gate==nullptr keeps the un-gated store bit-for-bit. -1 kernel/layer. - L4: route each residual-Add + STANDARD RMSNorm pair through the shared vt::FusedChain(kFusedAddRmsNormStd) seam (res += x; out = rms_norm(res)*w). Byte-exact to vt::Add + rms_norm_seq (commutative f32 add + ResRound<f32> identity + the SAME 256-thread strided-sum/tree/1-over-sqrt reduction as RmsNormSeqKernel); the default Tier-0 composite already collapses it to ONE RmsNorm(residual) launch. The post-attn norm folds in place; the last MLP residual add folds with the next layer's input norm (final layer's with the final norm), so both loose per-layer Adds disappear into fused nodes. ~-2/layer. - C (shared-into-MoeCombine) SKIPPED: Laguna's MoeCombine writes bf16 then casts to f32, so folding the f32 shared term before the bf16 store rounds (routed+shared) to bf16 -> not byte-exact (unlike qwen3_5's f32 combine). - L2 (fused qk-norm+RoPE preamble) DEFERRED: needs a capture-safe device-position kernel variant (graph decode indexes cos/sin by *pos_buf, half-split layout). DGX gate (GB10, laguna-xs-nvfp4, VT_LAGUNA_RESIDENT_DECODE=1 MARLIN_MOE=1 DECODE_GRAPH=1, ids 2,785,9626,377,15360,395, max-tokens 160): =0 and =1 emit byte-identical 159-token id streams (PASS). Paging-immune nsys cuda_gpu_kern_sum 2-length diff (160-20)/140 = steady decode/step: GPU-busy 28.90 -> 27.69 ms/step (-4.2%), graph nodes 1195 -> 1075/step (-120, -10%); drop_caches wall tied (4.48 vs 4.49s/159). Byte-exact + neutral-to-positive -> landed default-ON. FOLLOWING_AGENTS_PROTOCOL Assisted-by: Claude Code:claude-opus-4-8 [ClaudeCode]
1 parent cbe5017 commit ca5c7ad

6 files changed

Lines changed: 155 additions & 31 deletions

File tree

docs/BENCHMARKS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,27 @@ Built clean on GB10 sm_121a (incremental, **7 TUs, 0 warnings**). DGX-gated on `
9595

9696
**WASH on speed** (bf16 3.93s vs base 3.84s; spreads base 3.84–4.03 / bf16 3.93–4.07 overlap → indistinguishable) — did NOT move the residual. At the XS gate's short context (~130 tok) the decode-attention KV read is a small share of decode time; `lm_head_gemv` (streams the ~600 MB tower) + Marlin MoE dominate — so this **confirms the ~17%-to-vLLM-42.46 residual is GEMV/MoE, not KV/attention traffic**, at this context. **Near-tie BREAK on correctness**: bf16 output is coherent (re-emits the golden prefix from ~token 14) but flips the step-2 argmax 268→22345 — our device regime is not bit-identical to vLLM, so a near-tie flips even when moving TOWARD vLLM's bf16 dtype (vLLM itself uses bf16 KV yet emits 268). Two strikes → **NOT landed**; default stays f32 KV (base prefix + ~33 tok/s re-verified after DGX restore). Tested diff + analysis: `.agents/specs/laguna-kv-attn-port-2026-08-02.md`.
9797

98+
## Laguna decode attention-glue fusion — BYTE-EXACT, −4.2% GPU-busy, −120 graph nodes/step (2026-08-02, `CLAIM-LAGUNA-GLUE-FUSED`)
99+
100+
The last real Laguna-NVFP4 decode speed lever + a frameworkization win: fold the tiny, under-occupied attention-glue kernels the decode CUDA graph does NOT hide (device-side ramp/drain of the ~1,195 sequential kernel-nodes/token) into fewer, larger nodes — BYTE-EXACT. Gated behind `VT_LAGUNA_GLUE_FUSED` (default ON, `=0` opts out) for a same-binary A/B. Two folds landed on the resident decode-graph (`LagunaGraph::RunChain`) + the eager resident fallback:
101+
102+
- **L1 — softplus out-gate folded into the attention-combine store.** `SoftplusHeadGateKernel` was a separate pass doing `attn[h,d] *= softplus(g[h])` after the combine wrote `o=acc*inv`. Folding computes the same `(acc*inv)*softplus(g[h])` in registers inside `DecodeAttnCombineKernel` (+ the SPLIT≤1 fallbacks `DecodeAttnGqa{,G}Kernel`) — BYTE-EXACT because the store+reload of the f32 `acc*inv` is lossless. −1 kernel/layer.
103+
- **L4 — residual-Add + STANDARD RMSNorm pairs routed through the shared `vt::FusedChain(kFusedAddRmsNormStd)` seam** (the same machinery qwen3/gemma use): `res += x; out = rms_norm(res)*w`. BYTE-EXACT to `vt::Add + rms_norm_seq`: the f32 residual add is commutative (+ `ResRound<f32>` is identity) and the norm uses the SAME 256-thread strided-sum + shared-tree reduction + `1/sqrtf` as `RmsNormSeqKernel`. NODE-COUNT WIN in the DEFAULT Tier-0 composite (no `VT_FUSED_TIER` needed): the composite collapses `kAdd`+`kRmsNorm` to ONE `vt::RmsNorm(residual)` launch (`RmsNormRowKernel` does the add inline). The post-attn norm folds in place; the last MLP residual add folds with the NEXT layer's input norm (last layer's with the final norm), so both loose per-layer Adds disappear into fused nodes. ≈−2 kernels/layer.
104+
- **C (shared-into-MoeCombine) SKIPPED — provably NOT byte-exact in Laguna.** Unlike qwen3_5 (f32 combine out), Laguna's `MoeCombine` writes **bf16** `dout` then casts to f32, so folding the f32 shared `so` before the bf16 store rounds `(routed+shared)` to bf16 vs the current `bf16(routed)+so_f32` — a bit change. Left as its separate f32 add.
105+
- **L2 (fused qk-norm+RoPE preamble) DEFERRED.** The shared `AttnQkNormRopeGate` indexes cos/sin by token, but Laguna's graph decode needs position-from-DEVICE (`*pos_buf`, half-split layout) — a new capture-safe kernel variant; deferred to avoid correctness risk.
106+
107+
**BYTE-EXACT GATE (RED-first, `~/laguna-xs-nvfp4`, token-ids `2,785,9626,377,15360,395`, `VT_LAGUNA_RESIDENT_DECODE=1 VT_LAGUNA_MARLIN_MOE=1 VT_LAGUNA_DECODE_GRAPH=1`, max-tokens 160, SAME binary):** `=1` (fused) and `=0` (baseline) produce **byte-identical** 159-token id streams (first-20 `22345 83 268 33586 81 855 397 874 367 6376 815 340 9626 377 15360 83 1729 756 1205 565`). PASS.
108+
109+
**SPEED — paging-immune nsys `cuda_gpu_kern_sum`, `--cuda-graph-trace=node`, 2-length diff `(160−20)/140` = steady decode/step (the GB10 "e2e-wall-is-useless / GPU-busy is the anchor" rule):**
110+
111+
| arm | GPU-busy ms/step | graph nodes/step | wall decode/step (drop_caches, best-of-4) |
112+
|---|---|---|---|
113+
| `=0` baseline | 28.90 | 1,195 | 4.49s/159 = 35.4 tok/s |
114+
| `=1` fused | 27.69 | 1,075 | 4.48s/159 = 35.5 tok/s |
115+
| **Δ** | **−4.2%** | **−120 (−10%)** | **tied (no regression)** |
116+
117+
The **−120 nodes/step** confirms the folds took effect (48× softplus removed + ~2 norm/add folds/layer collapsed); GPU-busy device time drops **4.2%** — beyond the ~1.5-2.5% ramp/drain estimate, because the fused `RmsNorm(residual)` is itself cheaper than the loose `Add + RmsNorm`. At the wall the two arms are tied (`drop_caches`-standardized 4.48 vs 4.49s; the earlier un-standardized 5.95s reading was pure GB10 reload/paging noise) — the recovered device-time sits inside a graph replay still bounded by the fixed per-step host embed-gather + step-boundary sync. BYTE-EXACT + neutral-to-positive → **landed default-ON**. Repro: `~/laguna_glue_run.sh` (byte-exact) + `~/laguna_glue_nsys.sh` (nsys/wall).
118+
98119
## DeepSeek-V4-Flash — decode nsys attribution: 13.0 tok/s, Q8_0 GEMV is 53.5% (2026-08-01, `CLAIM-DSV4-DECODE-NSYS`)
99120

100121
Re-measured the GGUF decode from scratch (`deepseek-v4-gen --gpu --kv-cache`, `ds4flash.gguf` = `IQ2XXS-w2Q2K-AProjQ8-SExpQ8-OutQ8`, GB10, tmux drop-proof capture). **Decode = 13.0 tok/s** (0.076 s/step, dead-consistent over 23 steps) vs **ds4 16.5 → 79% of ds4, ~1.27× behind.** The prior-recorded **8.0 baseline is STALE** (device-resident decode + tuned glue landed since; superseded). Prefill 0.47s, PEAK RESIDENT 86.3 GiB.

docs/STATUS.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

include/vllm/model_executor/models/laguna_device.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,13 @@ struct LagunaDeviceKernels {
4545
// Physical cache row r has GLOBAL position kv_pos=first_pos+r (matches the host's
4646
// sliding-window eviction bookkeeping); causal (skip kv_pos>q_pos) + per-layer window
4747
// (skip if window>0 and q_pos-kv_pos>=window); NO attn-sink. scale=1/sqrt(Dh).
48+
// L1 (VT_LAGUNA_GLUE_FUSED): `gate` (or nullptr) folds the per-head softplus out-gate
49+
// into the normalized attention store — byte-exact vs the separate softplus_head_gate
50+
// pass, one fewer kernel/layer. nullptr keeps the un-gated store bit-for-bit.
4851
void (*decode_attn_gqa)(vt::Queue&, float* o, const float* q, const float* k, const float* v,
4952
int64_t Hq, int64_t Hkv, int64_t Dh, int64_t group, int64_t kv_rows,
50-
int64_t q_pos, int64_t first_pos, int64_t window, float scale);
53+
int64_t q_pos, int64_t first_pos, int64_t window, float scale,
54+
const float* gate);
5155
// Per-head softplus OUT-gate in place: attn[h,d] *= softplus(gate_logits[h]),
5256
// softplus(x)=(x>20)?x:log1p(exp(x)) in f32. Bit-exact to LagunaSoftplusHeadGate:25.
5357
void (*softplus_head_gate)(vt::Queue&, float* attn, const float* gate_logits, int64_t Hq,
@@ -70,10 +74,13 @@ struct LagunaDeviceKernels {
7074
// Hq / Hkv / Dh / group / scale are per-layer constants baked at capture. The key
7175
// set == decode_attn_gqa's cache[0..rows) AFTER the between-replay append, so the
7276
// replayed output is bit-identical to the eager decode_attn_gqa. NO attn-sink.
77+
// L1 (VT_LAGUNA_GLUE_FUSED): `gate` (or nullptr) folds the softplus out-gate into the
78+
// combine store (see decode_attn_gqa); capture-safe (glp is a persistent qkvg[l] slice).
7379
void (*decode_attn_gqa_g)(vt::Queue&, float* o, const float* q, const float* k, const float* v,
7480
const float* knew, const float* vnew, int64_t Hq, int64_t Hkv,
7581
int64_t Dh, int64_t group, int64_t first_pos, int64_t window,
76-
float scale, const int* len_dev, const int* pos_dev);
82+
float scale, const int* len_dev, const int* pos_dev,
83+
const float* gate);
7784
// Laguna lm_head M=1 decode GEMV: out[N] (f32) = W[N,K] (bf16, row-major) · x[K]
7885
// (f32), M=1. A dedicated one-block-per-row coalesced kernel that streams the
7986
// ~616 MB [vocab,hidden] weight ONCE at ~roofline — cuBLASLt mis-routes this

scripts/env-doc-allowlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,5 @@ VT_LAGUNA_RESIDENT_MOE
161161
VT_NVFP4_FP4_GEMV
162162
VT_LAGUNA_DECODE_GRAPH
163163
VT_LAGUNA_MARLIN_MOE
164+
VT_LAGUNA_GLUE_FUSED
164165
VLLM_CPP_QWEN3_DENSE_DECODE_GRAPH

src/vllm/model_executor/models/laguna.cpp

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
#include "vt/backend.h" // vt::GetBackend (device drain for the keep-quant GEMMs)
5050
#include "vt/dtype.h" // vt::IsBlockQuant / RowSizeBytes
5151
#include "vt/ops.h" // vt::MatmulBT (dispatches kMatmulBTQuant on block weights)
52+
#include "vt/recipes.h" // vt::kFusedAddRmsNormStd (L4 residual-add + RMSNorm fusion)
5253
#include "vt/tensor.h"
5354

5455
namespace vllm {
@@ -1501,6 +1502,24 @@ inline bool LagunaDecodeGraphEnabled() {
15011502
return on;
15021503
}
15031504

1505+
// ── Attention-glue fusions on the resident decode (VT_LAGUNA_GLUE_FUSED, default ON).
1506+
// Recovers the device-time the CUDA graph does NOT hide (ramp/drain of ~450 tiny
1507+
// under-occupied sequential kernel-nodes/token) by BYTE-EXACT folds that shrink the
1508+
// captured node count: L1 folds the per-head softplus out-gate into the attention
1509+
// combine store (one fewer kernel/layer); L4 folds each residual-Add + RMSNorm pair
1510+
// into ONE vt::FusedChain(kFusedAddRmsNormStd) call (two fewer kernels/layer with the
1511+
// Tier-1 interpreter, VT_FUSED_TIER=1). Both are bit-for-bit identical to the toggle-
1512+
// off path (same f32 arithmetic, same 256-thread RMSNorm reduction order) — a same-
1513+
// binary A/B: VT_LAGUNA_GLUE_FUSED=0 restores the separate kernels. inline so a CPU
1514+
// build (VT_MARLIN_NVFP4 off) does not -Wunused.
1515+
inline bool LagunaGlueFusedEnabled() {
1516+
static const bool on = [] {
1517+
const char* e = std::getenv("VT_LAGUNA_GLUE_FUSED");
1518+
return !(e != nullptr && e[0] == '0'); // default ON; =0 opts out
1519+
}();
1520+
return on;
1521+
}
1522+
15041523
bool LagunaCanRunResidentDecode(const LagunaParams& p, vt::Queue& q, const LagunaWeights& w,
15051524
int64_t T) {
15061525
#ifndef VT_MARLIN_NVFP4
@@ -1675,13 +1694,16 @@ std::vector<float> LagunaForwardResidentDecode(const LagunaWeights& weights, vt:
16751694
vt::GetBackend(dev).Copy(q, vdev + dr * kvdim, vnp, rowbytes);
16761695
const int64_t rows = dr + 1;
16771696
const int64_t fp = cache.dev_first_pos[static_cast<size_t>(l)];
1697+
// LEVER 1: the fused GEMV already produced g (glp -> qkvg_buf); only the split
1698+
// fallback still needs a dedicated g_proj GEMV. L1 folds the softplus out-gate into
1699+
// decode_attn_gqa's store, which reads g — so produce it BEFORE attention (reorder of
1700+
// two independent same-stream ops => byte-exact).
1701+
if (!fused_qkvg) GemmBf16Into(gl.data(), lw.attn.g_proj, hn.data(), Hq, H);
1702+
const bool glue_fused = LagunaGlueFusedEnabled();
16781703
LAG->decode_attn_gqa(q, attn.data(), qvp, kdev, vdev, Hq, Hkv, Dh, group, rows, pos, fp,
1679-
window, scale);
1704+
window, scale, glue_fused ? glp : nullptr);
16801705
cache.dev_rows[static_cast<size_t>(l)] = rows; // advance this layer's cached-row count
1681-
// LEVER 1: the fused GEMV already produced g (glp -> qkvg_buf); only the
1682-
// split fallback still needs a dedicated g_proj GEMV here.
1683-
if (!fused_qkvg) GemmBf16Into(gl.data(), lw.attn.g_proj, hn.data(), Hq, H);
1684-
LAG->softplus_head_gate(q, attn.data(), glp, Hq, Dh);
1706+
if (!glue_fused) LAG->softplus_head_gate(q, attn.data(), glp, Hq, Dh); // L1 off => separate pass
16851707
GemmBf16Into(o.data(), lw.attn.o_proj, attn.data(), H, qdim);
16861708
{
16871709
vt::Tensor ht = vt::Tensor::Contiguous(hidden.data(), DType::kF32, dev, {1, H});
@@ -1958,6 +1980,28 @@ struct LagunaGraph {
19581980
vt::Tensor at = DevT(a, H);
19591981
vt::Add(q, at, at, DevT(b, H));
19601982
};
1983+
const bool glue_fused = LagunaGlueFusedEnabled();
1984+
// L4: fold a residual-Add + STANDARD RMSNorm pair into ONE vt::FusedChain
1985+
// (kFusedAddRmsNormStd): res += x; out = rms_norm(res)*w. BYTE-EXACT to
1986+
// AddInto(res,x) + rms_norm_seq(out,res,w): the f32 residual add is the same
1987+
// (commutative + ResRound<f32> is identity), and the RMSNorm uses the SAME 256-thread
1988+
// strided sum + shared-tree reduction + 1/sqrtf as RmsNormSeqKernel. NODE-COUNT WIN in
1989+
// BOTH tiers: the default Tier-0 composite collapses the chain to ONE
1990+
// vt::RmsNorm(residual) launch (RmsNormRowKernel does the add inline), and Tier-1
1991+
// (VT_FUSED_TIER=1) to one interpreter kernel — either way 2 kernels -> 1. All
1992+
// operands are persistent member buffers => capture-safe (same temporary-Tensor
1993+
// pattern as the AddInto lambda already captured in this graph).
1994+
auto FusedAddNorm = [&](float* out, float* x, const float* wgt, float* res) {
1995+
vt::Tensor o2 = DevT(out, H), x2 = DevT(x, H), r2 = DevT(res, H);
1996+
vt::Tensor w1 = vt::Tensor::Contiguous(const_cast<float*>(wgt), vt::DType::kF32, dev, {H});
1997+
vt::FusedChain(q, o2, x2, w1, &r2, vt::kFusedAddRmsNormStd, eps);
1998+
};
1999+
// L4: when fused, `hn` always holds rms_norm(hidden, input_norm[l]) on entry to layer
2000+
// l — layer 0 is seeded here (no preceding residual add to fold), and every later
2001+
// layer inherits it from the previous layer's fused Pair-2 tail (last MLP add folded
2002+
// with the next input norm). When NOT fused, the input norm runs at each loop top.
2003+
if (glue_fused)
2004+
LAG->rms_norm_seq(q, hn.data(), hidden.data(), input_norm_f[0].data(), 1, H, eps, true);
19612005
for (int64_t l = 0; l < nlayers; ++l) {
19622006
const LagunaLayerWeights& lw = w->layers[static_cast<size_t>(l)];
19632007
const LayerC& c = lc[static_cast<size_t>(l)];
@@ -1972,8 +2016,9 @@ struct LagunaGraph {
19722016
// LEVER A: the position-indexed full RoPE table for this layer's regime; the graph
19732017
// RoPE indexes row *pos_buf on-device (no per-step host cos/sin rebuild).
19742018
const float* rcache = (c.global ? yarn_full : slide_full).data();
1975-
LAG->rms_norm_seq(q, hn.data(), hidden.data(), input_norm_f[static_cast<size_t>(l)].data(), 1,
1976-
H, eps, true);
2019+
if (!glue_fused) // L4: fused path carries hn in from the previous layer's Pair-2 tail
2020+
LAG->rms_norm_seq(q, hn.data(), hidden.data(),
2021+
input_norm_f[static_cast<size_t>(l)].data(), 1, H, eps, true);
19772022
GemmBf16(base, lw.attn.qkvg_proj, hn.data(), c.qdim + 2 * kvdim + c.Hq, H);
19782023
if (p->has_qk_norm && !lw.attn.q_norm.Empty()) {
19792024
LAG->rms_norm_seq(q, qvp, qvp, q_norm_f[static_cast<size_t>(l)].data(), c.Hq,
@@ -1986,7 +2031,8 @@ struct LagunaGraph {
19862031
// len/pos read from the DEVICE buffers. first_pos=0 (uniform, P<512).
19872032
LAG->decode_attn_gqa_g(q, attn.data(), qvp, cache_k[static_cast<size_t>(l)].data(),
19882033
cache_v[static_cast<size_t>(l)].data(), kn, vn, c.Hq, Hkv, Dh, c.group,
1989-
/*first_pos=*/0, c.window, scale, len_buf.data(), pos_buf.data());
2034+
/*first_pos=*/0, c.window, scale, len_buf.data(), pos_buf.data(),
2035+
glue_fused ? glp : nullptr); // L1: fold softplus out-gate
19902036
// LEVER A: append this token's post-RoPE K (kn) and raw V (vn) into the growing
19912037
// cache at the DEVICE-read slot *len_buf, IN-GRAPH — folds the 2×nlayers between-
19922038
// replay host Copy launches into the captured graph. Runs after decode_attn_gqa_g
@@ -1995,17 +2041,29 @@ struct LagunaGraph {
19952041
LAG->append_kv_row(q, cache_k[static_cast<size_t>(l)].data(),
19962042
cache_v[static_cast<size_t>(l)].data(), kn, vn, kvdim, len_buf.data());
19972043
// g was produced by the fused GEMV (glp -> qkvg[l]); untouched until here.
1998-
LAG->softplus_head_gate(q, attn.data(), glp, c.Hq, Dh);
2044+
if (!glue_fused) // L1 off => separate softplus out-gate pass
2045+
LAG->softplus_head_gate(q, attn.data(), glp, c.Hq, Dh);
19992046
GemmBf16(o.data(), lw.attn.o_proj, attn.data(), H, c.qdim);
2000-
AddInto(hidden.data(), o.data());
2001-
LAG->rms_norm_seq(q, hn.data(), hidden.data(), post_norm_f[static_cast<size_t>(l)].data(), 1,
2002-
H, eps, true);
2047+
// L4 Pair 1 (post-attn): hidden += o; hn = rms_norm(hidden, post_norm[l]).
2048+
if (glue_fused) {
2049+
FusedAddNorm(hn.data(), o.data(), post_norm_f[static_cast<size_t>(l)].data(), hidden.data());
2050+
} else {
2051+
AddInto(hidden.data(), o.data());
2052+
LAG->rms_norm_seq(q, hn.data(), hidden.data(), post_norm_f[static_cast<size_t>(l)].data(), 1,
2053+
H, eps, true);
2054+
}
2055+
// L4 Pair 2: the LAST residual add of this layer folds with the NEXT layer's input
2056+
// RMSNorm (or the post-loop final RMSNorm on the last layer) — computed into hn.
2057+
const float* next_norm = (l + 1 < nlayers)
2058+
? input_norm_f[static_cast<size_t>(l + 1)].data()
2059+
: final_norm_f.data();
20032060
if (c.is_dense) {
20042061
GemmBf16(dg.data(), lw.mlp.gate_proj, hn.data(), dense_I, H);
20052062
GemmBf16(du.data(), lw.mlp.up_proj, hn.data(), dense_I, H);
20062063
SiluMul(dact.data(), dg.data(), du.data(), dense_I);
20072064
GemmBf16(fdn.data(), lw.mlp.down_proj, dact.data(), H, dense_I);
2008-
AddInto(hidden.data(), fdn.data());
2065+
if (glue_fused) FusedAddNorm(hn.data(), fdn.data(), next_norm, hidden.data());
2066+
else AddInto(hidden.data(), fdn.data());
20092067
} else {
20102068
// LEVER 2: fused router|shared_gate|shared_up GEMV -> rsg slices.
20112069
GemmBf16(rsg.data(), lw.moe.router_shared_gu, hn.data(), E + 2 * moe_I, H);
@@ -2021,8 +2079,10 @@ struct LagunaGraph {
20212079
topk, doutb.data());
20222080
SiluMul(dact.data(), sgp, sup, moe_I);
20232081
GemmBf16(so.data(), lw.moe.shared_down, dact.data(), H, moe_I);
2024-
AddInto(hidden.data(), doutb.data()); // hidden += routed
2025-
AddInto(hidden.data(), so.data()); // hidden += shared
2082+
AddInto(hidden.data(), doutb.data()); // hidden += routed (always a plain add)
2083+
// hidden += shared, folded with the next layer's input (or final) RMSNorm.
2084+
if (glue_fused) FusedAddNorm(hn.data(), so.data(), next_norm, hidden.data());
2085+
else AddInto(hidden.data(), so.data());
20262086
}
20272087
}
20282088
// final RMSNorm + lm_head INSIDE the captured region (device), into persistent
@@ -2034,7 +2094,9 @@ struct LagunaGraph {
20342094
// roofline, the measured #1 decode GPU cost). Fixed grid=Vsz + fixed pointers ⇒
20352095
// capture-safe. (f32/quant lm_head would fall back to GemmBf16, but the nvfp4 arm
20362096
// is always bf16 here.)
2037-
LAG->rms_norm_seq(q, hn.data(), hidden.data(), final_norm_f.data(), 1, H, eps, true);
2097+
if (!glue_fused) // L4: fused path already produced hn=norm(hidden,final_norm) in the
2098+
// last layer's Pair-2 tail (next_norm==final_norm_f when l+1==nlayers)
2099+
LAG->rms_norm_seq(q, hn.data(), hidden.data(), final_norm_f.data(), 1, H, eps, true);
20382100
if (w->lm_head.dtype == vt::DType::kBF16)
20392101
LAG->lm_head_gemv(q, logits.data(),
20402102
reinterpret_cast<const void*>(w->lm_head.bytes.data()), hn.data(), Vsz, H);

0 commit comments

Comments
 (0)