Repository navigation
Conversation
Code review范围:2 文件全覆盖 ——
最小加固(均不改 RNG/计算,fixture 逐字节不变,无需替换 committed 文件):
|
修改措施Pushed
复验:fixture 未替换(三处改动全部 fixture-inert,无 RNG/计算变更),仍 18 张量;provenance = |
Code reviewTL;DR:正向数据流全部符合预期——
修改措施
|
|
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>
7d36eef to
ba9fb48
Compare
Rebase已 rebase 到 冲突只有两处,均为 main 搬动后的适配,语义未改:
重跑:gate 5 passed; |
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 dumpsQwen4ExpTextGatedDeltaNet(transformers 5.16.1qwen4_exp) at the real checkpoint geometry, including theoutput_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.in_proj_qkv/in_proj_z/in_proj_a/in_proj_bandout_projare NOT stored (~110 MiB combined, over GitHub's 100 MiB push limit) — their outputs are recorded instead, andout_projis dropped because the golden's expected output is the gated-norm output before it. Onlyconv1d.weightandnorm.weightare stored: the two weights a GDN kernel applies to the recorded activations.A_log/dt_biasare not stored (nor is the layer inputhidden): the fixture recordsg/betain their post-derivation form, so those two vectors have no storeda/bprojection output to act on — a gate that starts from the rawa/bprojections 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.
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 andformat_versionstay 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.rsreads the fixture and replays the GDN prefill stack against the recordedout.What reads the fixture. Per case the gate replays the in-tree GDN prefill stack: Qwen3.5's causal
conv1dprefill kernel → the chunk-wise gated delta rule (theqwen35Triton AOT kernels) → the shared gated RMSNorm with the sigmoid activation this checkpoint'soutput_gate_typeselects, and compares the result against the recordedout(the sublayer output beforeout_proj). The fixture records the gate pairg/betaafter derivation (thea/bprojections,dt_bias,A_logthat 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 apegainfer-qwen4or standalone crate because the chunk-wise prefill driver and its scratch (GdrChunkwiseScratch35) are only exposed insidepegainfer-qwen35(pegainfer-kernelsexposes 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_intois split into a prepare stage (head-expand q/k, L2-norm, split v, derive the rawg/beta) and afrom_gatesstage (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_intoand the two stages) and the scratch'sg_cumsum/betafields widen frompub(crate)topub(re-exported viaruntime_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.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 drivechunkwise_into's derivation end-to-end.Commands + measured numbers.
On the RTX 5060 (sm_120) host, all 5 tests pass:
Tolerance derivation. The bound is
abs(got-want) <= ATOL + RTOL*abs(want)withATOL=1.5e-1,RTOL=2.5e-2. The recurrent state is f32 but the storedoutis 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); atwant=0the 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) — its1e-3bound 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):output_gate_typetrap of this checkpoint): worst element 88–130× past bound, 24.8–25.8% of elements out.What remains ungated.
--features qwen35gates for this fixture yet — the gate is run manually viadocker/dev.sh.a/b→g/betaderivation (the fixture stores post-derivation gates).Refs #1103