Skip to content

feat(qwen4): add Qwen3.8-Flash-Next Gated DeltaNet golden - #1152

Draft
sysuTobo wants to merge 10 commits into
pegainfer-project:mainfrom
sysuTobo:feat/qwen38fn-gdn-golden
Draft

sysuTobo wants to merge 10 commits into
pegainfer-project:mainfrom
sysuTobo:feat/qwen38fn-gdn-golden

Conversation

@sysuTobo

@sysuTobo sysuTobo commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Reference golden for the Gated DeltaNet sublayer of Qwen3.8-Flash-Next (#1103).

  • tools/accuracy/dump_qwen38fn_gdn_golden.py — a pure recorder: it dumps Qwen4ExpTextGatedDeltaNet (transformers 5.16.1 qwen4_exp) at the real checkpoint geometry, including the output_gate_type: "sigmoid" gated norm that the in-tree kernel hardcodes as silu.
  • test_data/qwen38fn-gdn-golden.safetensors — prefill chunk cases S=16 / S=64 / S=80 / S=128 — the last two cross a chunk boundary, so the reference's carried recurrent state is covered.
  • Geometry: real frozen-config geometry (hidden 2560, 16 K / 48 V heads @ 128, conv kernel 4, sigmoid gate). The plain-GEMM weights in_proj_qkv / in_proj_z / in_proj_a / in_proj_b and out_proj are NOT stored (~110 MiB combined, over GitHub's 100 MiB push limit) — their outputs are recorded instead, and out_proj is dropped because the golden's expected output is the gated-norm output before it. Only conv1d.weight and norm.weight are stored: the two weights a GDN kernel applies to the recorded activations. A_log / dt_bias are not stored (nor is the layer input hidden): the fixture records g / beta in their post-derivation form, so those two vectors have no stored a / b projection output to act on — a gate that starts from the raw a / b projections needs its own dump. The V/K ratio 3 (repeat_interleave), the depthwise conv and the sigmoid gated norm are preserved.

Validation

The dump asserts nothing: what it stores is the reference's own output at the real geometry. The consumer gate asserts the fixture digest on load.

$ python tools/accuracy/dump_qwen38fn_gdn_golden.py --out test_data/qwen38fn-gdn-golden.safetensors
case0: S=16 out_absmax=5.0938
case1: S=64 out_absmax=6.0000
case2: S=80 out_absmax=7.0000
case3: S=128 out_absmax=5.8125
wrote test_data/qwen38fn-gdn-golden.safetensors (18.16 MiB, 38 tensors)

The two original cases and the shared weights are bit-identical to the previous revision — only case2_* / case3_* were added (20 -> 38 tensors, 5.10 -> 18.16 MiB) — the metadata JSON and format_version stay unchanged, and re-running into a scratch path is byte-identical. Chunk coverage traced through the reference's chunk loop (modeling_qwen4_exp.py:327): S=16/64 run one chunk, S=80/128 run two.

The fixture + dump-script commits carry no runtime code changes. The consumer gate below adds a test-only refactor to pegainfer-qwen35 (shipped line) plus the gate itself.

Consumer

pegainfer-qwen35/tests/qwen38fn_gdn_prefill_parity.rs reads the fixture and replays the GDN prefill stack against the recorded out.

What reads the fixture. Per case the gate replays the in-tree GDN prefill stack: Qwen3.5's causal conv1d prefill kernel → the chunk-wise gated delta rule (the qwen35 Triton AOT kernels) → the shared gated RMSNorm with the sigmoid activation this checkpoint's output_gate_type selects, and compares the result against the recorded out (the sublayer output before out_proj). The fixture records the gate pair g/beta after derivation (the a/b projections, dt_bias, A_log that would reproduce them are not stored), so the gate injects them into the chunk stages directly; the derivation itself is not this gate's subject — the chunk math it feeds is.

Placement. The gate lives in pegainfer-qwen35/tests/ rather than a pegainfer-qwen4 or standalone crate because the chunk-wise prefill driver and its scratch (GdrChunkwiseScratch35) are only exposed inside pegainfer-qwen35 (pegainfer-kernels exposes only the raw per-stage FFI). Sitting the reader beside the operator it drives avoids re-sequencing six stages over FFI in a second place.

Shipped-line diff (pegainfer-qwen35). To let an out-of-crate test drive the chunk math with a recorded gate pair, gated_delta_rule_prefill_chunkwise_into is split into a prepare stage (head-expand q/k, L2-norm, split v, derive the raw g/beta) and a from_gates stage (the chunk math over a scratch that already carries the gate pair). The production entry point still runs prepare → cumsum → A → solve → recompute → state → output in the same order over the same kernels, so serving numerics are unchanged; the split only exposes the seam. Three functions (conv1d_prefill_batch_into and the two stages) and the scratch's g_cumsum/beta fields widen from pub(crate) to pub (re-exported via runtime_ops) so the gate can inject the recorded gates. No production caller, kernel, or buffer layout changes.

Regression evidence.

  • cargo test --release --locked -p pegainfer-qwen35 --features qwen35 --lib → 111 passed, 0 failed, 8 ignored.
  • cargo clippy --release --locked -p pegainfer-qwen35 --features qwen35 --all-targets -- -D warnings → clean.
  • cargo fmt --all -- --check → clean.
  • The gate exercises the refactored stages (prepare, from_gates), the conv kernel and the gated norm on GPU and matches the golden, so the split stages are GPU-verified. The production composition (chunkwise_into) is a mechanical pass-through not separately exercised here — the fixture stores post-derivation gates, so it cannot drive chunkwise_into's derivation end-to-end.

Commands + measured numbers.

./docker/dev.sh run cargo test --release --locked -p pegainfer-qwen35 \
  --features qwen35 --test qwen38fn_gdn_prefill_parity -- --nocapture

On the RTX 5060 (sm_120) host, all 5 tests pass:

case chunks conv leg max-abs gated-norm max-abs budget used out of bound
S=16 1 0.0 (bit-exact) 4.69e-2 27.8% 0 / 98304
S=64 1 0.0 (bit-exact) 4.69e-2 22.3% 0 / 393216
S=80 2 0.0 (bit-exact) 6.25e-2 30.1% 0 / 491520
S=128 2 0.0 (bit-exact) 6.25e-2 25.5% 0 / 786432

Tolerance derivation. The bound is abs(got-want) <= ATOL + RTOL*abs(want) with ATOL=1.5e-1, RTOL=2.5e-2. The recurrent state is f32 but the stored out is bf16, so the comparison is quantized to bf16 ulps of values up to ~7 (one ulp ≈ 3.1e-2..6.25e-2 there). The measured worst case is 6.25e-2 — two bf16 ulps, rounding-order noise rather than bias (the multi-chunk cases show the same shape as the single-chunk ones, so the carried state adds no drift of its own). The tightest element uses 30.1% of its allowance (~3.3× headroom); at want=0 the bound is 2.4× the worst max-abs. The margin is kept wide on purpose so a future toolchain that reorders a dot product does not flake. The conv leg is bit-exact (max-abs 0.0 over 2.9M elements) — its 1e-3 bound is a fuse, not a noise allowance.

Negative controls (each its own test, asserting the comparison fails the way the named fault fails — worst element past the bound by CONTROL_MIN_WORST_RATIO=10, plus a per-control violation-fraction floor):

  • silu instead of sigmoid (the exact output_gate_type trap of this checkpoint): worst element 88–130× past bound, 24.8–25.8% of elements out.
  • skipped conv leg: 29–34× past, 52.7–53.3% out.
  • S=128 replayed as two independent single-chunk runs (state carry dropped): the first half still matches (0 violations) while the tail misses by 15.4× on its worst element — pinning the fault on the dropped carry, not the split. The carry is genuinely narrow (gate decay attenuates the carried state over 64 tokens, so only 0.51% of tail elements leave the bound), which is why this control leans on worst-ratio + the head/tail contrast rather than a wide fraction.

What remains ungated.

  • The decode path (recurrent single-token GDN) is not covered — this gate is prefill-only.
  • Real-weight parity: the gate replays recorded activations, not a loaded checkpoint; end-to-end logits against a real Qwen3.8-Flash-Next checkpoint are not asserted here.
  • No CI job runs --features qwen35 gates for this fixture yet — the gate is run manually via docker/dev.sh.
  • The gate does not exercise the a/b → g/beta derivation (the fixture stores post-derivation gates).

Host note. On this host (RTX 5060 / sm_120, CUDA 12.9 driver 575.57.08) the default Triton 3.7.1 toolchain compiles sm_120 with its bundled ptxas-blackwell (CUDA 13.1), and the 12.9 driver's cuModuleLoadData segfaults on the 13.1 cubin — this affects every qwen35 Triton kernel, not just this gate, and is independent of this diff. Building with TRITON_PTXAS_BLACKWELL_PATH=/usr/local/cuda-12.9/bin/ptxas (ptxas 12.9 supports sm_120a) produces driver-loadable cubins; that is how the numbers above were obtained. A CUDA 13.x driver loads the default cubins without the override.

Refs #1103

@github-actions github-actions Bot added the enhancement New feature or request label Oct 12, 2026
@sysuTobo

Copy link
Copy Markdown
Contributor Author

Code review

范围:2 文件全覆盖 —— tools/accuracy/dump_qwen38fn_gdn_golden.py(200 行)+ test_data/qwen38fn-gdn-golden.safetensors(18 张量),无运行时数据流。
正向路径(本机实测,torch 2.14.1+cpu / transformers 5.16.1,与 PR body 声明一致):

  • 重推导 torch.equal(out, rederived) 逐位相等(dump_qwen38fn_gdn_golden.py:179)—— project/finish/forward_ref 的分解 wiring 正确,非恒真。
  • 用 fixture 的 9 个权重张量 strict 重建模块并回放:missing/unexpected=[],case0 逐位相同、case1 差 2.44e-4(bf16 跨环境 ULP 漂移,即 PR body 承认的那条)——权重完整、fixture 自洽。
  • 当前 fixture 确为 sigmoid gate:norm.activation=sigmoid、config_sha256 前 8 hex aa4da0bf(与本地重算一致)、conv1d.bias=None(所有 Linear/conv 均 bias=False)。
  • chunk/recurrent 两臂共用同一份 project() 输出(对照臂一致),本环境实测 maxabs=0.0(逐位)。
ID 级别 问题 证据
M1 M 本 golden 的存在理由(sigmoid gate)在 dump 内没有任何断言看守,重推导对它是同源比较。out(module.forward)与 rederived(forward_ref→finish→module.norm)都调同一个 module.norm,gate 激活由 config 内部选择。实测把 gate 换成 silu 后 torch.equal(out, rederived) 仍为 True(而输出偏离 golden 达 0.70)。gate 只被 config_sha256 记录,但 digest 从 live config 自洽写入、dump 内从不 assert → 未来有人改 OUTPUT_GATE_TYPE(或参考默认变动)重生成时,dump 会静默全绿地产出一份 silu fixture。当前 fixture 本身正确,此项是把 raison-d'être 不变量的看守前移到源头。 dump_qwen38fn_gdn_golden.py:51,71,78,114,179
L1 L chunk↔recurrent 交叉验证的容差无标定依据。atol=2e-2, rtol=2e-2 相对实测 maxabs=0.0(逐位)、out_absmax≈1.0 约为 5 个 bf16 ULP;注释只说"两算法应收敛",未给这个数从哪来。主 guard(对 module 的 torch.equal)已逐位,此为二次检查,宽松风险不高,但足以放走一次真实的 chunk/recurrent 分歧。 dump_qwen38fn_gdn_golden.py:180-184
L2 L decode/recurrent 路径只在 dump 时一次性断言,未落进 fixture。fixture 只存 prefill 的 case{i}_out;recurrent 输出没有保存、无 recurrent state,且交叉验证用 initial_state=None 的全序列 recurrent,不是带缓存的单步 decode。想 gate decode kernel 的消费者从本 fixture 拿不到 oracle。PR 范围写明"2 prefill cases",属已知边界,但 docstring 未点明 decode 不落盘,易被后续 gate 误当作已覆盖。 dump_qwen38fn_gdn_golden.py:57,173-177,187-190
L3 L PR body Validation 的 "reload reproduces the output bit-for-bit from the stored weights" 脚本并未演练(无 load_file/重建步骤),且受 bf16 跨环境漂移影响:实测 case1 从 stored weights 回放差 2.44e-4,并非 bit-for-bit。权重完整(strict load 通过)、fixture 确自洽,仅"bit-for-bit"措辞是环境相关的。#1150 同样无脚本内 reload,属家族约定。 dump_qwen38fn_gdn_golden.py:150-196
L4 L(家族) config_digest 漏 RMS_EPS:gated norm 读 eps(rsqrt(variance+eps),transformers qwen4_exp Qwen4ExpTextRMSNormGated.forward),改 eps 会改输出却不改 digest → 一小块假 provenance 面。#1150 的 digest 同样未收 eps,属家族级;记录给系列,不在本 PR 单独改(改它会变动 config_sha256 → 触发 fixture 替换 + 与姊妹 PR 分歧)。SEED/CASES 不入 digest 是对的(权重、每 case 的 seq_len 都已存)。 dump_qwen38fn_gdn_golden.py:50,75-80

最小加固(均不改 RNG/计算,fixture 逐字节不变,无需替换 committed 文件):

  • 修 M1:module 构造后加一行 assert module.norm.activation == "sigmoid",把 gate 不变量从"消费者断言 digest"前移到"dump 当场失败"。
  • 补 L1/L2 文档:L1 给容差一句标定依据(bf16 输出 O(1)、两规则在 bf16 精度下一致、2e-2 为跨后端归约顺序留裕量);L2 在 docstring 点明 decode/recurrent 仅 dump 时交叉验证、不落盘,fixture 只存 prefill 输出。
  • L3/L4 记为已知/家族边界,不动本 PR 代码(避免 fixture churn 与 feat(qwen4): add Qwen3.8-Flash-Next QSA indexer golden #1150 分歧)。

@sysuTobo

Copy link
Copy Markdown
Contributor Author

修改措施

Pushed 37e54d7f。

  • M1 已修:module 构造后新增 assert module.norm.activation == "sigmoid",把本 golden 的 raison-d'être 不变量从"消费者断言 config_sha256"前移到"dump 当场失败"。负测已验证:silu 模块触发该断言(dump abort),sigmoid 通过。断言不触碰 RNG/计算,fixture 逐字节不变。dump_qwen38fn_gdn_golden.py:142-147
  • L1 已修(注释):给 atol=2e-2, rtol=2e-2 补标定依据 —— bf16 输出 O(1)、两规则在 bf16 精度下一致(实测 maxabs=0.0 逐位)、2e-2 ≈ 5 个 bf16 ULP 为跨后端归约顺序留裕量;并点明主 guard 是对 module 的逐位 torch.equal,此交叉验证是二次网。dump_qwen38fn_gdn_golden.py:190-197
  • L2 已修(docstring):明确 chunk/recurrent 交叉验证仅在 dump 时进行,recurrent 输出与 recurrent state 均不落盘,本 fixture 是 prefill 路径的 oracle,decode kernel gate 需另建 dump。dump_qwen38fn_gdn_golden.py:13-18,24
  • L3 接受为已知边界:脚本仍不做 reload/重建(与 feat(qwen4): add Qwen3.8-Flash-Next QSA indexer golden #1150 家族约定一致);权重完整(strict load missing/unexpected=[]),case1 从 stored weights 回放差 2.44e-4 属 bf16 跨环境 ULP 漂移,故 PR body 的 "bit-for-bit" 措辞是环境相关的。记录在案,不加脚本内自检以免与姊妹 PR 分歧。
  • L4 不修(家族级):config_digest 未收 RMS_EPS,与 feat(qwen4): add Qwen3.8-Flash-Next QSA indexer golden #1150 一致;改它会变动 config_sha256 → 触发 fixture 替换 + 与姊妹 PR 分歧,故记录给 feat(qwen4): add Qwen3.8-Flash-Next QSA indexer golden #1150–feat(qwen4): add Qwen3.8-Flash-Next PLE n-gram golden #1155 系列统一处理,本 PR 不动。

复验:fixture 未替换(三处改动全部 fixture-inert,无 RNG/计算变更),仍 18 张量;provenance = ref_transformers=5.16.1 / config_sha256=aa4da0bf… / format_version=1。重跑 dump(torch 2.14.1+cpu / transformers 5.16.1):2 case rederivation_ok=True;regen vs committed 17/18 张量逐字节相同,仅 case1_out 差 2.44e-4(PR body 已承认的 bf16 跨环境 ULP 漂移)。从 stored weights 重建回放:case0 逐位、case1 差 2.44e-4;chunk↔recurrent 实测 maxabs=0.0(逐位)。新增 gate 断言负测:silu → FIRES、sigmoid → PASSES。红线扫描(评论正文 + git diff origin/main...HEAD)零命中。

@github-actions github-actions Bot added the qwen35 Qwen3.5 model crate (pegainfer-qwen35) label Oct 16, 2026
@sysuTobo

Copy link
Copy Markdown
Contributor Author

Code review

TL;DR:正向数据流全部符合预期——mixed_qkv → conv1d(逐位精确)→ prepare(head 扩展 + L2norm,g/beta 槽位被记录值覆盖)→ cumsum→A→solve→recompute→state(f32 递归态、跨 chunk carry)→ output → gated RMSNorm(sigmoid) → out,4 个 case 对记录 out 0 越界;shipped-line 改动是保序重构 + 可见性放开,未改任何 kernel/生产调用。真正阻塞点是环境级的 Triton cubin 与驱动版本不匹配(非本 diff)。共 7 条发现,其中 3 条已修、1 条环境项留给 lead、3 条为核实/接受。

ID 级别 一句话 证据(文件:行)
1 H(环境,非本 diff) Triton 3.7.1 用内置 ptxas-blackwell(CUDA 13.1) 编 sm_120,宿主 12.9 驱动 cuModuleLoadData 对 13.1 cubin 直接 SIGSEGV,阻塞所有 qwen35 Triton kernel 实测 cuModuleLoadData exit 139;cubin release 13.1 V13.1.80 vs 驱动 575.57.08
2 已修(H) 主 parity 测试 report(...) 多传一参(5 实参 vs 4 形参)→ 原未提交态根本编译不过;且 CONTROL_MIN_VIOLATION_FRACTION=0.0 使 fraction>=0.0 恒真 = 空断言 原 test line 644(已删多余参)、原 line 139(=0.0);现 per-control 下限 qwen38fn_gdn_prefill_parity.rs:164-167
3 已修(M) clippy --all-targets -D warnings 三处报错:chunks_exact(2/4) 常量步长、&vec![bf16::ZERO; N] test:285/293/470(改前)→ as_chunks::<N>().0.iter() / &[..; N]
4 就地更正(M) 生产入口 chunkwise_into 未被本 gate 直接 GPU 跑(gate 直驱 prepare+from_gates),组合正确性靠阅读核对 recurrent.rs:467-506(新入口=prepare+from_gates)与原始 stage 顺序逐条比对一致
5 接受(L) 重构把 w/u/v_new/gate 尺寸断言从 prepare 前移到 from_gates(prepare 后):非法输入多跑一个 prepare kernel 再 panic,生产(合法输入)无差异 recurrent.rs:517-550
6 核实(L) provenance 断言完整且 fail-closed:key 可解析、format_version==2、config_sha256 == 树内 frozen_config.json 的 sha256(实测 889658f2… 相符)、5 项几何、output_gate_type==sigmoid、rms_norm_eps==1e-6;缺文件/配置 panic 不 skip qwen38fn_gdn_prefill_parity.rs:191(assert_provenance)、load_fixture
7 接受(L) conv-leg 测试跑完整 GDN pipeline 却只比 conv 输出(轻微冗余计算,无害;conv_host 在 prepare 前已回读) qwen38fn_gdn_prefill_parity.rs:625(report)所在 conv 测试复用 replay_case

修改措施

  • ID1(环境)——明确留给 lead/环境:非本 diff 缺陷。本机(RTX 5060 / sm_120,CUDA 12.9 驱动)验证时用 TRITON_PTXAS_BLACKWELL_PATH=/usr/local/cuda-12.9/bin/ptxas(ptxas 12.9 支持 sm_120a)重建,得到驱动可加载的 cubin,全部 5 个测试通过;CUDA 13.x 驱动可直接加载默认 cubin。已写入 PR body 的 Host note,未改任何被追踪代码/构建脚本。
  • ID2——已修:删除 report 多余实参(现 4 参一致);把单一 vacuous 下限拆成 per-control 下限 CONTROL_SILU_MIN_FRACTION=0.10 / CONTROL_CONV_MIN_FRACTION=0.25 / CONTROL_STATE_MIN_FRACTION=0.002(各约 2x 低于实测切片:silu 24.8%、conv 53%、state-carry 0.51%),并保留通用 CONTROL_MIN_WORST_RATIO=10(三控制实测 15–130x)。report 现打印 max-abs、bound、budget 使用率、越界数(qwen38fn_gdn_prefill_parity.rs:625)。
  • ID3——已修:cargo clippy --release --locked -p pegainfer-qwen35 --features qwen35 --all-targets -- -D warnings 干净通过;cargo fmt --all -- --check 干净。
  • ID4——就地更正:核对 chunkwise_into 仍按 prepare→cumsum→A→solve→recompute→state→output 原序、参数逐项对齐(recurrent.rs:467-506 vs from_gates:517-611);shipped-line 回归证据 = lib 测试 111 passed / 0 failed / 8 ignored + clippy/fmt 干净;"组合未单独 GPU 跑(fixture 存 post-derivation gate 所致)"已在 PR body 写明,非遗漏。
  • ID5——接受并记录:对合法输入行为等价,断言顺序变化仅影响非法输入的 panic 时机,已在 PR body 说明。
  • ID6——核实通过:无需改;断言覆盖任务要求的全部 provenance 项,实测与树内 config 字节相符。
  • ID7——接受:保留单一 replay 路径以免第二份实现(避免 D1 重复),冗余计算量小且不影响 conv 比对正确性。

@github-actions

Copy link
Copy Markdown

This pull request has merge conflicts with main. Please rebase it so it can be reviewed and merged.

Signed-off-by: bohaotu <bohaotu@pony.ai>
The GDN golden exists to pin the `output_gate_type: "sigmoid"` gated norm
that the in-tree kernel hardcodes as silu. The re-derivation assertion
calls `module.norm` on both arms, so it is same-source for the gate and
cannot catch a sigmoid->silu flip (verified: a silu regen stays green
while the output drifts 0.70 from the golden). Assert the module's norm
activation so a regen that loses the defining invariant fails at source.

Also record the chunk/recurrent cross-check tolerance basis and note that
decode/recurrent is cross-checked at dump time only (the fixture oracles
the prefill path). Fixture-inert: no RNG/compute change, committed
fixture byte-unchanged.

Signed-off-by: bohaotu <bohaotu@pony.ai>
Replace the reduced-geometry `Config` class with the real checkpoint
`text_config` (hidden 2560, 16 K / 48 V heads @ 128, conv kernel 4,
`sigmoid` gate) and bump the provenance to `format_version` 2 in one JSON
metadata key.

Real geometry makes `Qwen4ExpTextGatedDeltaNet` ~110 MiB, over GitHub's
100 MiB push limit, so only the small component-local weights are stored
(`conv1d.weight`, `A_log`, `dt_bias`, `norm.weight`); the plain-GEMM
projections (`in_proj_qkv`/`in_proj_z`/`in_proj_a`/`in_proj_b`) are recorded
through forward hooks as the activations entering the next stage (pre-conv
`mixed_qkv`, its post-conv `query`/`key`/`value`, `z`, `g`, `beta`) and
`out_proj` is dropped: the golden's output is the gated-norm output before
it. Cases S=16/S=64 cover the prefill chunk path; the recurrent rule is
cross-checked in fp32. The sigmoid-gate guard, the V/K ratio 3
(`repeat_interleave`) and the depthwise conv traps are preserved.

Fixture reloads and reproduces every stored tensor from the stored inputs
plus stored weights bit-for-bit.

Signed-off-by: bohaotu <bohaotu@pony.ai>
The other four dumpers resolve the repo root from __file__; this one defaulted
--root to the current directory, so running it from anywhere but the repo root
failed with a FileNotFoundError on the frozen config.

Signed-off-by: bohaotu <bohaotu@pony.ai>
The fixture stored three tensors nothing could exercise: `A_log`/`dt_bias`
[48] and the per-case layer input `hidden` [1,S,2560]. `A_log`/`dt_bias` only
drag `g = -A_log.exp()*softplus(a+dt_bias)` out of the `a`/`b` projections, and
the fixture records `g` itself -- the `in_proj_a`/`in_proj_b` outputs those two
vectors would act on are not stored, so no consumer can apply them. `hidden`
feeds only the `in_proj_*` GEMMs, whose weights are absent (the fixture is
captured one stage later, at the pre-conv `mixed_qkv`), and `verify()` read it
for a shape it never used. The 20 surviving tensors are bit-identical
(torch.equal, same dtype/shape) to the previous fixture and the metadata JSON is
unchanged; `format_version` stays 2 and a regen is byte-identical.

The reload pass stays: it re-runs the reference's own `causal_conv1d_fn` /
`torch_chunk_gated_delta_rule` / gated norm on the stored inputs rather than
re-implementing the math, so it still pins the stored weight set and the input
naming.

Signed-off-by: bohaotu <bohaotu@pony.ai>
The dump re-checked properties the GDN architecture already guarantees:
`norm.activation == "sigmoid"`, the recorded activations reproducing the
captured gated-norm output, `out_proj(captured) == module output`, and the fp32
chunk/recurrent agreement -- then reloaded the fixture to reproduce every stored
tensor. The fixture stores the reference's own output at the real geometry there
is nothing to re-check, so delete every assert and the code that existed only to
serve them: the `rederive_out()` re-derivation helper, `build_norm()`, the whole
`verify()` pass and its `--verify` flag, `FP32_REC_TOL`, capture's unused
`output` return entry, and the dead imports (`safe_open`, `causal_conv1d_fn`,
`torch_chunk_gated_delta_rule`, `torch_recurrent_gated_delta_rule`,
`Qwen4ExpTextRMSNormGated`). `split_qkv()` and `capture()` stay -- they are on
the data path.

The 20-tensor fixture is byte-identical and the metadata JSON is unchanged;
`--out` still writes it and `format_version` stays 2.

Signed-off-by: bohaotu <bohaotu@pony.ai>
The reference's chunk rule uses `chunk_size=64` and carries its recurrent state
from one chunk into the next, but both existing cases ran exactly one chunk
(S=16 pads to 64, S=64 is 64), so the carried-state path had no golden. Add S=80
(pads to two chunks, the second 16 real rows + 48 pad) and S=128 (two full
chunks). Traced through the reference's chunk loop (modeling_qwen4_exp.py:327):
S=16/64 run 1 chunk, S=80/128 run 2.

The two existing cases and the shared weights are bit-identical; the fixture
grows by the new `case2_*`/`case3_*` tensors only (20 -> 38 tensors, 5.10 -> 18.16
MiB) and the metadata JSON is unchanged.

Signed-off-by: bohaotu <bohaotu@pony.ai>
…iven stages

Split `gated_delta_rule_prefill_chunkwise_into` into a prepare stage
(head-expand q/k, L2-norm, split v, derive the raw g/beta pair) and a
`from_gates` stage (the chunk math over a scratch that already carries the
gate pair). The production entry point still runs prepare then the chunk
stages in the same order over the same kernels, so serving behaviour is
unchanged; the split lets a caller that already holds a recorded g/beta drive
the chunk stages directly. Widen `conv1d_prefill_batch_into`, the two stages,
and the scratch's `g_cumsum`/`beta` fields from `pub(crate)` to `pub`
(re-exported via `runtime_ops`) so an out-of-crate gate can inject the gates.

Signed-off-by: bohaotu <bohaotu@pony.ai>
Add `qwen38fn_gdn_prefill_parity`, a consumer for the
`test_data/qwen38fn-gdn-golden.safetensors` golden. It replays the GDN prefill
stack (causal conv1d -> chunk-wise gated delta rule via the Triton AOT kernels
-> gated RMSNorm with the sigmoid activation this checkpoint's
`output_gate_type` selects) over cases S=16/64/80/128 and compares the recorded
gated-norm output. S=80/128 cross the 64-token chunk boundary, so the carried
recurrent state is covered. Provenance is asserted on every load, never skipped
(metadata key parses, format_version 2, config_sha256 matches
`pegainfer-qwen4/tests/frozen_config.json`). Three negative controls assert the
comparison is sensitive: silu instead of sigmoid, a skipped conv leg, and
S=128 replayed as two independent single-chunk runs with the state carry
dropped. The test lives in `pegainfer-qwen35/tests/` because the chunk-wise
prefill driver is only exposed inside that crate.

Signed-off-by: bohaotu <bohaotu@pony.ai>
main moved GDN_AOT_KEY_HEAD_DIM / GDN_AOT_VALUE_HEAD_DIM out of the
qwen35 config module when the decode wrappers moved to pegainfer-core;
the split prefill stages still name them, so import them from
pegainfer_kernels::ops like the rest of the crate now does.

Signed-off-by: bohaotu <bohaotu@pony.ai>
@sysuTobo
sysuTobo force-pushed the feat/qwen38fn-gdn-golden branch from 7d36eef to ba9fb48 Compare October 21, 2026 03:02
@github-actions github-actions Bot removed the needs-rebase Conflicts with main, needs a rebase label Oct 21, 2026
@sysuTobo

Copy link
Copy Markdown
Contributor Author

Rebase

已 rebase 到 main @ 1f17ddb3(含 #1159 GDN decode 包装上提、#1160、#1176)。

冲突只有两处,均为 main 搬动后的适配,语义未改:

  • pegainfer-qwen35/src/ops.rs:conv1d_decode_batch_into / gated_delta_rule_decode_batch_into 已随 feat(qwen4): share the Qwen3.5 GDN decode wrappers and prove their reuse #1159 由 kernels 再导出,保留 main 的版本;本 PR 新增的两个 prefill 入口继续从 recurrent 导出(conv1d_prefill_batch_into 保持 pub,runtime_ops 依赖它)
  • recurrent.rs:GDN_AOT_KEY_HEAD_DIM / GDN_AOT_VALUE_HEAD_DIM 已不在 crate::config,改从 pegainfer_kernels::ops 取(单独一个 fix(qwen35) commit)

重跑:gate 5 passed;cargo test -p pegainfer-qwen35 --features qwen35 --lib 112 passed(含此前在本机 SIGSEGV 的 chunkwise prefill 对照测试——根因是 Triton 捆绑的 ptxas 13.1 与驱动 12.9 不匹配,设 TRITON_PTXAS_BLACKWELL_PATH=/usr/local/cuda-12.9/bin/ptxas 后通过)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request qwen35 Qwen3.5 model crate (pegainfer-qwen35)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant