Repository navigation
融合 AOT:结构脱离运行期值(步骤①②③ + 自登记前置) - #14
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`(数据产物,与本次文档同步无关)。
EthanPeng-2048
marked this pull request as ready for review
October 1, 2026 09:25
EthanPeng-2048
added a commit
that referenced
this pull request
Oct 1, 2026
…spatch 融合:精度签名降级为运行期参数 + nn_enable_gpu_fusion(叠在 #14 之上)
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.
融合 AOT:结构脱离运行期值 + 自登记锚点 + 构建期单步
这个 PR 解决什么
以前"某个内联表达式能被 GPU 融合"这件事,依赖手写的覆盖清单(
scan_exprs里逐层 dry-run 块 + 注意力 fold 的显式登记)。漏登记 = GPU 闭合世界硬报错。
本 PR 把前半句反了过来:结构由表达式类型决定,写在 Layer 里的
dsl::compute(...)编译进来即被登记,不再需要有人记得填清单。关键改动
① key 只描述结构(自登记成立的前提)
常量池值、
MatmulSpec.transA/transB、分组归约 R 三者退出expr_spec_key(分别改走 push constant
c<i>/ PCmm_trans/ 视图参数vp)。契约由新增的
src/expr_fused_key_test.cpp锁定(并入expr_cpu_test,ctest 目标数不变):符号实例 key ≡ 真实实例 key(8 组)、常量值/转置/R 不进 key、
mm_trans位编码、sum≠max 必须区分。任何"重新让运行期值决定结构"的改动都会立刻红。
②
FusedAnchor<Expr>自登记(4...)在静态初始化期按表达式类型登记结构——覆盖"编译进来但任何驱动都没执行到"
的调用点。关掉锚点(
-DNN_SCAN_NO_ANCHOR):结构 84 → 71。运行时证据(不是理论):关掉锚点后
text_infer(KV-cache 增量解码)在 GPU硬报错:
该 key =
mm=1 + rparam×1,即forward_step的注意力打分链——手写 dry-run 从未覆盖的路径;打开锚点后同一命令正常(8 tokens / 0.2s)。
③ 签名仍需执行期发现(锚点给不出来)
"哪些输入是 f16"由运行期张量精度决定,类型层面推不出:现有 86 个签名变体里
44 个是混合签名。因此保留 per-layer dry-run + 模型级配置矩阵(5 个 shipped
模型 × f32/f16),并用
NN_PREC_TRACE+tools/prec_backfill.txt兜底。实测 7 个 f16 负载
[prec][miss]全 0(mnist mlp/cnn/transformer、mem_probe默认/
--doc-mask、text_train GPT/RAPT)。④ 手写扫描 −223 行
删掉 13 个已被模型 pass 覆盖的 per-layer dry-run 块。判据是两条硬证据:
结构集合逐字节不变 + 运行时 miss 集合不变(跨 MNIST/CNN/文本三路验证)。
保留的都是整模型跑不到的来源(优化器/损失/RoPE 增量/MSA 掩码组合/显式测试登记)。
⑤ 构建期收为单步
gen_fused并入scan_exprs(tools/fused_generate.hpp),删除expr_specs.bin序列化(
expr_registry.hpp388 → 88 行)。行为保持的硬证据:在 HEAD 的git worktree里用旧代码独立构建,产出的fused_registry.hpp与新流程SHA256 逐字节相同。
⑥ 可诊断性:闭合世界 4 处硬报错现在都打印
key=。怎么验证
回归开关(只影响登记集合、不改语义):
-DNN_SCAN_NO_ANCHOR、-DNN_SCAN_NO_DRYRUN。未做 / 不做的
只由
make_fold_attn_o构造、不走dsl::compute钩子——提模板参数只把运行期枚举换成模板分派,收益为负。
scan_exprs/NN_EXPR_SCAN:实测证伪。签名必须执行期产生(见 ③),且优化器/损失/增量推理仍需独立驱动;剩余手写块若删除,其签名要靠回填清单逐条
补回,行数反而更多。现状 = "锚点管结构 + 多来源管签名 + 构建期台账 + run-only 回填"。
已知的非本 PR 问题
--f16比同配置 GPT 慢约 16×(61.4s vs 3.9s):main(586cfb2)独立worktree 实测 60.1s ⇒ 既有行为,见 issue RAPT GPU 训练比同配置 GPT 慢约 16×(既有行为,待定位) #15。