Repository navigation
融合:精度签名降级为运行期参数 + nn_enable_gpu_fusion(叠在 #14 之上) - #16
Merged
Merged
Conversation
key 的语义收敛为"纯结构":常量池仍喂**个数**(决定 push constant 布局), 不再喂**值**。glsl_gen 本就把 consts 声明进 push constant、操作数发射成 `c<idx>`(ExprOperandKind::Const),值进 key 只会让同结构不同取值各编译 一份**逐字节相同**的 shader,并让"结构 = 表达式类型"这一目标不成立。 变更: - `include/neuralnet.cpp/expr_spec.hpp`:`expr_spec_key` 去掉 consts 值喂入 (只留个数);补 key 语义注释(值不进 key、个数进 key 的原因)。 - `tools/prec_backfill.txt`:迁移 fold 结构 key (`9396eb8a1f95352c` → `66b03ef936bb97c7`;其余 6 条 `consts==0` 故不变)。 - `docs/development/03-ir-optimization.md` §4.1:新增"key 字段归属表" (哪些字段进 key / 运行期载体是什么),并标出两个**待收敛**项 (matmul transA/transB、分组归约 R)。 - `docs/development/02-operator-fusion.md` §二期教训 1:更正为"值不进 key、 但个数进 key,且 canonicalize 按值去重 → 运行期标量塞常量池仍会让 key 漂移"。 验收(本机 clang + Vulkan dev2): - build 零告警(-Werror); - ctest 19/19; - CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变; - `expr_specs.bin` SHA256 与改动前**完全一致**(bin 存 spec 不存 key, 且本例无"仅常量值不同"的结构被合并);`fused_registry.hpp` 因 fold key 改名而变化(预期); - shader 数:77 结构 + 76 变体 = 153,与改动前相同 (现有表达式常量池基本为空——运行期标量早已走 rparam,故本步**立即收益为 0**, 价值在语义收敛与后续步骤的前提)。
`MatmulSpec.transA/transB` 原先按"结构"进 key,同一表达式的 4 种转置组合各占
一份 shader;而 DSL 的 `dsl::matmul(A,B,transA,transB)` 在**类型层面看不出转置
差异**(四组合同类型)——这是"结构 = 表达式类型"(AOT 按类型实例化自登记的
前提)的直接障碍。本步把它降级为运行期 operand layout。
变更:
- `expr_spec.hpp`:
- `expr_spec_key` 剔除顶层 `matmul.transA/transB`(`a_input/b_input` 仍进 key);
- 新增 `expr_spec_runtime_matmul_trans`(bit0=transA / bit1=transB);
- `MatmulSpec` 与 `expr_spec_key` 注释同步(转置不进 key 的理由与载体)。
- `expr_glsl_gen.hpp`:
- `generate_glsl_matmul` 的 PC 增 `uint mm_trans`;`load_tiles` **两条加载
路径都发射**,外层套 `if ((mm_trans & 1u/2u) != 0u)`(分支按 dispatch 统一
= uniform,预测代价≈0);
- `generate_glsl_reduce`(matmul+归约)的 PC 增 `uint mm_trans`,内联点积把
两种索引折算成 `base + kk*stride`(循环体内无分支);
- `a_load`/`b_load` 改收显式 `bool trans`(由两条分支分别传常量)。
- `compute_vk_backend.hpp`:`run_fused_gpu` 增 `matmul_trans` 形参;PC 形态
matmul 5→6、matmul+归约 6→7(fold 形态不变);**创建侧 `pc_base` 与写入侧
同改**(顺手把归约单形态 5→4 的既有松量收紧为逐形态一致)。
- `compute_gpu_engine.hpp`:4 处 `run_fused_gpu` 调用点传
`expr_spec_runtime_matmul_trans`(fold 路径恒传 0——fold 的 mm 段转置仍进 key)。
- `tools/prec_backfill.txt`:Linear matmul 段 key 迁移
(`644a67003cf02a4d` → `ad5a483ea593210a`;其余 6 条无顶层转置差异、不变)。
- `docs/development/02`、`03`:新增"运行期 operand layout:matmul 转置"一节 +
key 字段归属表更新。
验收(本机 clang + Vulkan dev2):
- build 122/122,零告警(-Werror);
- ctest 19/19;
- CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变;
- 结构数 77 → 72(5 个"仅转置不同"的结构合并);shader 153 → 146
(72 结构 + 74 变体);回填清单 7/7 命中;
- 生成物抽查:23 份 matmul/matmul+归约 shader 含 `mm_trans` 与两条加载路径;
归约形态为 `base + kk*stride`(内层无分支)。
`GroupedReduceSum/Max` 视图的 `param = R`(池化窗口面积 = pool²)原先按"结构"
进 key:R 由层构造期参数(`pool_`)决定、取值空间无界,于是
**GPU 上 `MaxPool2D(pool≠2)` 会闭合世界未命中硬报错**——扫描只跑了 pool=2,
而 DSL 侧 `dsl::grouped_reduce_*(t, R)` 四种 R 同类型,无法按类型区分。
本步把 R 降级为运行期视图参数(与 RowMod/RotateHalf/BatchMod/BatchCol/RowAccess
同族),使"结构 = 表达式类型"对分组归约成立。
变更:
- `expr_spec.hpp`:`expr_view_has_runtime_param` 纳入 GroupedReduceSum/Max
→ R 不进 key、改走 vp 槽(`expr_spec_runtime_view_params` 自动带上 R,
`expr_view_runtime_param_slots` 复用既有 1 槽语义);枚举/注释同步。
- `expr_glsl_gen.hpp`:
- `glsl_view_read` 的 GroupedReduce 分支不再编译期展开 R 次读取链,改为发
`gr_r<slot>(row, col)` 调用;
- 新增 `glsl_emit_grouped_reduce_helpers()`:为每个分组归约视图发射
`gr_r<i>()`(内部 `for (tt = 1u; tt < R; ++tt)` 运行期循环,累加顺序与
CPU 模板路径 `GroupedReduceRef::eval` 逐元素一致,铁律 #8);在 elementwise
生成器 PC 之后调用(分组归约不是归约视图 → 只走该路径)。
- `src/maxpool_gradcheck.cpp`:新增 `pool=3`(R=9)用例——该用例在改动前
于 GPU 上必然硬报错,现为门禁(CPU/GPU 双跑)。
- 文档:`03` key 字段归属表、`usage/03` 的 R 语义、`12` 的新增 ExprViewKind
落点清单(补"runtime param 需要生成器侧辅助函数"这一分叉)。
验收(本机 clang + Vulkan dev2):
- build 122/122,零告警(-Werror);
- ctest 19/19(`cnn_test` 与 `cnn_test_gpu` 均含新 pool=3 用例并通过);
- CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变;
- 结构数 72 不变(扫描只登记 pool=2,无合并);shader 72 + 74 变体 = 146;
- 回填清单 7/7 命中(无 key 迁移);生成物抽查:3 份分组归约 shader 含
`gr_r0()` 运行期循环 + 调用点。
AOT 自登记(`FusedAnchor<Expr>`:按表达式**类型**折叠出结构)需要"从类型构造
符号实例"——DSL 叶子持有 `Tensor`,符号实例只能用默认构造的 `Tensor{}`
(未绑定、无存储)。而 `CpuViewCache::cache_cpu_view()` 对默认张量会走到
`TensorAccess::cpu_matrix()` 并在 `NN_ASSERT` 处硬失败(探针实测)。
本步只加一道护栏:`cache_cpu_view` 在 `!t.valid()` 时直接返回(`data` 保持
nullptr)。语义不变——eval 路径上真用到无存储张量仍会在 `at()` 处断言,
只是断言点后移;`to_spec` 折叠路径本就不读张量数据。
**同批完成的可行性验证**(结论:方案成立,未入库的临时探针):对 9 组表达式
分别用真实张量与 `Tensor{}` 构造并比较 `expr_spec_key`,全部一致——
ReLU fwd / Linear fwd(matmul+row_broadcast)/ Linear grad_input(transA)/
Linear grad_w(transB+累加)/ row_reduce_sum / grouped_reduce_max(不同 R)/
rotate_half / row_access / **不同常量值**(确认常量值不进 key)。
即:`to_expr_spec(符号实例)` 的 key ≡ 运行时实例的 key —— 这是
"结构 = 表达式类型"成立、以及步骤⑤按类型自登记的前提。
验收:build 零告警;ctest 19/19;CPU 锚 `6f8849f14da23110`、GPU dev2 锚
`8ef51b2927253c50` 逐位不变。
每个 `dsl::compute/_into/_reduce` 实例化 odr-use 一个 `FusedAnchor<Expr>`,其 **静态初始化期**把"从表达式类型默认构造的符号实例"折叠出的结构登记进注册表。 调用点在函数体里 → 函数被编译即实例化 → 覆盖 = **编译期可达**(该分支/配置 没被跑到也会登记),正好补上 dry-run"只覆盖跑到的路径"的缺口。 前提(步骤①②③已落地):结构 = 表达式类型。本步补齐最后一块: - `GroupedReduceRef` 的 `is_max` 提为模板参数 `GroupedReduceRef<IsMax>`——它 决定视图 kind(= 结构),运行期 bool 会让 sum/max 同类型、结构不可枚举; `has_reduction_v` 随之对两个特化分别显式声明。 - 视图节点加符号默认构造:`TensorRef`/`RotateHalfRef`/`RowModRef`/ `RowAccessRef`/`BroadcastRef`;并为 `block`/`mod`/`offset`/`modulo`/ `per_batch_cols` 补中性默认值(它们是不进 key 的运行期视图参数;不补默认值 会让符号实例带不确定值 → bin 内容跨构建漂移,破坏 hash 恒定的验收口径)。 **A/B 实测**(`-DNN_SCAN_NO_ANCHOR` 关掉锚点):结构数 **72(仅 dry-run) → 84(+锚点)**,多出的 12 条均为编译进来的真实调用点(3 条含 matmul、1 条 归约、1 条 16 个 vp 槽的宽表达式、其余逐元素)——即此前闭合世界的潜在硬报错点。 ⚠ **本步暴露的关键限制:精度签名登记不出来。** 同一表达式里"哪些输入是 f16" 由运行期张量精度决定(类型层面不可见)。实测扫描出的 60 个带类型变体里 **只有 16 个是"全 f16 输入+输出"**,其余 44 个是混合签名 (`0x0001/0x0003/0x10005…`)。故 f16 带类型变体**仍须**由 profile_f32/profile_f16 两遍 dry-run(或运行期 `NN_PREC_TRACE` + `prec_backfill.txt`)产生 → **步骤⑥ 不能直接删掉 dry-run**:改为"锚点管结构 + 模型级驱动管签名",下一步实施(原计划"删 scan_exprs"作废)。 验收(本机 clang + Vulkan dev2): - build 零告警(-Werror);ctest 19/19; - CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变; - shader:84 结构 + 74 变体 = 158(较上一步 146 多 12,即新增结构的 sig=0 版); - `prec_backfill.txt` 7/7 命中;文档 `02` 新增"自登记锚点"一节(含 A/B 数字与 签名限制)。 可用 `-DNN_SCAN_NO_ANCHOR` 关锚点做 A/B(只影响登记集合,不影响语义)。
原步骤⑥「删除 scan_exprs / NN_EXPR_SCAN」**不可行**,本步把边界做成可验证的:
- **结构**:自登记锚点已覆盖(编译期可达),dry-run 不再是结构的唯一来源;
- **签名**:精度签名(同一表达式里哪些输入是 f16)由运行期张量精度决定,
**类型层面推不出来**。实测现有 60 个带类型变体里只有 16 个是"全 f16 输入+
输出",其余 44 个是混合签名(0x0001/0x0003/0x10005/0x100ff…)
→ 必须由 profile_f32/profile_f16 两遍 dry-run(或 NN_PREC_TRACE +
prec_backfill.txt)产生,锚点无法取代。
故把扫描的职责**收窄并显式化**,而不是删除:
- `expr_dsl.hpp`:锚点改登记进独立注册表 `anchor_registry()`(原先直接进
`global_registry()` 会与 dry-run 混在一起,无法计量贡献)。
- `tools/scan_exprs.cpp`:
· 末尾合并锚点注册表并打印常驻台账
`[scan] 结构来源:dry-run 72 + 锚点 59 → 合并 84(锚点独有 12)`;
**锚点独有数突然变大 = 有新层路径没纳入 dry-run**(回归信号);
· 新增签名覆盖报告 `[scan][note] 37/84 条结构没有带类型变体`
(缺 f16 → 运行时回退边界 cast:正确但慢;报告里写了补法)。
新增融合表达式时这一行会立刻提示该结构没有 f16 变体。
- `docs/development/02`:锚点一节补"常驻台账"。
实测:dry-run 72 / 锚点 59(去重后)/ 合并 84 / 锚点独有 12 / 无变体结构 37。
验收(本机 clang + Vulkan dev2):
- build 零告警(-Werror);ctest 19/19;
- CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变。
上一步证明"自登记只能给结构、给不了精度签名"——签名必须由执行产生。本步把
执行面从"per-layer dry-run 块"扩到"**完整模型**":
- `tools/scan_exprs.cpp` 新增 `model_pass`:在两个 profile(f32/f16)下各用
shipped 工厂建 5 个模型并跑 forward/backward —— `mnist_mlp` /
`mnist_transformer`(ViT)/ `cnn`(LeNet)/ `gpt`(Learned PE)/
`rapt`(RoPE+causal,小配置 vocab=64/d_model=16/seq=8/H=2/d_ff=32/L=2)。
· GPT/RAPT 输入是 token id (seq, batch),`gather_rows` 按值取行 → 必须
`engine.write` 填**合法** id(否则越界);
· 失败只打 `[scan][warn]`、不中断构建——本 pass 是**增量**(结构已由
FusedAnchor 保证,主路径已由 dry-run 块覆盖),但信息不吞。
- 依赖补齐:scan_exprs 增 include `domain_{mnist,cnn,gpt,rla}.hpp`。
实测(同一台机、同一配置):
- 精度变体 **60 → 72**(+12;直方图上 0x0002 +4、0x10000 +3、0x10001 +2、
0x0003/0x0005/0x10005 各 +1);
- `tools/prec_backfill.txt` 的回填命中由 **7/7 降到 2/7** —— 即 5 个原本
"只有真实训练跑起来才暴露"的 f16 混合签名(Linear matmul 段 10001/10005、
三条 Norm 归约链 0002、全 f32 入 + f16 出 10000)现在**构建期即可预测**。
回填清单条目**保留不动**(生成端幂等去重,留着可防模型 pass 变更后回退);
- 结构数不变(84):模型级调用点的结构此前已由锚点补上(锚点独有 12);
- shader:84 结构 + 81 变体 = 165(较上一步 158 多 7);
- 两次 `scan_exprs` 产物 SHA256 逐字节一致(确定性未破)。
验收(本机 clang + Vulkan dev2):
- build 零告警(-Werror);ctest 19/19;
- CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变。
在上一轮"模型级签名 pass"(5 个模型 × f32/f16)基础上:
1. **配置矩阵化**:把固定 5 个配置扩成矩阵——`mnist_mlp`×{LayerNorm,RMSNorm,
BatchNorm}、`mnist_transformer`(ViT)、`cnn`×{pool2,pool3}、`gpt`×
{Learned,ALiBi,Sinusoidal,RoPE} 与 {GeLU,SwiGLU}×{LayerNorm,RMSNorm}、
`rapt`×{causal,bidir}(小配置;id 输入填合法 token 值)。签名 72 → **73**,
全部 15 个配置零告警跑通,扫描耗时 0.83s。
2. **分项来源台账**(每次构建打印):
```
[scan] 结构来源:dry-run 64(含模型 pass +0、显式登记 +8) + 锚点 59 → 合并 84(锚点独有 12)
[scan] 签名来源:dry-run 60 + 模型 pass 13 + 显式登记 0 = 73
```
模型 pass 贡献的是**签名**(+13)而非结构(+0)——结构此前已由锚点补齐,
台账把这一点显式化,避免误读。
3. **A/B:手写 per-layer 块的残余贡献已被量化**(新增 `-DNN_SCAN_NO_DRYRUN`
开关,同 `-DNN_SCAN_NO_ANCHOR`)。关掉那批块后只剩 **76 结构 / 50 签名**
→ 手写块目前仍贡献 **8 结构 + 23 签名**。其中 5 个结构是 scan_exprs 内的
**显式测试覆盖登记**(matmul+bias+relu / bmm_reduce×3 / reduce_consts),
不是层 dry-run。这组数字 = "手写清单还差多少才能删"的度量:
⇒ 需要先让 dry-run 之外的来源覆盖"优化器 / 损失 / 增量推理(apply_step) "
等整模型跑不到的路径。
4. `-DNN_SCAN_NO_DRYRUN` 与既有的 `-DNN_SCAN_NO_ANCHOR` 都只影响登记集合、
不改语义;两者都用于上述 A/B。
验收(本机 clang + Vulkan dev2):
- build 零告警(-Werror);ctest 19/19;
- CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变;
- 两次 `scan_exprs` 产物 SHA256 逐字节一致(确定性)。
上一轮把"手写块的残余贡献"量化到 8 结构 + 23 签名。本步用**两条硬证据**判定 其中 13 个块可以整块删掉,而不是靠推断: 删除范围(`tools/scan_exprs.cpp`):ReLU / SwiGLU / GeLU / Softmax / RMSNorm / LayerNorm / ReLULinearAttention(causal + bidir) / GPTBlock / TransformerEncoderLayer / Linear / Conv2D / MaxPool2D —— 共 223 行。 判据 1(结构无关):**删除前后 bin 的结构集合逐字节一致**(84 条), 台账从 `dry-run 64(模型 pass +0、显式 +8) + 锚点 59 → 84` 变为 `dry-run 38(模型 pass +25、显式 +8) + 锚点 59 → 84` —— 模型 pass 从"+0(都被手写块先占了)"变成"+25(接手了这些结构)", 证明覆盖确实被模型 pass 接走。 判据 2(运行时签名无关):`NN_PREC_TRACE=1 mem_probe --f16` 的 5 个工作负载 (默认 / `--doc-mask` / `--checkpoint-every 2` / `--optimizer muon` / `--activation-offload`)在删除前后**是同一个 miss 集合**(都只有 fold doc-mask 一条,且该条与本删除无关——fold 登记未动,见下一提交的补法)。 保留的来源 = **整模型跑不到**的那些:优化器(5 变体)、损失(MSE / CE 稠密 / CE 稀疏)、RoPE `apply_step`(增量推理)、CSA×4(掩码/偏置组合,含模型未覆盖的 doc 变体)、MHA、以及 `scan_exprs` 内的显式测试覆盖登记(matmul+bias+relu / bmm_reduce / reduce_consts)。 文档:`docs/development/02` 的锚点一节补"已验证可删"的判据与保留清单、 更新台账示例数字。 验收(本机 clang + Vulkan dev2): - build 零告警(-Werror);ctest 19/19; - CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变。
`NN_PREC_TRACE=1 mem_probe --f16 --steps 2 --doc-mask` 复现出一条 fold 的
run-only 签名:
```
[prec][miss] key=74a6eaacc0e5d766 sig=0x10007 in=[f16,f16,f16,f32,f32] out=f16
instrs=0 mm=0 fold=1 raxis=-1
```
即注意力 forward 单 fold kernel 的**文档掩码变体**:5 输入 [f16,f16,f16,f32,f32]
= Q/K/V f16 直读 + 文档 id/边界 f32,输出 f16。scan 的 fold 显式块按
"掩码 × 位置偏置"组合登记**结构**,但该形态的 f16 **签名**只有真实 doc-mask
训练才暴露(与既有 66b03ef936bb97c7 那条同族,多两个 f32 输入槽)。
- `tools/prec_backfill.txt`:新增 `74a6eaacc0e5d766 10007`(含来源与复现命令)。
- 复跑验证:`[gen][backfill] 3/8 条命中发射`;`mem_probe --f16 --doc-mask`
的 `[prec][miss]` 由 1 条归零。
无库代码改动(纯构建期清单,生成端幂等去重)。
验收:build 零告警;ctest 19/19;CPU 锚 `6f8849f14da23110`、GPU dev2 锚
`8ef51b2927253c50` 逐位不变(本提交不影响运行期数值)。
对上一轮的块删除做了**跨路径**的 miss 验证(此前只验了 GPT 路径的 mem_probe),
结果发现 MNIST/CNN f16 训练本来就有 miss,且删除又新增了 2 条:
基线对比(`NN_PREC_TRACE=1 mnist_train --arch <x> --f16 --epochs 1 --batch-size 8
--max-samples 32`,上一版含手写块的构建 vs 当前):
| arch | 基线 miss | 当前 miss(删除后) |
|---|---|---|
| mlp | 1 | 1 |
| cnn | 5 | **7(+2)** |
| transformer | 2 | 2 |
两条新增来自被删掉的 ReLU / 逐元素块(`cb3d830f25b84895 10001`、
`8a29d02213a0e196 10003`);其余 6 条是**既有缺口**(与本次删除无关)。
处理方式 = 走既定兜底通道 `tools/prec_backfill.txt`(而不是把手写块加回来):
追加 8 条 `(结构, 签名)`,含复现命令与成因说明。成因:MLP/CNN 的输入与部分中间
张量在 master-weights 配方(param=f32/compute=f16)下是 **f32 存储** → 出现
"f32 入 + f16 出"的混合签名;scan 的 dry-run 用 compute 精度的占位张量,预测不到。
验证(全部 0 miss):
- `mnist_train --arch {mlp,cnn,transformer} --f16`(3 个负载);
- `mem_probe --f16 --steps 2 --kv-steps 4`(默认)与 `--doc-mask`;
- `[gen][backfill] 11/16 条命中发射`;shader 84 结构 + 84 变体 = 168。
验收:build 零告警;ctest 19/19;CPU 锚 `6f8849f14da23110`、GPU dev2 锚
`8ef51b2927253c50` 逐位不变(本提交不触碰库代码,只增加构建期变体)。
`FusedAnchor<Expr>` 按表达式**类型**登记结构,其正确性完全依赖"key 只描述结构"。
此前这条不变量只有临时探针在 `build/` 下验过(不入库);本步固化为正式测试
`src/expr_fused_key_test.cpp`,并入 `expr_cpu_test` 聚合目标(ctest 目标数不变 = 19)。
覆盖(15 项断言):
- **符号实例 key ≡ 真实实例 key**(8 组:ReLU / Linear fwd(matmul+row_broadcast) /
matmul transA / matmul transB / row_reduce_sum / grouped_reduce_max /
rotate_half / row_access)——这是自登记机制的全部依据;
- 常量池**值**不进 key(`Scalar{1e-6}` vs `Scalar{1e-5}` 同 key);
- matmul **转置**不进 key(4 种组合同 key)+ `expr_spec_runtime_matmul_trans`
位编码(bit0=transA / bit1=transB,编码错 = GPU 静默读错布局);
- 分组归约 **R** 不进 key(R=4 vs R=9 同 key),且 sum/max 是两种视图仍必须区分;
- 三个符号默认可构造性 static_assert(锚点要求整棵表达式树可符号构造)。
任一条被违反 → 运行时 key 与构建期登记 key 不一致 → 闭合世界 miss;本测试立刻红,
把"步骤①②③建立的 key 语义"变成不可回退的契约。
文档:`docs/development/02` 锚点一节补"契约回归"。
验收(本机 clang + Vulkan dev2):
- build 零告警(-Werror);ctest 19/19(新子测试 0 failure);
- CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变。
原构建期两步(`scan_exprs` 写 `expr_specs.bin` → `gen_fused` 读 bin 再生成 `fused_registry.hpp`)中间的那次序列化没有任何必要——两步都只是同一个注册表的 搬运。本步合并为**一步**,并删掉整套 bin 序列化。 变更: - `tools/gen_fused.cpp` → **`tools/fused_generate.hpp`**:`main` 改为 `nn::tool::generate_fused_registry(out_dir, glslc, reg, prec_manifest)` (注册表由调用方传入),加 `#pragma once`;`read_registry` 调用删除。 `--list-backends` 迁到 `scan_exprs`。 - `tools/scan_exprs.cpp`:include 生成头;CLI 从 `<out.bin>` 改为 `<out_dir> <glslc> [<prec_backfill.txt>]`;收集完直接调用生成函数。 - `CMakeLists.txt`:删除 `gen_fused` target 与第二条 custom command; `fused_registry.hpp` 由单条命令(`scan_exprs`)产出。 - `include/neuralnet.cpp/expr_registry.hpp`:删除 `write_registry` / `read_registry` / `kExprBinVersion` 及 `<fstream>`/`core_file.hpp` 依赖 (388 → 90 行);头注释改写为当前单步架构。 - `src/expr_fold_test.cpp`:原 "bin 序列化 roundtrip" 子测试改为**内存版** (同一 `ExprRegistry` 的 add 去重 + tri_skip 进 key 两条不变量), 保住原测试的意图而不依赖已删除的序列化。 **行为保持的硬证据**:在与 HEAD 同源的 `git worktree` 里用**旧代码**独立构建 (旧的 scan+gen 两步流程),产出的 `build/generated/fused_registry.hpp` 与本步 新流程的产物 **SHA256 逐字节相同**(`4BDEEC3A3B4888E08B3BDC94A1714698FA2B773DB283F489FC490BE208D2F590`); 新流程连跑两次产物亦相同(确定性未破)。 文档同步(仅"当前状态"类文档;`history.md`/`18-roadmap`/`release-notes` 属历史 存档按约定不改):`02`(构建工具链两步 → 一步)、`01-architecture`(文件表 + AOT 收集原则)、`03`/`04`/`05`/`usage/03`(工具名与 bin 口径)。 验收(本机 clang + Vulkan dev2): - build 零告警(-Werror);ctest 19/19; - CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变; - f16 trace 5 个负载(mnist mlp/cnn/transformer + mem_probe 默认/--doc-mask) `[prec][miss]` 全为 0; - 生成头与旧流程逐字节相同(见上)。
`bench/doc_align_audit.ps1` 的 [A] 段(文档引用但仓库中不存在的文件)报了 2 处—— 都在上一步的单步构建改动里:改写文档时写了"(原 `gen_fused.cpp`)",而该文件已 重命名为 `tools/fused_generate.hpp`。按仓库约定(当前态文档不得引用不存在的文件, 历史提及只留在 `history.md`),改为不引用文件名: - `docs/development/02-operator-fusion.md`:生成阶段说明改为"原独立工具 `gen_fused`,现已并入本工具"。 - `docs/introduction/01-architecture.md`:文件表同样措辞。 验收:`bench/doc_align_audit.ps1` **[A] 可行动 0 处**、[D]/[E]/[F] 均 0; `bench/doc_inventory.ps1` **L2-VIOLATIONS: 0**(virtual 49、Layer 直调 21 不变); `python bench/gui_cli_audit.py` **PASS(可行动 0)**。纯文档改动,无代码影响。
用真实训练负载(用户口径)跑 `NN_PREC_TRACE=1` 复现出两条此前未被任何 trace 覆盖的 run-only 签名: ``` text_train datasets/tinystories_smoke.txt --epochs 1 --d-model 128 --num-heads 4 --num-layers 4 --d-ff 512 --seq-len 64 --batch-size 50 --gpu --f16 [--model rapt] [prec][miss] key=b7e6d363af1963ac sig=0x0001 in=[f16,f32] out=f32 (GPT, mm=1) [prec][miss] key=a1d46a05bb060c47 sig=0x10001 in=[f16,f32,f32,f32,f32] out=f16 (RAPT, 8 指令链) ``` 第二条的 key `a1d46a05bb060c47` 正是第 5 轮 A/B 里"dry-run 有、锚点没有"的那条结构 ——现在有了它的运行时证据:**RAPT 训练真的会用到它**,此前每次 `--f16` 都走边界 cast。 处理:`tools/prec_backfill.txt` 追加两行(含复现命令与形态说明)。 验证:`[gen][backfill] 13/18 条命中发射`;两条 miss 均归零(shader 84 + 86 = 170)。 顺带(无代码改动)的一次基线核查:用户在 40HX 上观察到 `--model rapt --f16` 比 GPT 慢约 16×(61.4s vs 3.9s)。在 `main`(586cfb2)的独立 worktree 里跑同一 命令得 **60.1s** —— **该差异是既有行为,不是本分支引入的回归**(loss 的小差 来自 text_train 的运行期洗牌随机性,见 AGENTS §12)。 验收:build 零告警;ctest 19/19;CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变(本提交只增加构建期变体,不触碰库代码)。
本轮把"锚点覆盖"从集合对比升级为**运行时证据**,顺带修掉闭合世界报错不可定位的问题。
## 1. 运行时验证(`-DNN_SCAN_NO_ANCHOR` 关锚点,结构 84 → 71)
关掉锚点后跑真实负载:
| 负载 | 结果 |
|---|---|
| `gpu_stability_probe --gpu 2 --steps 20` | PASS(hash 不变) |
| `mnist_train --arch {mlp,cnn,transformer} --f16 --gpu` | 无错 |
| **`text_infer --gpu`(KV-cache 增量解码)** | **闭合世界硬报错** |
```
GpuEngine::eval_expr: 未找到该内联表达式的 AOT 融合 shader(闭合世界);
|key=9ca81b4967cdfa20
```
`9ca81b4967cdfa20` = `mm=1 + rparam×1 + 两个 Linear 输入` —— 即 `forward_step`
的注意力打分链,**手写 dry-run 从未覆盖过的路径**(它正是第 5 轮 A/B 里"锚点独有
13 条"之一)。打开锚点后同一命令正常:`生成 8 tokens, 耗时 0.2s (47 tokens/s)`。
⇒ 结论升级:锚点不是"纯理论安全网",它挡住了**已发布负载** `text_infer` 在 GPU 上的
硬报错。文档 `docs/development/02` 已记录该证据。
## 2. 闭合世界报错带 key(库代码)
此前 4 处闭合世界硬报错只说"请纳入构建期扫描",无法定位是哪个结构。现在都追加
`key=<expr_spec_key>`(`expr_spec_key` 是登记/查表两侧的唯一标识,可直接去
`fused_registry.hpp` 里查有无该条目):
- `GpuEngine::eval_expr`(fold 分支 + 通用分支)
- `GpuEngine::eval_expr_reduce`
- `GpuEngine::eval_expr_into`
上面那条 `key=9ca81b4967cdfa20` 就是靠它定位的——没有这个改动,只能看到
"该路径未扫描",仍需人工二分。
验收(本机 clang + Vulkan dev2):
- build 零告警(-Werror);ctest 19/19;
- CPU 锚 `6f8849f14da23110`、GPU dev2 锚 `8ef51b2927253c50` 逐位不变
(错误串不影响数值);结构 84 / shader 84+86=170。
两部分(用户裁定"完成",故一并提交): ## 1. AGENTS.md 跟进本分支的 AOT 融合现状(5 处) - §3 构建期工具:`tools/gen_fused.cpp` → `tools/fused_generate.hpp`,并标注 `scan_exprs` 为**单步**(收集 + 生成)。 - §7 分组归约:删去"R 进 key"(R 走运行期视图参数 `vp`,**不进 key**,同一结构 不同 R 共享一个 shader)。 - §7 构建期:**两步 → 单步**(`FusedAnchor<Expr>` 按类型自登记**结构** + 模型 配置矩阵产生**精度签名**,同进程直接生成 `fused_registry.hpp`,不经 `.bin`)。 - §12 约束 4:改写为"**key 只描述结构**"——常量池**值** / `MatmulSpec.transA·transB` / 分组归约 **R** 三者不进 key(分别走 PC `c<i>` / `mm_trans` / `vp`);属结构必须 进 key 的 = `AttnScoreMask::mask_kind()`、`FoldSpec.tri_skip`;并指向契约回归测试 `src/expr_fused_key_test.cpp`。 - §12 约束 6:IR 扩展点补 transA/transB 与分组归约 R 的 key/PC 口径。 ## 2. 13 号清单合并归档(用户未提交的 WIP,随本提交入库) `docs/development/13-refactor-backlog.md` 收敛为重定向 stub(−334 行),正文归档到 `docs/history.md`(+87 行),未完成项与裁定台账迁入新建的 `docs/development/18-roadmap.md`(296 行);AGENTS.md §11 索引同步指向后两者。 **单独提交 AGENTS.md 会引用未入库的 18-roadmap.md,故四文件一并提交。** 验收(文档改动,无代码影响): - `bench/doc_align_audit.ps1` **[A]/[D] 可行动 0**(引用文件全部存在); - `bench/doc_inventory.ps1` **L2-VIOLATIONS: 0**(virtual 49 / Layer 直调 21 不变); - `python bench/gui_cli_audit.py` **PASS**。 未提交:`fineweb_2048mb_bpe_32768.json`(数据产物,与本次文档同步无关)。
新增 EXPR_PREC_SIG_DISPATCH 哨兵(0x00020000,落在输入位/输出位之外, 既有谓词对它一律 false)+ 后缀 #x。 五个生成器(元素/matmul/fold v1/fold v2/reduce)支持分派形态:输入输出各 声明 f32 + float16_t 双视图,加载/存储处按 push constant uint prec 走 uniform 分支。索引数学**逐字不变**(两种视图共用同一逻辑下标),因此分派 分支与既有带类型变体在数值上等价。 非分派路径的发射文本完全保留:重建 build/generated/fused_registry.hpp 与改前 SHA256 逐字节一致(6DF95D20…84D)。
生成期(tools/fused_generate.hpp):
· 每个结构无条件发 V1(键 key#x)= 双视图 + PC prec;
· V2(键 key#a,native16)改由**结构谓词**判定生成(代入「全输入 f16
+ 输出 f16」这一常量签名),不再依赖执行期发现的签名;
· 删除 (结构,签名) 变体的发射与 run-only 回填(回填清单不再参与生成)。
运行期(compute_gpu_engine.hpp / compute_vk_backend.hpp):
· find_prec_variant_ 改为:psig==0 → V0;全 f16 且输出 f16 → V2(若有);
否则 → V1。V1 对每个结构无条件存在 ⇒ **任何签名都命中,无 miss**;
· run_fused_gpu 增 prec_sig 形参,分派变体写 PC 末尾的 prec 槽;
· 分派变体描述符 = 2N 输入视图 + 2 输出视图(同一 VkBuffer 绑两次,
shader 只解引用与真实元素类型一致的那一个);
· supports_expr_precision_variant 与 eval_expr_impl 选路同源。
验收:
· build 零告警;ctest 19/19;
· CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变;
· GPT f16 训练 NN_PREC_TRACE=1:[prec][miss]=0、边界 cast 归因表为空、
alu-hit 6670 —— f16 路径全程命中预生成 shader,零 cast 回退;
· 注册表 84 结构 + 84 分派 + 35 native16 = 203 条(旧:84 + 86 变体)。
· tools/prec_backfill.txt 删除:生成阶段不再消费回填清单 (CMake 的 COMMAND/DEPENDS 同步去掉); · scan_exprs 用法去掉 [<prec_backfill.txt>] 形参; · f16 dry-run 遍 + f16 模型 pass 删除——**实测其对结构贡献为 0**: 跳过 f16 遍后签名 66 → 0、结构恒 84(NN_SCAN_F32_ONLY 探针); 该两遍存在的唯一理由是发现精度签名,而签名已不是构建期集合; · scan_exprs 末尾的"没有带类型变体 / 用 NN_PREC_TRACE 回填"提示改为 结构侧诊断(该工作流已不存在)。 验收: · build 零告警;ctest 19/19; · 注册表仍 84 结构 + 84 分派 + 35 native16 = 203 条(与 A1b-d 逐字节同构); · CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变。
· AGENTS.md §7:构建期单步改为「只收集结构;每个结构发 V0/V1/V2 三份」, 新增两节——「精度签名不再是构建期集合」与「新增调用点无需改动其他文件」 (含端到端实测数字);§12 约束 4 补「精度签名同样不进 key」。 · docs/development/02:新增 §精度签名已不是构建期集合(A1),改写收集步骤、 自登记锚点(只登记结构 / 模型 pass 只跑 f32)、常驻台账、回填条目改为"已删除"。 · docs/development/05 §12.10 ⑤:签名回填流程 → 运行期精度分派;§12.12 待办改写。 · docs/introduction/01:「AOT 收集原则」段同步。 · docs/usage/03:NN_PREC_TRACE 行与闭合世界排错行同步。 · docs/history.md:新增归档条目(旧机制、删除清单、A vs B 性能 A/B、 实测验收、以及"结构侧只能靠 dry-run/模型 pass 提供 71/84"这一关键约束)。 验收:doc_align_audit [A]/[D]/[E]/[F] 可行动 0;doc_inventory L2-VIOLATIONS 0、 virtual methods 49;build 零告警(本次仅文档变更);注册表仍 203 条 / 84 结构。
缺口:本库 header-only,但 fused_registry.hpp 的生成绑死在本仓库
(CMakeLists 硬编码 tools/scan_exprs.cpp,nn_core 无导出)——库外使用者
写自己的 dsl::compute 后没有任何合法途径产出自己的注册表。
落地:CMake 函数 nn_enable_gpu_fusion(<target> MAIN <含 main 的源>
[OUT_DIR <目录>])。收集器 = 库内收集逻辑(tools/scan_exprs.cpp:锚点 +
dry-run + 模型 pass + 显式登记)+ 本目标的全部 TU(-DNN_EXPR_SCAN 编译,
令使用者调用点自登记),产物目录作该目标的私有 include 目录。
· scan_exprs.cpp 的 `#define NN_EXPR_SCAN` 加防重定义护栏(收集器侧用
-DNN_EXPR_SCAN 编使用者 TU 与此冲突);
· **顺带修 header-only 缺陷**:compute_vk_backend.hpp 的
`GpuBuffer::~GpuBuffer()` 类外定义缺 `inline` → ≥2 个 TU 的程序链接期
duplicate symbol(此前未暴露,因为库自身目标都是单 TU);
· 新增样例 examples/fusion_custom_layer{,.hpp,_main.cpp}(库外使用者形态,
含自研融合层 + 一行 CMake),并加进默认构建。
实测:样例目标注册表 = 库内 203 条 + 自研层 `94a2cc052598fe09` / `#x`
(该表达式含 rparams → native16 谓词不通过,无 `#a`,符合预期);样例
可执行文件输出与期望值 0.6712035 逐位一致;库内文件零改动。
验收:build 零告警;ctest 19/19;CPU 锚 6f8849f14da23110 / GPU dev2 锚
8ef51b2927253c50 逐位不变;库内注册表仍 203 条 / 84 结构。
EthanPeng-2048
marked this pull request as ready for review
October 1, 2026 10:53
签名不再是构建期集合之后,这些代码全部成了死重:
· expr_registry.hpp:删除 `ExprVariant`、`variants`、`variant_keys`、
`contains_variant`;`add()` 收敛为只收结构;
· expr_dsl.hpp:scan 模式的 compute/compute_into/compute_reduce 与 FusedAnchor
一律只 `add(spec)`(原先每个调用点还要算一次 `expr_prec_sig_of`);
· tools/scan_exprs.cpp:删除 variants 计数器与"签名来源"台账行、
以及末尾的"精度变体 N 条"打印;
· tools/fused_generate.hpp:`emit_one` 的 key/suffix 收敛为两种形态
(`key#x` 分派 / `key#a` native16),删掉永不可达的 `key#<sighex>` 分支;
分派变体发射失败改为**可见告警**(它是该结构唯一的签名无关 shader),
native16 失败仍静默跳过;
· expr_spec.hpp:删除已无调用点的 `expr_prec_sig_alu_key`
(保留 `expr_prec_sig_key` 供 `NN_PREC_TRACE` 诊断用)。
验收:
· build 零告警;ctest 19/19;
· **build/generated/fused_registry.hpp 与清理前 SHA256 逐字节一致**
(3D47775F77D34888069482F3BFDA410B837D55F9A8A68FE5B5A15A5BA1B7F64D)
—— 证明删掉的全是不可达路径;
· CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变;
· doc_align [A]/[B]/[D]/[E]/[F] 可行动 0。
原先的样例只验 CPU——而 CPU 走编译期模板求值、与注册表无关,**等于没验到
本 PR 的核心机制**。补上 GPU 路径:
CPU f32 = 0.6712035(差 0)
GPU f32 (V0) = 0.6712036(差 5.96e-08)
GPU f16 (V1 分派) = 0.6708984(差 3.05e-04,f16 舍入内)
该表达式含 rparams ⇒ expr_prec_sig_native16 谓词不通过 ⇒ f16 那一跑命中的
必然是 `key#x` 运行期精度分派 shader(双视图 + PC `prec`)。注册表若漏了它
就是闭合世界硬报错 —— "跑通"本身即断言。
顺带修 nn_enable_gpu_fusion 的两处:
· include 顺序:只加 ${NNF_OUT_DIR} 时样例报 `matmul SPIR-V bytecode not
embedded`(手写原语 shader 嵌入头在 ${CMAKE_BINARY_DIR}/generated,而该
目录**同时含库自身的 fused_registry.hpp**)。现在两者都加且 **NNF_OUT_DIR
在前**(__has_include 取第一个匹配),否则使用者的自定义结构会解析到库的
注册表 → 全部闭合世界报错;
· add_dependencies(... compile_gpu_shaders)。
验收:build 零告警;ctest 19/19;库内注册表 SHA256 仍为 3D47775F…84D
(逐字节不变);CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50
逐位不变;样例退出码 0。
问题:⑥ 的目标是"删除扫描",此前只知"dry-run 贡献 71/84",**不知道那 71 条
为什么锚点看不到** —— 没这个答案就无法判断还能不能再进一步。
改动(纯诊断,env 门控,默认零打扰):
· scan_exprs 的合并处计算 dry_keys / anchor_keys 两个集合;
· NN_SCAN_DUMP_SOURCES=1 时逐条打印 `[scan][dry-only]` / `[scan][anchor-only]`
+ 结构摘要(raxis / mm / fold / nreg / ninstr / nview / viewkinds);
· "[scan] 结构来源"台账行补 `dry-run 独有 N`。
实测(84 条结构):per-layer dry-run 38(模型 pass +25、显式登记 +8)、锚点 59,
重叠 46 → **锚点独有 13、dry-run 独有 25**。
dry-run 独有 25 条分类:fold 8(= 显式登记的掩码×偏置组合与通用样例)、含
matmul 段 7、RowAccess(12) 4(共享内存 SwiGLU 半偏移行切分)、RowGather(9)/
ColBroadcast(8) 若干(稀疏 CE 的 5 视图形态)。共同点:**结构由运行期配置
决定**(视图种类/项数/掩码组合),类型层面推不出;符号实例 `Expr{}` 与真实
实例折叠出不同 key —— 最硬的证据是镜像对:dry-only `784781c7b82fa76c` 与
anchor-only `28cdf7c6d5df0e1d` 摘要逐字段相同,只有 key 不同。
结论:锚点与 dry-run **互补、不冗余**,单独删任一方都会丢结构。
`scan_exprs` 这一构建步骤本身删不掉;能删的只有签名维(已删,工作量减半)。
验收:build 零告警;ctest 19/19;**build/generated/fused_registry.hpp 与改前
SHA256 逐字节一致**(3D47775F77D34888069482F3BFDA410B837D55F9A8A68FE5B5A15A5BA1B7F64D)
——诊断只加打印,不改产物;CPU 锚 6f8849f14da23110 / GPU dev2 锚
8ef51b2927253c50 逐位不变;样例仍 OK。
起因:round 15 测出"25 条结构只有执行 Layer/模型才拿得到",由此引出一个更要紧 的问题——**库外自定义层没有 dry-run,`nn_enable_gpu_fusion` 是否就不可靠?** 实验(而非推理):给样例加第二种自研形态——归一化 `rsqrt(col_reduce_sum(x)·inv + ε)`,与库内 LayerNorm/RMSNorm 的 std_inv 同形 (raxis=1、viewkinds=[0,8],正落在 dry-run 独有 25 条的形态类别里),**只用锚点** 在 GPU 上以 f16 跑通: [example] GPU f16 归一化形态 = 1.2246094 ⇒ 锚点对常见自定义层可靠,A2 机制成立。 顺带排除一个错误假设:曾怀疑"符号实例常量默认值(全 0)触发 CSE 合并出不同 结构"。查 expr_opt.hpp:CSE key = op+a.kind/idx+b.kind/idx+c.kind/idx,**不含 常量值** ⇒ 与值无关,假设排除。同时发现此前的"镜像对"是误判——粗摘要相同但 指令序列完全不同(nconst=2 [17(1/0,1/1);19(0/0,2/0,2/1)] vs nconst=1 [1(2/0,0/0);10(1/0,0/0)]),是两个不同表达式。 诊断增强:NN_SCAN_DUMP_SOURCES=1 现在除形态摘要外还打印 nconst/nrparam 与**完整指令序列**(op(a.kind/idx,b.kind/idx,c.kind/idx)), 使"同源不同 key"这类判断不再依赖粗摘要。 验收:build 零告警;注册表 SHA256 仍 3D47775F…84D(逐字节不变); 样例 CPU/GPU f32/f16/归一化四路全过、退出码 0。
问题:`examples/fusion_custom_layer*` 此前只由默认构建覆盖编译——**运行时断言 断言需手工跑**(返回码/数值对拍),会静默腐化。而它正是"库外使用者写 dsl::compute + 一行 CMake → GPU 能跑"的唯一自动门禁。 改动: · CMakeLists:`nn_enable_gpu_fusion` 调用之后 `add_test(NAME fusion_custom_layer_example ...)` + `SKIP_RETURN_CODE 77`(无 Vulkan 设备 时跳过,与仓库既有约定一致); · 文档当前数字同步:AGENTS.md §12 测试条与 §11 索引行、docs/development/12 头部「当前数字」——19 → **20**(18 个测试目标 + `cnn_test_gpu` + `fusion_custom_layer_example`)。 验收:build 零告警;**ctest 20/20**(含新门禁,2.0s); 库内注册表 SHA256 仍 3D47775F…84D(逐字节不变); CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变; doc_align [A]/[E]/[F] 可行动 0;doc_inventory virtual 49、L2-VIOLATIONS 0。
发现:`src/fused_gpu_test.cpp`(44 KB,融合 shader **逐形态** GPU 对拍) **未被任何 CMake 目标引用**——既不编译也不注册。git log -S 显示最后一次改动 是 516d781(Release v1.2.0),此后再无引用。 它覆盖的正是 AOT 融合最核心的回归面:rope / swiglu / gelu / softmax / matmul / matmul+列归约 / norm / reduce_consts / fold v1(rowmax/rowsum/ softmax_denom)/ fold attn(plain/causal/alibi/doc/alibidoc × 5 形状)/ 未扫描表达式硬报错。 改动:CMakeLists 紧邻 expr_gpu_test 处 `nn_add_executable(fused_gpu_test ...)` + `list(APPEND NN_TEST_TARGETS fused_gpu_test)`;文档当前数字 20 → 21。 实测:编译零告警、`ALL PASS`、退出码 0;ctest 21/21 全绿。 仍存在的缺口(如实记录,本轮不修):该文件 0 处 f16 引用 ⇒ 覆盖 V0(全 f32) 逐形态路径,**不覆盖 V1 运行期精度分派**。V1 目前只有 7 个真实 f16 负载与样例 第二形态在压测,无逐形态数值断言。 验收:build 零告警;ctest 21/21;注册表 SHA256 仍 3D47775F…84D(逐字节不变); CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变。
起因:想量"V1 运行期精度分派的逐形态覆盖",用 NN_PREC_TRACE=1 跑 6 个 GPU 测试 → 得到 0 条 `#x`、只有 4 条 `#a`。 **对照实验救了这一轮**:跑已知必然走 V1 的库外样例(它含 rparam ⇒ native16 结构谓词不通过)——**样例也一行都不打** ⇒ 是测量工具瞎,不是"V1 没被用"。 根因:`compute_gpu_engine.hpp::find_prec_variant_` 的 `[prec][alu-hit]` 打印**只写在 V2(`#a`)分支**,V1(`#x`)与 V0 分支都静默返回。 改动: · V1 分支补同款日志(V0 默认路径仍不记——f32 热路径逐调用打日志会淹没 输出)。现规则:**变体选择事件(V1/V2)一律打日志**; · `scan_exprs` 的来源 dump 补 `[scan][both]`(锚点∩dry-run 交集,84 条里 38 条在此)——否则"某结构由哪个生成器负责、运行时是否按 V1 分派"无法从 dump 回答(V1 覆盖的 20 条里 18 条在交集中)。 修正后的实测(单跑 `f16_precision_test` 即够):12040 次分派、24 条 distinct = **20 条 V1(`#x`)+ 4 条 V2(`#a`)**;V1 的 20 条**覆盖全部四个生成器族** (元素 9 / 归约 6 / matmul 4 / fold 1),且该测试用 CHECK_NEAR 容差断言。 ⇒ **更正上一轮(A5)的错误结论**:"V1 无逐形态数值断言"**不成立**。`fused_gpu_test` 缺的是"**本套件的** rope/swiglu/gelu/softmax/fold-attn 形状的 f16 变体",不是 "V1 无覆盖"。history 与 AGENTS.md §7 均已更正。 验收:build 零告警;ctest 21/21;注册表 SHA256 仍 3D47775F…84D(逐字节不变); CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变。
对 feat/fused-runtime-prec-dispatch 相对 main 的全部改动做完整代码审核 (简化/规范化/指针与内存/逻辑),本轮修掉的真缺陷(非风格问题): 1. native16 谓词与 key 错配(**静默错值**):expr_prec_sig_native16 含常量值 判据(0<|c|<6e-8 在 f16 下 flush-to-0),而步骤①让常量值退出 key ⇒ 生成期 用"生成那一份时的常量"判定、运行期按位图选 #a,同结构换一组常量即被套上会 flush 的 native16 shader。修:选 #a 前用**真实实例的 spec 重跑同一谓词** (find_prec_variant_ 改收 const ExprSpec&)。 2. [prec][alu-hit] 先打后查 + 双重打点:V1 分支在 find_fused 返回前就打印 (miss 也报 hit),且 supports_expr_precision_variant(每 op 前置查询)也打 ⇒ 计数翻倍(实测 12040 行 = 6020 次真实分派 × 2)。修:确认命中后才打、 只由真正 dispatch 的三个入口传 log_hit;miss 打 [prec][fallback]。 3. push constant 固定头长度两侧各推一遍(创建侧 vs 写入侧入参来源不同): 降为**注册时算一次 + 存 map**(fused_pc_base_fixed_),写入侧只读。 4. in_rd 用子串替换拼 f16 视图名:找不到前缀时静默保留 f32 视图名 ⇒ 把 2B 缓冲当 float 读。改为显式拼 b<i>[…] / b<i>h[…]。 5. nn_enable_gpu_fusion 的 include 顺序不成立:普通 target_include_directories 对重复项保留首次出现位置,库内 GPU 循环先加了 generated ⇒ 追加的 NNF_OUT_DIR 反排在后面。改用 **BEFORE**。 6. 收集器不继承目标的编译条件(NN_HAS_VULKAN/私有 include/自定义宏)⇒ 使用者 调用点静默消失。改为把目标的 INCLUDE_DIRECTORIES/COMPILE_DEFINITIONS/ COMPILE_OPTIONS 用生成器表达式传给收集器;MAIN 未匹配即 FATAL_ERROR。 7. 模型 pass 失败只 warning(它贡献 25/84 条结构)⇒ 改为**失败即构建失败**, 与 dry-run 的 fail-fast 一致;NN_SCAN_NO_DRYRUN 同时门控模型 pass(与文档语义对齐)。 8. 两条"永远通过"的测试:fold tri_skip 进 key 用 Causal vs Plain 比 key(body 本就不同)→ 改为只翻该字段;expr_fused_key_test 里与 expr_matmul_test 逐字 重复的 mm_trans 位编码断言删掉一份。 9. read_spv 不校验 tellg()(失败返回 -1 → size_t(-1)/4 请求 ~4GB)+ 忽略截断读。 删除死物:prec_manifest 形参、ExprRegistry::contains、fusion_anchor_touch (实测删除后库外样例的 2 条自定义结构仍被锚点登记:registry 84 → 86 不变)、 std::exit(1)、scan_exprs 里重复 4 次的 matmul trans 登记、AGENTS 里 4 处有争议 的 ctest 19/19。 简化/去重(消掉"同一份知识写在两处"的同步点):glsl_vp_slot() 取代 6 处逐字 重复的 vp 槽累计;scan_register<E>() 取代 3 个 dsl 入口各写一遍的 锚点+折叠+校验+登记;is_fusable/fused_vec_width_for 取代三个循环各写一遍的 判据;expr_variant_out_f16() 取代 4 处输出精度选择;EXPR_PREC_ALU_SUFFIX 取代 3 处硬编码 "#a";tool_env_flag() 取代两份 _dupenv_s 样板;删除 f16 双 float16_t() 冗余转换与 GroupedReduce 在分派模式下的重复三元。 检测口径收紧:expr_prec_sig_in_f16 先与 EXPR_PREC_SIG_INPUT_MASK 相与(否则 哨兵 0x00020000 的 bit17 被读成"第 17 个输入是 f16");分组归约辅助函数在 matmul/归约生成器也发射(此前只有 elementwise 发,含该视图的 spec 会生成未声明 函数);--list-backends 排序(铁律 8);分发查询改用 reg.keys 而非重建集合。 文档纠偏:AGENTS §7/§12、docs/development/02/05/18、docs/usage/03 中"运行期 永不 miss""[prec][miss] 已删除""bin v9 变体段""gen_fused 产物""ctest 19" "12040 次分派"等表述统一为当前事实;05 §12.7 改为历史机制(标注已删除)、 §12.10 ③ 标注为 V0 时代口径;history.md 补本轮条目与 19/20/21 计数口径来源。 验收(本机 Debug+Ninja+clang):build 零告警(-Werror);ctest 21/21; NN_BIND_DEBUG=1 ctest 全绿;CPU 字节锚 6f8849f14da23110 与 GPU dev2 锚 8ef51b2927253c50 逐位不变(GLSL 文本改动全部数值等价);库外样例端到端值不变 (0.6712035 / 0.6712036 / 0.6708984 / 1.2246094);结构数 84(例 86)不变; NN_PREC_TRACE 12040 → 6022 行、distinct 仍 24(20 #x + 4 #a); L2-VIOLATIONS: 0;doc_align_audit A/D/E/F 可行动项 0。 教训:9 条真缺陷里 4 条同形——同一份知识写在两处、靠注释维持同步(谓词 vs key、 创建侧 vs 写入侧 PC 长度、f16 视图名 vs 子串替换、目标编译条件 vs 收集器); 另两条是"测量/断言必须先证明它能失败"的延续(round 15/19 已各记一次)。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
融合:精度签名从构建期枚举 → 运行期参数;库自带
nn_enable_gpu_fusion解决什么问题
原设计里精度签名(哪些输入/输出是 f16)是 shader 的「身份」——每个
(结构, 签名)一份 shader(key#<sighex>)。而签名由运行期张量精度决定、类型层面推不出来,于是只能靠执行发现:profile_f32/profile_f16两遍 + 模型级配置矩阵两遍;NN_PREC_TRACE=1的[prec][miss]手工收集,写进tools/prec_backfill.txt,再重跑构建;后果:改一处 Layer 表达式可能要在另一个文件里补一行回填——"写
dsl::compute即可"不成立。做法:签名从「身份」降级为「参数」
新增 V1「运行期精度分派」 shader(键
key#x):对每个输入/输出各声明 f32 +float16_t双视图(同一VkBuffer绑两次,只解引用与真实元素类型一致的那一个),加载/存储处按 push constantuint prec(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支。关键性质:索引数学逐字不变(两种视图共用同一逻辑下标)⇒ 与旧带类型变体数值等价。V1 默认对每个结构都尝试生成 ⇒ 构建期不再需要"枚举签名"这件事。
key#a)改由结构谓词判定生成(代入"全输入 f16 + 输出 f16"常量签名)。⚠ 该谓词含常量值判据(0 < |c| < 6e-8在 f16 下 flush-to-0),而常量值已不进 key ⇒ 运行期选#a前必须用真实实例的 spec 重跑同一谓词(本轮审核修复项,见下);key#a→key#x→ 基类边界 cast。V1 在"生成被[skip]/NN_SCAN_NO_DISPATCH=1/ 设备无 16bit 存储"时会 miss 并落到 cast([prec][miss]保留为通用诊断);实测 f16 融合路径 miss = 0。顺带删除
tools/prec_backfill.txt[prec][miss]保留为通用诊断)supports_expr_precision_variant的 (key,sig) 探测eval_expr_impl同源的选路ExprRegistry::variants/contains_variant/write_registry/read_registry/expr_specs.bingen_fused并入scan_exprs)库自带能力
nn_enable_gpu_fusion收集器 = 库内收集逻辑(
tools/scan_exprs.cpp)+ 本目标的全部 TU(-DNN_EXPR_SCAN编译,令使用者的dsl::compute调用点经FusedAnchor自登记),产物目录只作该目标的私有 include 目录。顺带修一个 header-only 缺陷:
GpuBuffer::~GpuBuffer()类外定义缺inline→ 任何 ≥2 个 TU 的程序链接期duplicate symbol(样例两 TU 复现;此前未暴露,因为库自身目标都是单 TU)。怎么验证的(全部实测)
f16 路径(GPT f16 训练,
NN_PREC_TRACE=1):[prec][miss]=0、边界 cast 归因表为空 ⇒ 全程命中预生成 shader,零 cast 回退。V1 覆盖(
NN_PREC_TRACE=1 f16_precision_test):6022 条命中日志 / 24 条 distinct 结构 = 20 条 V1(#x)+ 4 条 V2(#a),V1 覆盖全部四个生成器族(元素 9 / 归约 6 / matmul 4 / fold 1),且该测试用CHECK_NEAR容差断言。⚠ 本 PR 审核前该数字是 12040 —— 那是supports_expr_precision_variant(查询)也打日志造成的 2× 虚高,本轮已修(只由真正 dispatch 的入口、且确认命中后才打)。f16 扫描遍的价值(决定删除它):
端到端(A1,库内):在 Layer 头里新写一个 dry-run 覆盖不到的结构 → 锚点独有 13 → 14、结构 84 → 85、注册表新增
key/key#x/key#a,其他文件零改动。端到端(A2,库外 / 下游自定义层形态):
已注册为常驻 ctest(
fusion_custom_layer_example,无 Vulkan 设备时退出码 77 = skip)。注册表规模
代码审核轮(round 20,commit
14892f5)对相对
main的全部改动做完整审核(简化 / 规范化 / 指针与内存 / 逻辑)。修掉的真缺陷:#a。改用真实实例的 spec 重跑谓词。[prec][alu-hit]先打后查 + 双重打点:miss 也报 hit;计数 12040 → 6022(真实分派数)。miss 另打[prec][fallback]。in_rd用子串替换拼 f16 视图名:找不到前缀时静默保留 f32 视图名 ⇒ 把 2B 缓冲当 float 读 → 改为显式拼两个视图名。nn_enable_gpu_fusion的 include 顺序并不成立(普通 append 对重复项保留首次位置)→BEFORE。NN_HAS_VULKAN/ 私有 include / 自定义宏 → 调用点静默消失)→ 用生成器表达式继承;MAIN未匹配即FATAL_ERROR。fold tri_skip 进 key用 Causal vs Plain 比 key(body 本就不同)→ 改为只翻该字段;删除与expr_matmul_test逐字重复的 mm_trans 位编码断言。read_spv不校验tellg()(失败返回 -1 →size_t(-1)/4请求 ~4GB)+ 忽略截断读。消除"同一份知识写在两处"的同步点(9 条里有 4 条同形):
glsl_vp_slot()(6 处→1)、scan_register<E>()(3 个 dsl 入口→1)、is_fusable/fused_vec_width_for(3 个循环→1)、expr_variant_out_f16()(4 处→1)、EXPR_PREC_ALU_SUFFIX(3 处硬编码"#a"→1)、tool_env_flag()(两份_dupenv_s样板→1);另删除 5 处死物(prec_manifest、ExprRegistry::contains、fusion_anchor_touch、std::exit(1)、重复 4 次的 matmul trans 登记)。实测验收(本机 Debug+Ninja+clang):build 零告警;ctest 21/21;
NN_BIND_DEBUG=1 ctest全绿;CPU/GPU 字节锚逐位不变(GLSL 文本改动全部数值等价);结构数 84(样例 86)不变;L2-VIOLATIONS: 0;doc_align_auditA/D/E/F 可行动项 0。A vs B(被否决的备选)
方案 B = "同质化 + 边界 cast"(数值与混合变体等价、改动更小)。实测 GPT f16 1 epoch /
tinystories_smoke/ 40HX:4.1s(B)vs 4.0/3.9s(变体路径)≈ +4%,且 B 会给含 f16 输入的表达式造 f32 临时量、推高 f16 峰值显存(f16 的核心卖点)。故选 A。已知局限
float16_t视图 ⇒ 需要设备的 16bit storage;无该特性的设备只注册 V0(全 f32)。此时设备本来也无法持有 f16 张量,故无回退损失。fused_gpu_test(逐形态 GPU 对拍)覆盖口径是 V0(全 f32);V1/V2 的覆盖在f16_precision_test(见上)。该文件 0 处 f16 引用,是已知的覆盖缺口(不是"V1 无覆盖")。nn_enable_gpu_fusion用库内收集逻辑 + 目标 TU 覆盖它们,但自定义层若命中未覆盖结构会在 GPU 闭合世界报错(错误信息带key,可直接定位)。下一步的兜底是给该函数加COLLECT <file>钩子。