Repository navigation
Conversation
Code review范围:2 文件、无运行时数据流(dump 脚本 + fixture),逐文件认领: 突变测试(把参考实现性质改坏,看断言红不红;A/C/D 突变后 fixture 输出实测已改变,但脚本全绿放行):
最小加固:修 H1(扰动点改到 |
修改措施Pushed
复验:fixture 重生成于 torch 2.14.1+cpu / transformers 5.16.1 / CPU——29 张量(3.08 MiB),provenance = 5.16.1 / 家族级备注(不改兄弟 PR):#1150 的 |
Address the family-level finding from the pegainfer-project#1153/pegainfer-project#1155 reviews: RMS_EPS feeds Config.rms_norm_eps but was missing from the config_digest canon, so editing eps changed the golden while the stored provenance stayed green. Canon now carries eps like the sibling scripts. Fixture regenerated (66 tensors): config_sha256 b9fd359a -> 20cb29e2; 59 tensors bitwise stable; the six case*_scores carry bf16 ULP-level drift (<=4.8e-4) from re-dumping under torch 2.14.1+cpu / transformers 5.16.1 instead of the original torch 2.5.1+cu121 environment. Second same-environment run is byte-identical; all six cases pass mask_ok and the reload assertions. Signed-off-by: bohaotu <bohaotu@pony.ai>
* feat(accuracy): add Qwen3.8-Flash-Next QSA indexer golden Dump the QSA token indexer reference (transformers 5.16.1 `qwen4_exp`) into a fixture the Stage 1 kernel gate replays: per-block scores + selected token indices for 6 cases, covering short/medium inputs, the budget-boundary truncation (block_topk reached with a shrunk budget) and the always-kept tail. The re-derivation is validated against the reference module's own mask in-dump. Signed-off-by: bohaotu <bohaotu@pony.ai> * fix(accuracy): stamp QSA indexer golden provenance + explicit score validity Address PR review: add `format_version` / `config_sha256` / `ref_transformers` so a consumer can assert the oracle's origin, and an explicit `scores_valid` mask so the NaN-padded block scores cannot be mis-consumed (no silent nan_to_num pass). L1 (real-budget truncation) kept as a known bound. Signed-off-by: bohaotu <bohaotu@pony.ai> * docs(accuracy): trim QSA indexer golden dump comments Drop references to local-only planning docs and tighten the docstring/comments. No behavior change; fixture is unaffected. Signed-off-by: bohaotu <bohaotu@pony.ai> * fix(accuracy): include norm eps in the QSA indexer golden config digest Address the family-level finding from the #1153/#1155 reviews: RMS_EPS feeds Config.rms_norm_eps but was missing from the config_digest canon, so editing eps changed the golden while the stored provenance stayed green. Canon now carries eps like the sibling scripts. Fixture regenerated (66 tensors): config_sha256 b9fd359a -> 20cb29e2; 59 tensors bitwise stable; the six case*_scores carry bf16 ULP-level drift (<=4.8e-4) from re-dumping under torch 2.14.1+cpu / transformers 5.16.1 instead of the original torch 2.5.1+cu121 environment. Second same-environment run is byte-identical; all six cases pass mask_ok and the reload assertions. Signed-off-by: bohaotu <bohaotu@pony.ai> * fix(accuracy): record the QSA indexer golden from the reference itself Signed-off-by: Feathbow <feathbow@gmail.com> * fix(accuracy): cover the real QSA budget boundary in the indexer golden Signed-off-by: Feathbow <feathbow@gmail.com> --------- Signed-off-by: bohaotu <bohaotu@pony.ai> Signed-off-by: Feathbow <feathbow@gmail.com> Co-authored-by: Feathbow <feathbow@gmail.com>
The golden recorded in pegainfer-project#1155 had no reader, so it proved nothing on its own: issue pegainfer-project#1103's rule is that a fixture lands with the code that checks it. Add that consumer as a device-free integration test, keeping this crate's no-CUDA property so it runs with no device, no weights and no Python. Two halves, matching the two things a PLE port can get wrong: - The mul-xor n-gram hash is recomputed from `input_ids` and compared with i64 equality, zero tolerance: the EOS-resetting context window, the bigram/ trigram head layout (16 heads = 2 orders x 8) and the per-head prime modulus and table offset all have to be right for one id to land. 128/128 and 192/192 ids exact on the S=8 and S=12 cases. - The block is replayed from the recorded `key`/`value` projections and is bit-identical on all 81920 + 122880 stored bf16 outputs, because the port emulates each bf16 rounding point of the reference chain - including the gate's double rounding, where the group sum is rounded before the 1/sqrt(hidden) divide and again after it. `OUT_MAX_ABS` is sized from measurement, not guessed: the residual is 0.0, and injecting a whole bf16 ulp into every `sigmoid` and `silu` output - the chain's only libm-dependent op, far more than any platform disagreement - moves `out` by at most 7.8e-3. 1.6e-2 is 2x that and still 17x below the weakest negative control. Four controls are each asserted to fail: dropping the EOS window reset (moves only the 8 trigram heads of the row after the EOS, and leaves the EOS-free case untouched), dilation 1 (0.79), unsigned sqrt (0.27) and a swapped bigram/trigram head order (every id). Provenance is checked on every load and never skipped: the manifest must parse, carry format_version 2, and name the frozen config with the sha256 that file hashes to. The config's eos_token_id and every tensor width are cross-checked against the stored scalars and shapes, and `caseN_rows` must be the identity - this replay is whole-sequence, so a row selection would fail loudly rather than replay against the wrong conv state. Still ungated, both landing with the device side in pegainfer-project#1106: the n-gram table gather that turns `ngram_ids` into head vectors, and the GPU dilated conv. Signed-off-by: bohaotu <bohaotu@pony.ai>
The bring-up doc's TL;DR and "What landed" still described the crate as an identity probe plus one config validation, and never mentioned the PLE golden pegainfer-project#1155 records or the gate that now reads it - so a reader could not tell that the block's hash and arithmetic are already pinned on the host, or what the fixture deliberately leaves out. Add the landed bullet with the two boundaries the fixture cannot carry (the 102.4 GB table is not stored, so the replay starts at the block's post-projection inputs; the recording is whole-sequence, so the decode-side states stay ungated), the note that the hash reads `layer_multipliers` rather than recomputing them - which is what keeps `seed` unmodelled - and the CI coverage that makes the no-device property enforceable. Refresh the TL;DR and the index row to match. Signed-off-by: bohaotu <bohaotu@pony.ai>
|
This pull request has merge conflicts with main. Please rebase it so it can be reviewed and merged. |
Code review自审对象:本次追加到 #1155 的三个 commit(
已逐项核对为符合预期的部分(附取证方式,均为直接搜索/实测,非外推):
修改措施已修(9 条)
保留(3 条,附理由)
复跑回执(修改后,非改前数字) |
Branch state note (after the three commits above)
Fixing that means rebasing this branch, which needs a force-push — deliberately not done here. Two things that make the rebase safe: |
The golden recorded in pegainfer-project#1155 had no reader, so it proved nothing on its own: issue pegainfer-project#1103's rule is that a fixture lands with the code that checks it. Add that consumer as a device-free integration test, keeping this crate's no-CUDA property so it runs with no device, no weights and no Python. Two halves, matching the two things a PLE port can get wrong: - The mul-xor n-gram hash is recomputed from `input_ids` and compared with i64 equality, zero tolerance: the EOS-resetting context window, the bigram/ trigram head layout (16 heads = 2 orders x 8) and the per-head prime modulus and table offset all have to be right for one id to land. 128/128 and 192/192 ids exact on the S=8 and S=12 cases. - The block is replayed from the recorded `key`/`value` projections and is bit-identical on all 81920 + 122880 stored bf16 outputs, because the port emulates each bf16 rounding point of the reference chain - including the gate's double rounding, where the group sum is rounded before the 1/sqrt(hidden) divide and again after it. `OUT_MAX_ABS` is sized from measurement, not guessed: the residual is 0.0, and injecting a whole bf16 ulp into every `sigmoid` and `silu` output - the chain's only libm-dependent op, far more than any platform disagreement - moves `out` by at most 7.8e-3. 1.6e-2 is 2x that and still 17x below the weakest negative control. Four controls are each asserted to fail: dropping the EOS window reset (moves only the 8 trigram heads of the row after the EOS, and leaves the EOS-free case untouched), dilation 1 (0.79), unsigned sqrt (0.27) and a swapped bigram/trigram head order (every id). Provenance is checked on every load and never skipped: the manifest must parse, carry format_version 2, and name the frozen config with the sha256 that file hashes to. The config's eos_token_id and every tensor width are cross-checked against the stored scalars and shapes, and `caseN_rows` must be the identity - this replay is whole-sequence, so a row selection would fail loudly rather than replay against the wrong conv state. Still ungated, both landing with the device side in pegainfer-project#1106: the n-gram table gather that turns `ngram_ids` into head vectors, and the GPU dilated conv. Signed-off-by: bohaotu <bohaotu@pony.ai>
e53a83e to
77d78ca
Compare
Code review自审对象:#1155 追加的三个 device-path commit( 门禁重跑(本轮实测,非引用旧数字):
第一轮审计(A 系列,均已在
|
| ID | 级别 | 一句话 | 证据(文件:行) |
|---|---|---|---|
| A1 | L | NgramHasher 带着四个无调用者的 accessor 出厂——gather 要的是 ids,不是被 hash 的向量——新生产模块上的死 API 面 |
pegainfer-qwen4/src/ngram.rs(f1ebd3d8 删除;本轮 grep 逐个确认剩余 accessor 均有调用者,含 shifted_context 由 windows 内部调用) |
| A2 | M | table_head_of 原按 slot 而非 order 取参,换头序 control 只能自行重推布局——control 可能漂移到私有布局副本上仍然绿 |
pegainfer-qwen4/src/ngram.rs:194-196(现签名为 (order, head));pegainfer-qwen4/tests/ple_ngram_replay.rs:492-500(换序 control 直接调生产 order_of_slot / table_head_of) |
| A3 | M | 拒绝矩阵每个入口各有一个洞:gate 从不拒绝错误 branch count,conv 从不拒绝错误 group 或 branch count——编译常量守卫零覆盖 | pegainfer-kernels/tests/qwen38fn_ple_block_parity.rs:672-688(gate branch count)、765-780(conv group)、782-797(conv branch count),现全部拒绝并断言 CUDA_ERROR_INVALID_VALUE |
| A4 | M | 输出 sentinel 先于 finiteness 比较:kernel 自己产出的 NaN 会被误报成"未写入元素",掩盖真实故障形态 | pegainfer-kernels/tests/qwen38fn_ple_block_parity.rs:434-447(finiteness 先行,报错信息区分 sentinel 与 kernel 产 NaN 两种形态) |
| A5 | L | kernel 头注释与代码不符:block 有三个 grouped norm 却写 "both grouped norms";把 ple_block_into 写成唯一相关入口,而 variant 入口同样是公开的 |
pegainfer-kernels/csrc/qwen38fn/qwen38fn_ple_block.cu:36-42(现为 "All three grouped norms … twelve launches")、53-56(现点名 ops::ple_block_variant_into 且 "sole callers are the gate's three controls",与 ops.rs / gate 的实际调用面一致,本轮 grep 证实) |
| A6 | L | 惰性覆盖未记录:gate 的 clamp_min 地板与 sign(0) 分支在两 case 的最小 |pre_gate|(0.0232)下不可触发,错误地板常量也会通过;seq_len 1 无对照;ops 层 extent 守卫需 device 才可达 |
pegainfer-kernels/tests/qwen38fn_ple_block_parity.rs:34-51("What this fixture cannot reach",记录而非掩饰) |
复核 pass(B 系列,均为确认项,未改代码)
| ID | 级别 | 一句话 | 证据(文件:行) |
|---|---|---|---|
| B1 | ✓ | f1ebd3d8 声称的每项修复在 diff 中逐条为真(见 A 系列各行证据);NgramHasher 无残留死 accessor |
A1–A6 各行;pegainfer-qwen4/src/ngram.rs:153-196 |
| B2 | ✓ | 容差论证成立:1.6e-2 = 最大存储元素 0.8438 处 4 个 bf16 ulp(2^-8 = 3.9e-3),覆盖三个非可移植点——rsqrt.approx.ftz.f32(bring-up 期插桩实测 245760 个 norm 输出中恰好 2 个翻转,各为自身 1 ulp,gated 流位相同)、归约树差异(f32 butterfly vs 参考 f32 树 vs host f64 精确和,相距一两个 f32 ulp,低于 bf16 步长四个数量级)、expf(host 侧已按每个 sigmoid/silu 注入整 ulp 预算,≤7.8e-3);与 host gate 同数,两半读同一把尺;且写明了为何不能向实测 1.2e-4 收紧——翻转集合由架构/libm 决定,是离散随机变量而非常量 |
pegainfer-kernels/tests/qwen38fn_ple_block_parity.rs:78-126;pegainfer-qwen4/tests/ple_ngram_replay.rs:87(同为 1.6e-2) |
| B3 | ✓ | control 非空转:拒绝矩阵以每入口的零 extent 正对照开头(必须放行且不形成 launch),"拒绝一切"无法让下游拒绝断言变绿;三个负 control 断言每个 case 都出界而非只一个;host 侧 hash control 组合在参考设置下被断言与生产 ids 逐元素相等,control 无法带着私有 hash 副本通过 |
pegainfer-kernels/tests/qwen38fn_ple_block_parity.rs:566-571, 592-634, 1042-1049;pegainfer-qwen4/tests/ple_ngram_replay.rs:665-673 |
| B4 | ✓ | provenance 断言 panic 而非 skip:assert_provenance 全部 assert/expect,由 device-free 测试 fixture_records_the_frozen_reference 不经 device_or_skip 直接调用;device 测试在任何比较前先过 provenance;device_or_skip 仅在确无设备时跳过,且 PEGAINFER_REQUIRE_GPU=1 会把跳过变成失败——"另一 checkpoint 的 fixture 在这里失败而不是被跳过"为真 |
pegainfer-kernels/tests/qwen38fn_ple_block_parity.rs:160-243, 535-562, 949-956;pegainfer-kernels/tests/common/mod.rs:12-36 |
| B5 | ✓ | .qoder 未跟踪卫生:git diff --name-only 77d78cae..HEAD 共 14 个文件,无一为 .qoder 路径;.qoder/ 保持 untracked(未被 ignore),后续提交必须逐文件 add、绝不 git add -A。红线扫描该范围仅一处误报:kGateClampMin 标识符恰好包含子串 "mpm",与任何内部工具无关 |
pegainfer-kernels/csrc/qwen38fn/qwen38fn_ple_block.cu:72;git diff --name-only 77d78cae..HEAD |
修改措施
- A1–A6:已修,全部落在
f1ebd3d8;本轮复核逐条对照 diff 证实为真(行号见上表),无新 commit。 - B1–B4:确认项,无需修改。B5 的红线命中为标识符子串误报,就地更正该扫描结论,不改动任何提交内容。
- 明确留给 [Model] Qwen3.8-Flash-Next: n-gram embedding #1106:102.4 GB n-gram 表的 lookup + gather 本身,以及这么大一张表如何喂进 kernel 的 UVA / in-graph 问题——两个 gate 都从 block 的 post-projection 输入开始,表不在 fixture 里;decode 侧状态(2-token id 窗口 + 9-slot dilated conv 状态)——录制为整序列、无 cache 状态,同样留给 [Model] Qwen3.8-Flash-Next: n-gram embedding #1106 的 decode 接线。
- 明确留给 test(qwen4): pin the grouped (1+w) RMSNorm to a grouped weight slice #1158:grouped norm 的单 launch 入口。落地后生产切换过去,十二次 per-branch 调用收敛为三次、branch-major split 随之消失;在那个退化路径上按构造位相同(同一段算术,仅 launch 形状变化)。
- 明确留给"把 kernels
qwen4feature 接进 CI 的人":当前没有任何 CI job 启用该 feature,device gate 只在其头注的手动命令下运行(device-free 一半已在 CPU CI job 里);A6 第三条的 ops 层 extent 守卫需要 device + 故意短 buffer 才可达,随那个 job 一起补。
|
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>
…overage Review follow-up for the PLE golden dump: - EOS reset property assert was vacuous: it perturbed token 0, whose 3-token window radius never reaches the asserted post-EOS region (positions >= eos_pos+1 = 6), so removing the segment reset from the reference still passed. Perturb the token right before each EOS (inside the post-EOS window radius) and add a positive control asserting the perturbation moves the pre-EOS embeddings. - Re-derive the mul-xor n-gram hash in plain Python (EOS-reset window, per-order mix, per-head remainder+offset) and assert it reproduces the module embedding bit-for-bit; a xor->add reference mutation previously sailed through. - Pin the dilated conv: assert conv1d.dilation == ngram_size and short_conv_state_len == (kernel-1)*dilation; a consistent dilation-1 reference mutation previously sailed through. - config_digest now covers seed / eos_token_id / rms_norm_eps (all output-affecting constants); store eos_token_id in the fixture. - Add a double-EOS case (14, eos at 4 and 9) covering the second reset boundary; drop the unused Config.layer_types; document the cache=None scope boundary. Fixture regenerated (29 tensors, config_sha256=5acc59a2...) with torch 2.14.1+cpu / transformers 5.16.1; re-run is byte-stable. Signed-off-by: bohaotu <bohaotu@pony.ai>
Read the frozen checkpoint config instead of a hand-written reduced one: hidden 2560, hc_count 4 (10240-wide stream), ple_embed_dim 2560, ngram_size 3, 16 heads of 160. Capture the reference's own outputs with forward hooks (the real hashed n-gram ids, the key/value projections, the n-gram embedding and the block output) rather than recomputing them, and store the real per-head primes/offsets/vocab sizes and layer multipliers. The 95 GiB n-gram table is stubbed so the module builds; the hash math never touches it, so the captured ids stay real. key_proj/value_proj weights are not stored - their outputs are recorded. The dump asserts the captured ids equal an independent hash re-derivation and that the captured projections reproduce the block output, and asserts the EOS segment reset with a positive control. Signed-off-by: bohaotu <bohaotu@pony.ai>
case*_key_proj/value_proj held the captured projection outputs, not weights; rename them key/value (matching the GDN golden) so they are not read as the weights the golden deliberately does not store. Signed-off-by: bohaotu <bohaotu@pony.ai>
`verify()` reloaded the fixture and reproduced the hashed ids and the block output by re-implementing the reference math by hand (`grouped_norm` / `short_conv`, plus a second copy of the mul-xor hash), purely to show the fixture reproduces itself. Every stored value is captured from the reference module through forward hooks, so the oracle never depended on that pass: the fixture is byte-identical without it (26 tensors, same metadata). `safe_open` (that pass's only reader) goes with it. No tensor is dropped. The n-gram block is table/embedding-driven: the hash buffers (`layer_multipliers`, `ngram_heads_offsets`/`_vocab_sizes`, `eos_token_id`) are the lookup a consumer must perform to re-derive the ids, and `case*_ngram_emb` is the reference's own recorded n-gram-embedding activation for the stored ids -- the fixture's only record of the embedding stage, since the 95 GiB table cannot be committed. The in-run invariants stay, since they guard the oracle's semantics: the captured ids equal an independent plain-Python mul-xor re-derivation, the captured key/value projections reproduce the module's returned block output bit-for-bit, the EOS segment reset with its positive control, and the dilation / short-conv-state-length pins. Signed-off-by: bohaotu <bohaotu@pony.ai>
…tion `case*_ngram_emb` was the n-gram lookup into the capped stub table, whose weights are not in the fixture, so no consumer could reproduce it; the projection outputs it feeds (`key`/`value`) are stored instead. The verifiable part of the lookup -- the real mul-xor hash output -- is stored as `ngram_ids` and cross-checked by an independent re-derivation. The activation stays in-run, where it asserts the EOS segment reset. The 24 surviving tensors are bit-identical and the metadata JSON is unchanged. Signed-off-by: bohaotu <bohaotu@pony.ai>
The dump stored the reference's own output at the real geometry; every assert re-checked a property the model architecture already guarantees (hash re-derivation, block re-derivation, EOS segment reset, dilated-conv pins) or merely tested the capture. Delete the asserts and the code that existed only to serve them (rederive_ngram_ids, block_forward and its grouped_norm/short_conv helpers, _perturb, the structural pins, the dead math/F imports and the now-unused eps local, plus the dead embedding hook), so the dumper is a pure recorder. The fixture tensor set is unchanged and regenerates byte-for-byte. Signed-off-by: bohaotu <bohaotu@pony.ai>
The golden recorded in pegainfer-project#1155 had no reader, so it proved nothing on its own: issue pegainfer-project#1103's rule is that a fixture lands with the code that checks it. Add that consumer as a device-free integration test, keeping this crate's no-CUDA property so it runs with no device, no weights and no Python. Two halves, matching the two things a PLE port can get wrong: - The mul-xor n-gram hash is recomputed from `input_ids` and compared with i64 equality, zero tolerance: the EOS-resetting context window, the bigram/ trigram head layout (16 heads = 2 orders x 8) and the per-head prime modulus and table offset all have to be right for one id to land. 128/128 and 192/192 ids exact on the S=8 and S=12 cases. - The block is replayed from the recorded `key`/`value` projections and is bit-identical on all 81920 + 122880 stored bf16 outputs, because the port emulates each bf16 rounding point of the reference chain - including the gate's double rounding, where the group sum is rounded before the 1/sqrt(hidden) divide and again after it. `OUT_MAX_ABS` is sized from measurement, not guessed: the residual is 0.0, and injecting a whole bf16 ulp into every `sigmoid` and `silu` output - the chain's only libm-dependent op, far more than any platform disagreement - moves `out` by at most 7.8e-3. 1.6e-2 is 2x that and still 17x below the weakest negative control. Four controls are each asserted to fail: dropping the EOS window reset (moves only the 8 trigram heads of the row after the EOS, and leaves the EOS-free case untouched), dilation 1 (0.79), unsigned sqrt (0.27) and a swapped bigram/trigram head order (every id). Provenance is checked on every load and never skipped: the manifest must parse, carry format_version 2, and name the frozen config with the sha256 that file hashes to. The config's eos_token_id and every tensor width are cross-checked against the stored scalars and shapes, and `caseN_rows` must be the identity - this replay is whole-sequence, so a row selection would fail loudly rather than replay against the wrong conv state. Still ungated, both landing with the device side in pegainfer-project#1106: the n-gram table gather that turns `ngram_ids` into head vectors, and the GPU dilated conv. Signed-off-by: bohaotu <bohaotu@pony.ai>
`pegainfer-qwen4` was in no CI job: the CPU clippy list covers build/frontend/ sim/kvbm-logical, and the CUDA matrix lists the model packages that need a device. So the crate's 40 config/probe unit tests and the new PLE golden replay ran only on a developer box - a gate nobody runs is not evidence. The crate carries no CUDA dependency by design, so it belongs with the CPU jobs: add it to `cpu-clippy` (--all-targets -D warnings, pedantic) and run `-p pegainfer-qwen4 --tests` in `cpu-unit-tests`, which picks up the unit tests and the golden replay together. Both commands verified locally as written. Signed-off-by: bohaotu <bohaotu@pony.ai>
The bring-up doc's TL;DR and "What landed" still described the crate as an identity probe plus one config validation, and never mentioned the PLE golden the block's hash and arithmetic are already pinned on the host, or what the fixture deliberately leaves out. Add the landed bullet with the two boundaries the fixture cannot carry (the 102.4 GB table is not stored, so the replay starts at the block's post-projection inputs; the recording is whole-sequence, so the decode-side states stay ungated), the note that the hash reads `layer_multipliers` rather than recomputing them - which is what keeps `seed` unmodelled - and the CI coverage that makes the no-device property enforceable. Refresh the TL;DR and the index row to match. Signed-off-by: bohaotu <bohaotu@pony.ai>
The hash that addresses the 95 GiB n-gram table was test-local, so the exact-i64 golden gate pinned a copy rather than the code a loader will call. `pegainfer_qwen4::ngram::NgramHasher` now owns it: the EOS-padded history, the cummax segment reset as data the decode path can carry, the mul-xor fold, the per-head prime remainder plus offset, and the order-major head layout. The gate calls it directly for the zero-tolerance comparison. Its two hash controls compose the same primitives with one knob each — a segment start of 0 everywhere, and the order blocks walked in reverse — and the exact test now asserts that composition reproduces `ids` at the reference settings, so a control cannot drift onto its own copy of the hash and still pass. The crate stays CUDA-free: this is integer host math, and the device gather that consumes it is what needs a GPU. Signed-off-by: bohaotu <bohaotu@pony.ai>
The PLE golden had a host reader and no device consumer, so the block's arithmetic was pinned only by a CPU oracle. Three kernels in `csrc/qwen4/qwen4_ple_block.cu` now compute it behind the line's additive `qwen4` feature on `pegainfer-kernels`: the branch-major split that lets the grouped norms run on contiguous groups, the per-branch gate, and the dilated causal depthwise conv with its residual add. Every bf16 rounding point of the reference is reproduced — the gate's double rounding around the `1 / sqrt(hidden_size)` divide, the signed sqrt's `clamp_min` floor and `torch.sign` at zero, the conv's rounding before the silu — and the three grouped `(1 + w)` norms are twelve calls to the existing batched entry point rather than a new kernel, because that entry point's row stride is its `hidden_dim` and this stream's four groups are interleaved row-major. The gate drives `ops::ple_block_into` and measures 204799 of the golden's 204800 stored outputs bit-for-bit, the remaining one a single bf16 ulp, against a bound of 1.6e-2 — the host gate's own number, so both halves of this consumer read against one scale. Three device-side controls (unsigned sqrt, a full-stream gate dot, dilation 1) are each asserted to leave that bound, at 21x, 17x and 49x outside it, and they turn one scalar of the same launches rather than running a second copy of the kernels. Provenance and the entry points' refusal contract are the two device-free halves, so neither can skip. Signed-off-by: bohaotu <bohaotu@pony.ai>
f1ebd3d to
89a2092
Compare
`NgramHasher` shipped four accessors with no caller — the gather needs the ids, not the vectors they were hashed from — so they are gone, and `table_head_of` now takes the order rather than the slot, which lets the swapped-head-order control call it instead of re-deriving the layout. The parity gate's refusal matrix had a hole per entry point: the gate never refused a wrong branch count and the conv never refused a wrong group or branch count, so two of the six compiled-constant guards were unexercised. It also compared the output sentinel before finiteness, which would have reported a NaN the kernels produced as an unwritten element, and it now records what the fixture cannot reach — the gate's `clamp_min` floor and `sign(0)` branch are inert at the smallest `|pre_gate|` either case produces (0.0232), `seq_len` 1 is compared against nothing, and the ops layer's extent guards need a device. The kernel header said "both grouped norms" where the block has three, and named `ple_block_into` the only caller where the variant entry is public too. Signed-off-by: bohaotu <bohaotu@pony.ai>
89a2092 to
cc346a4
Compare
Rebase已 rebase 到
|
Summary
Reference golden for the PLE n-gram block of Qwen3.8-Flash-Next (#1103).
tools/accuracy/dump_qwen38fn_ple_golden.py— dumpsQwen4ExpTextPLELayer+Qwen4ExpTextNGramEmbedding(transformers 5.16.1qwen4_exp): the real hashed n-gram ids, the projection activations, and the injected block output (grouped(1+w)norms, signed-sqrt gate, dilated conv).test_data/qwen38fn-ple-golden.safetensors— 2 cases: no EOS, and a mid-sequence EOS that exercises the segment reset. 24 tensors / 1.41 MiB;eos_token_idis stored so consumers need not hardcode it, and metadata is a single JSON key (format_version2).hc_count4 (10240-wide stream),ple_embed_dim2560,ngram_size3, 16 heads × 160, and the real per-head prime vocabularies/offsets/multipliers. Moved boundaries: (a) the 95 GiB n-gram table is stubbed (not storable), so only the hash→id mapping is real; the embedding activation is a lookup into that stub table and is not stored (the projection outputs it feeds are); (b) thekey_proj[10240,2560] (~52 MB) andvalue_proj[2560,2560] (~13 MB) weights are not stored — their outputs are recorded; (c) every other shape is real (there is no final GEMM).Consumer
pegainfer-qwen4/tests/ple_ngram_replay.rsreads the fixture: a device-free parity gate in a crate with no CUDA dependency, so it needs no device, no weights and no Python. It runs in CI —cpu-unit-testsnow runs-p pegainfer-qwen4 --testsandcpu-clippycovers the crate with--all-targets -D warnings— which is what makes "the golden has a reader" enforceable rather than aspirational.What it proves. Two ports of
modeling_qwen4_exp.py, both driven from stored inputs only:Qwen4ExpTextNGramEmbedding): the EOS-padded context window with the cummax segment reset, the per-positionlayer_multipliersXOR fold, and the per-headremainder(ngram_heads_vocab_sizes[h]) + ngram_heads_offsets[h], with heads laid out (order, head) — bigram block first, then trigram. Compared with i64 equality at zero tolerance: 320/320 ids exact. Integer math, so a deviation is a bug, not noise.Qwen4ExpTextPLELayer) from the recordedkey/valueprojections andhidden: grouped(1+w)RMSNorm over 4×2560,dot/sqrt(2560)gate, signed sqrt,sigmoid × valuebroadcast over the 4 streams,(1+w)norm, thensilu(causal dilated depthwise conv(kernel 4, dilation 3, 9-slot zero state))added back to the gated value. Bit-identical on every stored output element (81920/81920 and 122880/122880).Tolerance. The measured residual is 0.0, so
OUT_MAX_ABS = 1.6e-2is headroom for the one non-portable op in the chain,expf(insidesigmoid/silu);sqrt, division and multiplication are IEEE correctly-rounded and the f64 reductions are the test's own. Injecting a whole bf16 ulp into everysigmoidand everysiluoutput — far more than any libm disagreement — movesoutby at most 7.8e-3, i.e. 2 bf16 ulps at the largest stored |out| (0.8438). The bound is 2× that and still 17× below the weakest negative control. Reaching bit-exactness required emulating the gate's double rounding: the reference rounds the group sum to bf16 before the1/sqrt(hidden)divide and again after it — collapsing the two is worth ~1 ulp on every gate and 3.9e-3 onout.Negative controls (each its own test, each asserting the comparison fails):
ngram_sizeProvenance. Every load parses the
qwen4_ple_goldenmanifest, assertsformat_version == 2, and assertsconfig_sha256equals the sha256 ofpegainfer-qwen4/tests/frozen_config.json— whose own hashsrc/config.rs::the_committed_fixture_is_the_frozen_revisionpins to the frozen revision, so the chain is fixture → config → checkpoint. A missing fixture panics: no skip path, no env override. The loader also cross-checks the config'seos_token_idagainst the stored scalar and every tensor width against the config geometry, and requirescaseN_rowsto be the identity (this replay is whole-sequence).Still ungated on the host — the n-gram table lookup/gather that turns
ngram_idsinto 160-dim head vectors needs the 95 GiB table's loader (#1106). The GPU dilated depthwise conv now runs on the device (below). The decode-side states (the 2-token id window and the 9-slot conv state) stay out of scope for both halves, since the recording is whole-sequence only.Device path (commits
6115dd89→39cf30f0→f1ebd3d8)pegainfer_qwen4::ngram::NgramHasher(pegainfer-qwen4/src/ngram.rs) owns the EOS-padded history, the cummax segment reset (returned as data, so a resumed decode can carry it), the mul-xor fold, the per-head prime remainder + offset, and the order-major head layout. The exact-i64 gate calls it directly, so the zero-tolerance comparison pins production code rather than a test-local copy; the two hash controls compose the same primitives with one knob each and are asserted to reproduce the productionidsat the reference settings, so a control cannot drift onto a private copy of the hash and still pass. The crate stays CUDA-free — this is integer host math; the device gather that consumes it is what needs a GPU.pegainfer-kernels/csrc/qwen4/qwen4_ple_block.cu— the branch-major split, the per-branch gate (every bf16 rounding point of the reference: the double rounding around the1 / sqrt(hidden_size)divide, the signed sqrt'sclamp_minfloor andtorch.signat zero, the sigmoid), and the dilated causal depthwise conv with its residual add — behind the line'sqwen4feature onpegainfer-kernels(default builds are unchanged:cargo check -p pegainfer-kernelsis green and reports the feature disabled; the new.cuis not compiled). The gate drivesops::ple_block_into, the production entry;ops::ple_block_variant_into's only callers are the gate's three controls, which turn one scalar of the same launches rather than running a second copy of the kernels.(1+w)RMSNorms is 4 per-branch calls of the existingrms_norm_batched_offset_cuda(twelve launches total, plus the branch-major split in front of them), because that entry point's row stride is itshidden_dimand this stream's four groups are interleaved row-major. If/when test(qwen4): pin the grouped (1+w) RMSNorm to a grouped weight slice #1158's single-launch grouped entry lands, production switches to it; bit-identical on that degenerate path.Measured per case (sm_120, CUDA 12.9.1, re-run after the final commit): case 0 (S=8) is bit-identical on all 81920/81920 stored outputs, max_abs 0.000e0; case 1 (S=12) is bit-identical on 122879/122880, the single deviation 1.221e-4 — one bf16 ulp at that element's magnitude (0.024292 against 0.024414), 0.0076 of the bound.
Device tolerance. The bound is the same
1.6e-2as the host gate's, so both halves of this consumer read against one scale. Derivation: one flipped bf16 anywhere upstream reachesoutas at most about one bf16 ulp ofoutat that element (the gate is a single scalar per (row, branch); the conv, its silu — derivative bounded by ~1.1 — and the residual add cannot amplify further); one ulp at the largest stored element (0.8438) is 2^-8 = 3.9e-3, and the bound is four of those. It covers the three places the device path cannot be the host path:rsqrt.approx.ftz.f32in the existing batched norm (~1.4e-7 relative error flips the bf16 rounding of roughly one normed element in 30000 — an instrumented bring-up run found exactly two across case 0's three norms, 245760 outputs, each one bf16 ulp of itself and the gated stream bit-identical), a different reduction tree (f32 butterflies vs the reference's f32 tree vs the host's f64 exact sum — within an f32 ulp or two, four orders below the bf16 step the result rounds to), andexpf(already budgeted on the host as a whole bf16 ulp in every sigmoid/silu, worth ≤ 7.8e-3). It cannot be tightened towards the measured 1.2e-4:rsqrt.approx,expfand reduction order are architecture- and libm-defined, so another GPU flips a different set of elements, possibly including one near 0.8438 where a single ulp is 3.9e-3 — the flip count is a discrete random variable, not a constant.Device negative controls — each turns exactly one scalar of the same launches, and every case is asserted to leave the bound (a control that passed would mean the gate cannot see that fault):
ngram_sizeTwo more device-gate tests never skip and need no GPU: provenance — panics, not skips, so a fixture from another checkpoint or another semantics fails — and the entry points' refusal contract, with every compiled-constant guard pinned and zero-extent positive controls first so a refuse-everything entry point cannot make the refusal matrix vacuous.
What remains ungated. The 102.4 GB n-gram table lookup/gather itself — and the UVA / in-graph question of feeding a table that size to a kernel — is #1106; both gates start at the block's post-projection inputs. The decode-side states (the 2-token id window and the 9-slot dilated conv state) stay ungated because the recording is whole-sequence. No CI job enables the kernels'
qwen4feature yet: the device gate runs only under the manual command quoted above, while the device-free half (the hash gate and CPU replay inpegainfer-qwen4) is in the CPU CI jobs. Recorded as inert in the gate's header rather than papered over: the gate'sclamp_min(1e-6)floor andsign(0)branch are inert at the smallest|pre_gate|either case produces (0.0232),seq_len1 is compared against nothing, and the ops layer's extent guards need a device to reach.Branch state. The branch was rebased onto current main (base
77d78cae); the conflicts described in the earlier "Branch state note" comment are resolved — that comment predates the rebase. GitHub reports the PR MERGEABLE/CLEAN.Validation
The dump asserts nothing. Every stored value is the reference's own output at the real geometry — the hashed ids and the key/value projections captured through forward hooks, plus the module's own returned block output. There is nothing to re-check in the dumper: the fixture is the reference, and it is the consumer gate that compares a PLE kernel against it.
Regenerating the committed fixture is byte-identical.
Fixture + dump script, plus the golden's two readers: the device-free consumer gate (
pegainfer-qwen4/tests/ple_ngram_replay.rs) and its CI wiring, the production host hash module (pegainfer-qwen4/src/ngram.rs), the PLE block kernels/FFI/ops behindpegainfer-kernels' additiveqwen4feature with their gate (pegainfer-kernels/tests/qwen4_ple_block_parity.rs), and the bring-up doc rows. No serving path calls any of it yet — the line still refuses to launch.Refs #1103