From 3cf591c6cb36f055ebfacedab7763fe6f8c76b03 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 14:21:31 +0800 Subject: [PATCH 01/17] =?UTF-8?q?=E8=9E=8D=E5=90=88=20AOT=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E2=91=A0=EF=BC=9A=E5=B8=B8=E9=87=8F=E6=B1=A0=E5=80=BC?= =?UTF-8?q?=E9=80=80=E5=87=BA=20expr=5Fspec=5Fkey=EF=BC=88key=20=E5=8F=AA?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E7=BB=93=E6=9E=84=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit key 的语义收敛为"纯结构":常量池仍喂**个数**(决定 push constant 布局), 不再喂**值**。glsl_gen 本就把 consts 声明进 push constant、操作数发射成 `c`(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**, 价值在语义收敛与后续步骤的前提)。 --- docs/development/02-operator-fusion.md | 2 +- docs/development/03-ir-optimization.md | 14 ++++++++++++++ include/neuralnet.cpp/expr_spec.hpp | 13 ++++++++----- tools/prec_backfill.txt | 2 +- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index a2688de9..68e0c257 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -257,7 +257,7 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba ### 二期关键教训(改融合/IR 代码前必读) -1. **运行时值禁进表达式常量池**(进 `expr_spec_key` 会破坏闭合世界):运行时标量一律用 **`rparam`** 承载——注意力 `scale_`(1/√d_k)折进 Q:`dsl::compute_into(engine, leaf(Q) * rparam(scale_), Q)`(backward 的 grad_Q 相应补乘),使表达式结构与 d_k 无关;稀疏 CE 的 `inv_num_valid` 用 `dsl::rparam(inv_num_valid)` 尾链。rparam 值不进 key,同结构不同值共享一个 shader。 +1. **运行时值禁进表达式常量池**:常量池的值虽已不进 `expr_spec_key`(只喂个数,`glsl_gen` 经 push constant `c` 读取),但**个数**仍进 key,而 canonicalize 会**按值去重**常量——把运行期标量塞进常量池会让同一结构的 key 随取值漂移,等于每个取值组合一份 shader。运行时标量一律用 **`rparam`** 承载——注意力 `scale_`(1/√d_k)折进 Q:`dsl::compute_into(engine, leaf(Q) * rparam(scale_), Q)`(backward 的 grad_Q 相应补乘),使表达式结构与 d_k 无关;稀疏 CE 的 `inv_num_valid` 用 `dsl::rparam(inv_num_valid)` 尾链。rparam 值不进 key、个数固定,同结构不同值共享一个 shader。 2. **BatchCol 视图要求 `(1, BH*seq)`**(doc_ids 按 (b,h) 块重复),`(1, batch*seq)` 会越界。 3. **RowGather 主输入行数≠网格行数**(loss_vec 在 (1,N) 读 (C,N) logits),校验只查 cols。 4. `gen_fused` `emit_spec` 的 ±inf 常量必须用 `numeric_limits`。 diff --git a/docs/development/03-ir-optimization.md b/docs/development/03-ir-optimization.md index bfe71805..fe9b068a 100644 --- a/docs/development/03-ir-optimization.md +++ b/docs/development/03-ir-optimization.md @@ -97,6 +97,20 @@ expr_spec_key(spec) ≡ expr_spec_key(canonicalize_expr_spec(spec)) 即 **key 一定在 canonical IR 上计算**。scan(构建期折叠时)与 runtime(运行时折叠时)必须都先 canonicalize 再算 key,保证两端一致。 +**key 只描述"结构",运行期数值一律不进 key**(进了会让每个取值组合各编译一份**逐字节相同**的 shader,并让"结构 = 表达式类型"不成立): + +| 字段 | 进 key | 运行期载体 | +|------|--------|-----------| +| 指令 / 视图 / 寄存器数 | ✅ | — | +| 常量池**个数** | ✅(决定 push constant 布局) | — | +| 常量池**值** | ❌ | push constant `c`(`ExprOperandKind::Const` 发射成 `c`) | +| `rparams` 个数 / 值 | 个数 ✅ / 值 ❌ | push constant `rp` | +| 视图形状参数(RowMod/RotateHalf/BatchMod/BatchCol/RowAccess 的 param) | ❌ | push constant `vp` | +| `matmul.k` / `matmul.batch` | ❌ | push constant `mm_k` / `mm_batch` | +| `matmul.transA/transB`、`a_input/b_input` | ✅(**待收敛**:目标是把旋转布局改成运行期 operand layout) | — | +| fold 的 `k` / `vec_state_len` | ❌ | push constant | +| 分组归约视图的 `R` | ✅(**待收敛**:目标改为运行期循环) | — | + ### 4.2 确定性铁律 - **pass 遍历顺序必须固定**(如始终按指令序从前到后、视图按输入序、常量按出现序)。 diff --git a/include/neuralnet.cpp/expr_spec.hpp b/include/neuralnet.cpp/expr_spec.hpp index 4e2b07ce..4a81b914 100644 --- a/include/neuralnet.cpp/expr_spec.hpp +++ b/include/neuralnet.cpp/expr_spec.hpp @@ -553,9 +553,9 @@ struct ExprSpec } // ── 规范结构 key(AOT 收集/匹配的单一依据)────────────────────────────── -// 把 ExprSpec 的**结构**(指令、视图、常量、寄存器数;不含输入张量)确定性地 -// 哈希成 16 位十六进制字符串。同一结构跨构建/跨调用恒得同 key,不同结构 -// 以极大概率不同。用于: +// 把 ExprSpec 的**结构**(指令、视图、常量池**个数**、寄存器数;不含输入张量、 +// 也不含常量**值**)确定性地哈希成 16 位十六进制字符串。同一结构跨构建/跨调用 +// 恒得同 key,不同结构以极大概率不同。用于: // 1. 构建期 scan_exprs 注册表去重(identical 表达式只合成一个 shader) // 2. gen_fused 产物命名(fused_)与嵌入注册 // 3. 运行时 eval_expr 折叠内联表达式 → key → 查预编译 shader(闭合世界) @@ -599,9 +599,12 @@ struct ExprSpec feed_u32(v.param2); } } + // 常量池:只喂**个数**(决定 push constant 布局),不喂值——常量值本身是 + // 运行时数据(glsl_gen 把 consts 声明进 push constant、操作数发射成 + // `c`;见 expr_glsl_gen.hpp 的 PC 声明与 ExprOperandKind::Const), + // 同结构不同常量值共享一个融合 shader。与 RParam 同处理。 + // *** 常量池值进 key 会让每个取值组合都编译一份 **逐字节相同** 的 shader *** feed_u32(static_cast(s.consts.size())); - for (const auto& c : s.consts) - feed(&c, sizeof(c)); // 运行时标量参数(RParam):只喂**个数**(结构),不喂值——值本身是 // 运行时数据(如优化器的 lr/eps/β),不进 key;同结构不同值的表达式 // 共享一个融合 shader(glsl_gen 把 rparams 作为 push constant 读取, diff --git a/tools/prec_backfill.txt b/tools/prec_backfill.txt index 25f118da..cbbce254 100644 --- a/tools/prec_backfill.txt +++ b/tools/prec_backfill.txt @@ -27,4 +27,4 @@ c0006fc8ea6a0e6b 0002 # 注意力 forward 单 fold kernel(Phase D2:fold 带类型变体;3 输入全 f16、 # 输出 f16 = Q/K/V 直读 + O 落回,算术/状态进位全 f32) -9396eb8a1f95352c 10007 +66b03ef936bb97c7 10007 From b960450ef6f4c499afd68aa945d60c5350734902 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 14:35:02 +0800 Subject: [PATCH 02/17] =?UTF-8?q?=E8=9E=8D=E5=90=88=20AOT=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E2=91=A1=EF=BC=9Amatmul=20=E8=BD=AC=E7=BD=AE=E9=80=80?= =?UTF-8?q?=E5=87=BA=20expr=5Fspec=5Fkey=EF=BC=88=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=9C=9F=20operand=20layout=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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`(内层无分支)。 --- docs/development/02-operator-fusion.md | 15 ++- docs/development/03-ir-optimization.md | 4 +- .../backend/compute_vk_backend.hpp | 38 +++++--- include/neuralnet.cpp/compute_gpu_engine.hpp | 12 ++- include/neuralnet.cpp/expr_glsl_gen.hpp | 93 ++++++++++++------- include/neuralnet.cpp/expr_spec.hpp | 30 ++++-- src/expr_matmul_test.cpp | 27 +++++- tools/prec_backfill.txt | 4 +- 8 files changed, 160 insertions(+), 63 deletions(-) diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index 68e0c257..8328e1f5 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -264,7 +264,7 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba 5. **matmul + 列归约已支持**:`generate_glsl_reduce` 列归约分支按元素分解 batch(`batch = row/m_per`,列归约遍历全部 `rows = batch*m_per` 行,与 CPU `matmul_out` 逐列归约语义一致),`gen_fused` 不跳过该形态(扫描到的 spec 全部生成);`expr_cpu_test::col_max(matmul)`(独立标量参考,batch=2)+ `expr_gpu_test::col_max(matmul)` 广播/归约向量/batch=2 对拍锁死(err≈1e-7)。 6. **PS 删大文件段行号易漂移**、`-replace` 多行静默失败——先 read 再 edit,删前 `git diff` 核对。 7. `dispatch_compute`(`compute_vk_backend.hpp` 的融合 dispatch 分发函数)有多处调用点——改签名或删除时按调用点逐一核对重建。 -8. **IR 扩展**:MatmulSpec.batch(不进 key,dispatch z)、Row/Col/Batch 操作数(6/7/8)、RowGather(9)/BatchMod(10)/BatchCol(11);注意力 forward 现为单 fold kernel(`FoldSpec`,掩码 `AttnScoreMask::mask_kind()` × 位置偏置 `PositionEncoder::has_score_bias()` 两个正交入参),bwd=掩码 → 位置偏置(独立一步)→ softmax 的 R/X 表达式+3 个 `batched_matmul`(m/l/W 表达式+bm(W,V_t) 的 S7 forward 结构已删);CE 稠密 `denom=col_sum(exp(logits-cb(col_max)))`,稀疏 grad/loss_vec 用 Row+RowGather。 +8. **IR 扩展**:MatmulSpec.batch(不进 key,dispatch z)、MatmulSpec.transA/transB(不进 key,运行期 operand layout → PC `mm_trans`;见"运行期 operand layout"节)、Row/Col/Batch 操作数(6/7/8)、RowGather(9)/BatchMod(10)/BatchCol(11);注意力 forward 现为单 fold kernel(`FoldSpec`,掩码 `AttnScoreMask::mask_kind()` × 位置偏置 `PositionEncoder::has_score_bias()` 两个正交入参),bwd=掩码 → 位置偏置(独立一步)→ softmax 的 R/X 表达式+3 个 `batched_matmul`(m/l/W 表达式+bm(W,V_t) 的 S7 forward 结构已删);CE 稠密 `denom=col_sum(exp(logits-cb(col_max)))`,稀疏 grad/loss_vec 用 Row+RowGather。 ⚠ `dsl::row()` 是"批内行号",其分解来自**同一 spec 里的 matmul 段**;把原本融在 matmul 表达式里的项拆成独立一步后 batch 退化为 1、`row()` 变全局行号 → 静默错值。需要批内位置时用 `(rows,1)` 行表 + `dsl::row_broadcast`(`AlibiPositionEncoder::apply_score_bias` 即此写法)。 9. **IR-D 现只有 `GlslEmitter` 一个注册后端**(`gen_fused --list-backends` 可列);`cpu_emitter.hpp` 不存在。 @@ -317,6 +317,19 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba **效果(当前)**:闭合世界不随 d_k 穷举(RoPE 的 4×dk 去重为同构);`expr_gpu_test` 用 d_k ∈ {16, 40, 64, 96, 128}(含非 2 的幂)全部命中同一 shader,err≤1.2e-7。零额外显存。matmul 段的 `k`/`batch` 与 `rparam` 运行时标量采用同样的形状无关处理。 +### 运行期 operand layout:matmul 转置(2026-10-01) + +`MatmulSpec.transA/transB` 原先按"结构"进 key,同一表达式的 4 种转置组合各占一份 shader。而 DSL 的 `dsl::matmul(A,B,transA,transB)` 在**类型层面看不出转置差异**(四个组合同类型)——这正是"结构 = 表达式类型"(AOT 按类型实例化自登记的前提)的障碍。 + +处理方式与形状参数同源(把结构信息从 key 拿到运行时): + +1. `expr_spec_key` 剔除顶层 `matmul.transA/transB`(`a_input/b_input` 仍进 key);`expr_spec_runtime_matmul_trans` 把两位打包成 push constant `mm_trans`(bit0=transA,bit1=transB)。 +2. `generate_glsl_matmul` 的 `load_tiles` **两条加载路径都发射**,外层套 `if ((mm_trans & 1u/2u) != 0u)`(分支按 dispatch 统一 = uniform,预测代价≈0);`generate_glsl_reduce` 的内联点积把两种索引折算成 `base + kk*stride`,循环体内无分支。 +3. `run_fused_gpu` 的 PC 形态随之扩 1 个 uint(matmul 5→6、matmul+归约 6→7;fold 形态不在此列)——**创建侧 `pc_base` 与写入侧必须逐形态同改**(见 `compute_vk_backend.hpp` 的告警)。 +4. ⚠ **双域 fold 自带的 matmul 段不在此列**:fold 的转置仍进 key(生成期定死),`mm_trans` 恒传 0。 + +**效果**:扫描结构 77 → 72(5 个"仅转置不同"的结构合并;`dsl::matmul` 的 4 组合由 4 份 shader 收敛为 1 份),并让 matmul 段成为"纯类型决定"的结构——AOT 自登记的前提之一。 + --- ## 显存收益与风险 diff --git a/docs/development/03-ir-optimization.md b/docs/development/03-ir-optimization.md index fe9b068a..a0acaf99 100644 --- a/docs/development/03-ir-optimization.md +++ b/docs/development/03-ir-optimization.md @@ -107,7 +107,9 @@ expr_spec_key(spec) ≡ expr_spec_key(canonicalize_expr_spec(spec)) | `rparams` 个数 / 值 | 个数 ✅ / 值 ❌ | push constant `rp` | | 视图形状参数(RowMod/RotateHalf/BatchMod/BatchCol/RowAccess 的 param) | ❌ | push constant `vp` | | `matmul.k` / `matmul.batch` | ❌ | push constant `mm_k` / `mm_batch` | -| `matmul.transA/transB`、`a_input/b_input` | ✅(**待收敛**:目标是把旋转布局改成运行期 operand layout) | — | +| `matmul.a_input/b_input`(输入槽位) | ✅ | — | +| `matmul.transA/transB` | ❌ | push constant `mm_trans`(bit0=transA / bit1=transB;shader 内两条加载路径 + uniform 分支,同一结构覆盖 4 种转置组合) | +| fold 段 matmul 的 transA/transB | ✅(fold 的 mm 段生成期定死,**尚未收敛**) | — | | fold 的 `k` / `vec_state_len` | ❌ | push constant | | 分组归约视图的 `R` | ✅(**待收敛**:目标改为运行期循环) | — | diff --git a/include/neuralnet.cpp/backend/compute_vk_backend.hpp b/include/neuralnet.cpp/backend/compute_vk_backend.hpp index 298b7c89..cba9a060 100644 --- a/include/neuralnet.cpp/backend/compute_vk_backend.hpp +++ b/include/neuralnet.cpp/backend/compute_vk_backend.hpp @@ -1719,8 +1719,8 @@ class GpuBackend const std::uint32_t num_bindings = static_cast(fs.spec.views.size()) + 1; // 输入 + 输出 // 归约 kernel 的 push constants 多 uint rows + uint vector_out; - // matmul 融合 kernel 多 uint rows + uint mm_k + uint mm_batch; - // matmul+归约组合再多 uint mm_k + uint mm_batch(6 槽); + // matmul 融合 kernel 多 uint rows + uint mm_k + uint mm_batch + uint mm_trans; + // matmul+归约组合再多 uint mm_k + uint mm_batch + uint mm_trans(7 槽); // fold v1(标量域)= count, cols, rows, vector_out, fold_k(5 槽)—— // **必须与 run_fused_gpu 的 pc_base 逐形态一致**:漏分支会让 // range 少算,vkCmdPushConstants 超 range 部分被驱动丢弃 → @@ -1731,8 +1731,10 @@ class GpuBackend const std::uint32_t pc_base = (fs.spec.fold && fs.spec.fold->matmul) ? 7u // fold+mm 7 槽 : (fs.spec.fold) ? 5u - : (fs.reduce_axis >= 0 && fs.has_matmul) ? 6u - : ((fs.reduce_axis >= 0 || fs.has_matmul) ? 5u : 2u); + : (fs.reduce_axis >= 0 && fs.has_matmul) ? 7u + : (fs.reduce_axis >= 0) ? 4u + : (fs.has_matmul) ? 6u + : 2u; const std::uint32_t pc_uints = pc_base + fs.view_param_count; const std::uint32_t pc_size = static_cast(pc_uints * sizeof(std::uint32_t) + @@ -3335,6 +3337,11 @@ class GpuBackend GpuTensor* output_override = nullptr, std::optional matmul_k = std::nullopt, std::uint32_t matmul_batch = 1, + // matmul 段转置布局(bit0=transA,bit1=transB):**运行期 operand layout, + // 不进 expr_spec_key** —— 同一结构覆盖 4 种转置组合。仅 matmul 形态 + // (matmul / matmul+归约)使用;fold 形态的 mm 段转置仍在 key 里 + // (生成期定死),此处忽略。 + std::uint32_t matmul_trans = 0u, // fold 收缩轴长度(fold shader 传入填 fold_k 槽;非 fold 传 nullopt) std::optional fold_k = std::nullopt, // 输出存储精度(Phase 2 in-kernel f16):true → 分配 f16 字节布局的 @@ -3492,20 +3499,21 @@ class GpuBackend // Push constants 布局与 glsl_gen.hpp 一致: // 逐元素: count, cols, [vp0..], c0.. // 归约: count, cols, rows, vector_out, [vp0..], c0.. - // matmul: count, cols, rows, mm_k, mm_batch, [vp0..], c0.. - // matmul+归约: count, cols, rows, vector_out, mm_k, mm_batch, [vp0..], c0.. + // matmul: count, cols, rows, mm_k, mm_batch, mm_trans, [vp0..], c0.. + // matmul+归约: count, cols, rows, vector_out, mm_k, mm_batch, mm_trans, + // [vp0..], c0.. // // ⚠ 固定头长度必须**逐形态**与生成器的 PC 声明一致:逐元素 2 / - // 逐元素+matmul 5 / 归约 4 / 归约+matmul 6 / fold 5 / fold+matmul 7。 + // 逐元素+matmul 6 / 归约 4 / 归约+matmul 7 / fold 5 / fold+matmul 7。 // 错一个 uint 即常量池整体后移 → shader 从错位处读常量,GPU 上带 // 常量的归约静默错值而 CPU 正常(实测把 select(cond,1,0) 的常量 // 读成垃圾)。 const std::uint32_t pc_base = (is_fold && matmul_k) ? 7u // fold+mm: …, fold_k, mm_k, mm_batch : (is_fold) ? 5u // fold: count,cols,rows,vector_out,fold_k - : (raxis >= 0 && has_mm) ? 6u // count, cols, rows, vector_out, mm_k, mm_batch + : (raxis >= 0 && has_mm) ? 7u // …, mm_k, mm_batch, mm_trans : (raxis >= 0) ? 4u // count, cols, rows, vector_out - : (has_mm) ? 5u // count, cols, rows, mm_k, mm_batch + : (has_mm) ? 6u // count, cols, rows, mm_k, mm_batch, mm_trans : 2u; // count, cols const std::uint32_t pc_uints = pc_base; std::vector pc( @@ -3527,17 +3535,21 @@ class GpuBackend sizeof(std::uint32_t)); if (has_mm) { - // matmul+归约:mm_k + mm_batch(形状参数,运行时填充) + // matmul+归约:mm_k + mm_batch + mm_trans(形状参数 + 运行期 + // operand layout,运行时填充) std::memcpy(pc.data() + 4 * sizeof(std::uint32_t), &*matmul_k, sizeof(std::uint32_t)); std::memcpy(pc.data() + 5 * sizeof(std::uint32_t), &matmul_batch, sizeof(std::uint32_t)); + std::memcpy(pc.data() + 6 * sizeof(std::uint32_t), &matmul_trans, + sizeof(std::uint32_t)); } } else if (has_mm) { - // matmul 融合:rows + mm_k + mm_batch(形状参数,运行时按实际 spec - // 填充 → 同结构不同 K/batch 共享一个融合 shader) + // matmul 融合:rows + mm_k + mm_batch + mm_trans(形状参数 + 运行期 + // operand layout,运行时按实际 spec 填充 → 同结构不同 K/batch/转置 + // 共享一个融合 shader) const std::uint32_t rows32 = static_cast(rows); std::memcpy(pc.data() + 2 * sizeof(std::uint32_t), &rows32, sizeof(std::uint32_t)); @@ -3545,6 +3557,8 @@ class GpuBackend sizeof(std::uint32_t)); std::memcpy(pc.data() + 4 * sizeof(std::uint32_t), &matmul_batch, sizeof(std::uint32_t)); + std::memcpy(pc.data() + 5 * sizeof(std::uint32_t), &matmul_trans, + sizeof(std::uint32_t)); } else if (is_fold) { diff --git a/include/neuralnet.cpp/compute_gpu_engine.hpp b/include/neuralnet.cpp/compute_gpu_engine.hpp index 1b56e917..7837333f 100644 --- a/include/neuralnet.cpp/compute_gpu_engine.hpp +++ b/include/neuralnet.cpp/compute_gpu_engine.hpp @@ -1417,9 +1417,11 @@ class GpuEngine final : public ComputeEngine ffs->key, gpu_inputs, raw_spec.consts, rows, cols, /*vector_out=*/false, fvp, raw_spec.rparams, /*output_override=*/nullptr, - // 双域 fold 自带 matmul 段的 k/batch(7 槽 PC 的 slot5/6) + // 双域 fold 自带 matmul 段的 k/batch(7 槽 PC 的 slot5/6); + // fold 的转置仍在 key 里(生成期定死)→ 此处恒 0(无该槽) nn::expr_spec_runtime_matmul_k(raw_spec), nn::expr_spec_runtime_matmul_batch(raw_spec), + /*matmul_trans=*/0u, nn::expr_spec_runtime_fold_k(raw_spec), fout_f16); if (!out) return std::unexpected(out.error()); // f16 输出:按 2B/元素分配后重贴 GpuTensorF16(同非 fold 路径) @@ -1466,6 +1468,7 @@ class GpuEngine final : public ComputeEngine /*vector_out=*/false, vp, spec.rparams, /*output_override=*/nullptr, nn::expr_spec_runtime_matmul_k(spec), nn::expr_spec_runtime_matmul_batch(spec), + nn::expr_spec_runtime_matmul_trans(spec), /*fold_k=*/std::nullopt, out_f16); if (!out) return std::unexpected(out.error()); if (out_f16) @@ -1581,7 +1584,9 @@ class GpuEngine final : public ComputeEngine fs->key, gpu_inputs, spec.consts, rows, cols, /*vector_out=*/true, vp, spec.rparams, /*output_override=*/nullptr, nn::expr_spec_runtime_matmul_k(spec), - nn::expr_spec_runtime_matmul_batch(spec), std::nullopt, out_f16); + nn::expr_spec_runtime_matmul_batch(spec), + nn::expr_spec_runtime_matmul_trans(spec), + std::nullopt, out_f16); if (!out) return std::unexpected(out.error()); if (out_f16) { @@ -1671,7 +1676,8 @@ class GpuEngine final : public ComputeEngine fs->key, gpu_inputs, spec.consts, rows, cols, /*vector_out=*/false, vp, spec.rparams, dst_override, nn::expr_spec_runtime_matmul_k(spec), - nn::expr_spec_runtime_matmul_batch(spec)); + nn::expr_spec_runtime_matmul_batch(spec), + nn::expr_spec_runtime_matmul_trans(spec)); if (!out) return std::unexpected(out.error()); // dst 原为 CPU staging 时,import 上传了新 buffer(结果在它上面) // → 用 upload 后的张量替换 dst,保证调用方看到更新后的数据 diff --git a/include/neuralnet.cpp/expr_glsl_gen.hpp b/include/neuralnet.cpp/expr_glsl_gen.hpp index fa39116d..06ebf697 100644 --- a/include/neuralnet.cpp/expr_glsl_gen.hpp +++ b/include/neuralnet.cpp/expr_glsl_gen.hpp @@ -266,12 +266,14 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) // (见 AGENTS §12 ⑥)。 // - 尾逐元素链编译为 eval_tail(mm, row, col) 函数(GLSL 内联零开销), // 写回时每个输出元素调用一次(Matmul 操作数 → mm,"虚拟寄存器 0")。 -// - transA/transB 是**结构**(进 key)→ 索引表达式硬编码进 shader; -// mm_k 是**形状参数**(不进 key)→ push constant,运行时填充 -// (同结构不同 K 共享一个融合 shader)。 +// - transA/transB 是**运行期 operand layout**(PC `mm_trans`,不进 key)→ +// 两条加载路径都发射、运行时经 uniform 分支选择;mm_k/mm_batch 是 +// **形状参数**(不进 key)→ push constant,运行时填充 +// (同结构不同 K/batch/转置共享一个融合 shader)。 // - 布局:bindings 输入 0..N-1 + 输出 N(A/B 就是其中的两个输入槽); // push constants: uint count, uint cols, uint rows, uint mm_k, -// uint mm_batch, [uint vp0..], [float c0..], [float rp0..]; +// uint mm_batch, uint mm_trans, [uint vp0..], [float c0..], +// [float rp0..]; // dispatch: (ceil(cols/BLOCK), ceil(rows/BLOCK), mm_batch)(见 // EXPR_MATMUL_TILE/BLOCK,后端 run_fused_gpu 与生成器共用)。 // - 归约指令(RowSum/RowMax/...)出现在尾链时属归约组合,由 @@ -284,8 +286,8 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) const std::size_t n_inputs = spec.views.size(); const std::uint32_t a_slot = mm.a_input; const std::uint32_t b_slot = mm.b_input; - const bool trA = (mm.transA != 0); - const bool trB = (mm.transB != 0); + // 注:mm.transA/transB **不在此处读取** —— 转置是运行期 operand layout + // (PC `mm_trans`),两条加载路径都发射、运行时经 uniform 分支选择。 // ── 带类型变体(Phase 2 in-kernel f16)─────────────────────────────── // sig==0 → 全 f32:生成的就是 f32 变体源码。sig!=0 → 对应槽 // 的缓冲区声明为 float16_t,**在全局加载处**统一转 f32(共享 tile / 内层 @@ -350,6 +352,7 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) L << " uint rows;\n"; L << " uint mm_k; // matmul 求和维度(形状参数,运行时填充)\n"; L << " uint mm_batch; // matmul 批量数(形状参数,运行时填充;dispatch z)\n"; + L << " uint mm_trans; // bit0=transA, bit1=transB(运行期 operand layout,不进 key)\n"; const std::uint32_t n_vp = expr_spec_runtime_view_param_count(spec); for (std::uint32_t i = 0; i < n_vp; ++i) L << " uint vp" << i << ";\n"; @@ -373,16 +376,22 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) // transA/transB 感知的全局内存加载表达式(batch:A/B 按 batch 垂直 // 切分,batch*m_per 为 A 行偏移 / batch*mm_k 为 B k 偏移;row/col 为 // **batch 内**坐标,block 偏移由调用方并入) - const auto a_load = [&](const std::string& row_e, const std::string& k_e) - { - const std::string raw = trA + // ── 全局内存加载表达式(batch:A/B 按 batch 垂直切分,batch*m_per 为 A + // 行偏移 / batch*mm_k 为 B k 偏移;row/col 为 **batch 内**坐标,block + // 偏移由调用方并入)。trans 由调用方按运行期 mm_trans 的两条分支分别传 + // 常量(true/false)——两条路径都发射,运行时经 uniform 分支选择 ── + const auto a_load = [&](const std::string& row_e, const std::string& k_e, + bool trans) + { + const std::string raw = trans ? "b" + std::to_string(a_slot) + "[((batch*mm_k + (" + k_e + "))*m_per + (" + row_e + "))]" : "b" + std::to_string(a_slot) + "[((batch*m_per + (" + row_e + "))*mm_k + (" + k_e + "))]"; return a_f16 ? ("float(" + raw + ")") : raw; }; - const auto b_load = [&](const std::string& col_e, const std::string& k_e) + const auto b_load = [&](const std::string& col_e, const std::string& k_e, + bool trans) { - const std::string raw = trB + const std::string raw = trans ? "b" + std::to_string(b_slot) + "[((batch*cols + (" + col_e + "))*mm_k + (" + k_e + "))]" : "b" + std::to_string(b_slot) + "[((batch*mm_k + (" + k_e + "))*cols + (" + col_e + "))]"; return b_f16 ? ("float(" + raw + ")") : raw; @@ -512,8 +521,9 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) // ── load_tiles:协作加载 A/B 分块(BK×B)→ 指定 stage。双缓冲流水的 // 发射端:前奏调 load_tiles(0,0),主循环内 if (t+1> 2][arT & 3u] = v;\n"; L << " }\n }\n"; } - if (trB) + L << " }\n"; + // B 的两条加载路径同样都发射,运行时按 mm_trans bit1 选择。 + L << " if ((mm_trans & 2u) != 0u) // transB: B 存 (N,K)\n"; + L << " {\n"; { // B 存 (N,K):沿 k 连续 → vec4 散写 4 个共享行 L << " const uint kBL = (e % " << BK4 << "u) * 4u;\n"; @@ -586,11 +602,11 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) L << " }\n else\n {\n"; L << " for (uint j = 0u; j < 4u; ++j)\n {\n"; L << " const float v = (bcG < cols && kBG + j < mm_k) ? " - << b_load("bcG", "kBG + j") << " : 0.0;\n"; + << b_load("bcG", "kBG + j", true) << " : 0.0;\n"; L << " Bsh[stage][kBL + j][nT >> 2][nT & 3u] = v;\n"; L << " }\n }\n"; } - else + L << " }\n else\n {\n"; { // B (K,N) 行主序:沿列连续 → vec4 整读 4 列同 k,直写共享 L << " const uint kBL = e / " << (B / 4u) << "u;\n"; @@ -605,11 +621,12 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) L << " }\n else\n {\n"; L << " for (uint j = 0u; j < 4u; ++j)\n {\n"; L << " const float v = (kBG < mm_k && bcG + j < cols) ? " - << b_load("bcG + j", "kBG") << " : 0.0;\n"; + << b_load("bcG + j", "kBG", false) << " : 0.0;\n"; L << " Bsh[stage][kBL][nB4][j] = v;\n"; L << " }\n }\n"; } L << " }\n"; + L << " }\n"; L << "}\n\n"; // ── main:4×4 寄存器分块 batched matmul(dispatch (x,y,z) = (列块,行块,批次)) @@ -1889,12 +1906,14 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << " uint cols;\n"; L << " uint rows;\n"; L << " uint vector_out; // 1=输出归约向量((rows,1)/(1,cols)),0=广播\n"; - // matmul+归约(注意力结构):mm_k 求和维度 + mm_batch 批量数 - // (均为形状参数,运行时填充;dispatch 不变,batch 由 idx 分解) + // matmul+归约(注意力结构):mm_k 求和维度 + mm_batch 批量数 + mm_trans + // 转置布局(前两者为形状参数、后者为运行期 operand layout,均运行时填充; + // dispatch 不变,batch 由 idx 分解) if (spec.matmul) { L << " uint mm_k;\n"; L << " uint mm_batch;\n"; + L << " uint mm_trans; // bit0=transA, bit1=transB(不进 key)\n"; } const std::uint32_t n_vp = expr_spec_runtime_view_param_count(spec); for (std::uint32_t i = 0; i < n_vp; ++i) @@ -1908,7 +1927,8 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << "shared float s_red[" << n_slots << "][256];\n\n"; // ── matmul 段(分块 + batch):当前元素 (row,col) 的 matmul 值(内联 - // K 循环点积,不物化 (batch*M,N) 中间矩阵;transA/transB 硬编码, + // K 循环点积,不物化 (batch*M,N) 中间矩阵;**transA/transB 是运行期 + // operand layout(mm_trans,不进 key)→ 两种索引都发射、运行时三元选择**, // mm_k/mm_batch 运行时填充;row 为全局行 = batch*m_per + 块内行)── const MatmulSpec* mm = spec.matmul ? &*spec.matmul : nullptr; const auto emit_mm_decl = [&]() @@ -1917,22 +1937,29 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, return; const std::uint32_t a_slot = mm->a_input; const std::uint32_t b_slot = mm->b_input; - const bool trA = (mm->transA != 0); - const bool trB = (mm->transB != 0); - const std::string a_idx = trA - ? "(batch*mm_k + kk)*m_per + row_in_batch" - : "(batch*m_per + row_in_batch)*mm_k + kk"; - const std::string b_idx = trB - ? "(batch*cols + col)*mm_k + kk" - : "(batch*mm_k + kk)*cols + col"; L << " const uint row_in_batch = row % m_per;\n"; // 列归约:batch 无顶层分解(idx 是列 tile)→ 按当前 row 就地分解; // 行归约:顶层已有 batch(= idx/m_per),此处不重复声明 if (!is_row) L << " const uint batch = row / m_per;\n"; + // 转置布局(运行期 mm_trans,不进 key):两种索引折算成 base + kk*stride, + // 循环体内**无分支**(mm_trans 是 uniform push constant)。 + // transA : A 存 (K,M) → idx = (batch*mm_k + kk)*m_per + row_in_batch + // !transA: A 存 (M,K) → idx = (batch*m_per + row_in_batch)*mm_k + kk + L << " const bool tA = (mm_trans & 1u) != 0u;\n"; + L << " const uint a_base = tA ? (batch*mm_k*m_per + row_in_batch)\n" + << " : (batch*m_per*mm_k + row_in_batch*mm_k);\n"; + L << " const uint a_stride = tA ? m_per : 1u;\n"; + // transB : B 存 (N,K) → idx = (batch*cols + col)*mm_k + kk + // !transB: B 存 (K,N) → idx = (batch*mm_k + kk)*cols + col + L << " const bool tB = (mm_trans & 2u) != 0u;\n"; + L << " const uint b_base = tB ? (batch*cols*mm_k + col*mm_k)\n" + << " : (batch*mm_k*cols + col);\n"; + L << " const uint b_stride = tB ? 1u : cols;\n"; L << " float mm = 0.0;\n"; L << " for (uint kk = 0u; kk < mm_k; ++kk)\n"; - L << " mm += " << rd(a_slot, a_idx) << " * " << rd(b_slot, b_idx) << ";\n"; + L << " mm += " << rd(a_slot, "a_base + kk*a_stride") + << " * " << rd(b_slot, "b_base + kk*b_stride") << ";\n"; }; L << "void main()\n{\n"; diff --git a/include/neuralnet.cpp/expr_spec.hpp b/include/neuralnet.cpp/expr_spec.hpp index 4a81b914..34750d0e 100644 --- a/include/neuralnet.cpp/expr_spec.hpp +++ b/include/neuralnet.cpp/expr_spec.hpp @@ -247,7 +247,11 @@ struct ExprInstr // - 其余逐元素输入的视图/形状仍要求 (M, N)(如 bias、残差)。 // - k(求和维度)是**形状参数**:不进 expr_spec_key(同结构不同 K 共享 // 一个融合 shader),运行时作为 push constant 填充。 -// - transA/transB/a_input/b_input 是**结构**:进 expr_spec_key。 +// - transA/transB 是**运行期 operand layout**:不进 expr_spec_key(同一个 +// 结构覆盖 4 种转置组合),运行时经 `expr_spec_runtime_matmul_trans` +// 打包成 push constant `mm_trans`(bit0=transA / bit1=transB),shader +// 内两条加载路径 + uniform 分支选择。 +// - a_input/b_input(输入槽位)是**结构**:进 expr_spec_key。 // - batch:批量数,A/B 按 batch 垂直切分为连续行块(与 // batched_matmul 原语同布局),输出网格 (batch*M, N); // **形状参数**:不进 key(同结构不同 batch 共享一个融合 shader), @@ -443,6 +447,17 @@ struct ExprSpec if (s.fold && s.fold->matmul) return s.fold->matmul->batch; return 1u; } +// 运行时 matmul 转置布局(bit0 = transA,bit1 = transB):**不进 key** —— +// 同一结构覆盖 4 种转置组合,shader 内两条加载路径 + uniform 分支按此选择 +// (见 generate_glsl_matmul / generate_glsl_reduce 的 PC `mm_trans`)。 +// ⚠ **双域 fold 自带的 matmul 段不在此列**:fold 的 transA/transB 仍进 key +// (生成期定死,见 expr_spec_key 的 fold 分支),此处只描述顶层 matmul 段。 +[[nodiscard]] inline std::uint32_t expr_spec_runtime_matmul_trans( + const ExprSpec& s) noexcept +{ + if (!s.matmul) return 0u; + return (s.matmul->transA ? 1u : 0u) | (s.matmul->transB ? 2u : 0u); +} // fold 收缩轴长度(形状参数,不进 key → push constant 运行时填充) [[nodiscard]] inline std::optional expr_spec_runtime_fold_k( const ExprSpec& s) noexcept @@ -610,16 +625,17 @@ struct ExprSpec // 共享一个融合 shader(glsl_gen 把 rparams 作为 push constant 读取, // dispatch 时按实际 spec 填充)。与 RowMod/RotateHalf 的 param 同处理。 feed_u32(static_cast(s.rparams.size())); - // matmul 段(可选):transA/transB/a_input/b_input 是**结构** → 进 key; - // k(求和维度)是**形状参数** → 不进 key(同 RowMod/RotateHalf 的 param - // 处理):同结构不同 K 共享一个融合 shader(glsl_gen 把 k 作为 push - // constant 读取,dispatch 时按实际 spec 填充)。 + // matmul 段(可选):a_input/b_input(输入槽位)是**结构** → 进 key; + // k(求和维度)与 batch 是**形状参数** → 不进 key(同 RowMod/RotateHalf 的 + // param 处理):同结构不同 K/batch 共享一个融合 shader(glsl_gen 把 k/batch + // 作为 push constant 读取,dispatch 时按实际 spec 填充)。 + // **transA/transB 同样不进 key**:它们是运行期 operand layout(PC `mm_trans`, + // 见 expr_spec_runtime_matmul_trans),shader 内两条加载路径 + uniform 分支 + // 运行时选择 → 同一结构覆盖 4 种转置组合(原先 4 份 shader 合并为 1 份)。 if (s.matmul) { feed(&s.matmul->a_input, 1); feed(&s.matmul->b_input, 1); - feed(&s.matmul->transA, 1); - feed(&s.matmul->transB, 1); } // fold 段:结构字段全进 key(num_state、inits 初值——-inf/0 属 // 结构、body/finalize 指令序列);k 是形状参数不进(同 matmul.k 处理, diff --git a/src/expr_matmul_test.cpp b/src/expr_matmul_test.cpp index 2546a703..1cfd74d2 100644 --- a/src/expr_matmul_test.cpp +++ b/src/expr_matmul_test.cpp @@ -8,7 +8,9 @@ // CPU err=0(与参考实现逐位/容差一致)。 // 3. DSL matmul(A,B) 叶子:折叠为 matmul 段,dsl::compute 自动分流引擎。 // 4. key 与形状无关:k(求和维度)不进 key,同结构不同 K 共享融合 shader; -// transA/transB/a_input/b_input 是结构,进 key。 +// a_input/b_input(输入槽位)是结构,进 key; +// **transA/transB 是运行期 operand layout,不进 key** —— 4 种转置组合共享 +// 同一个融合 shader(GPU 端由 push constant `mm_trans` 选加载路径)。 // 5. matmul 输出可被归约指令消费(row_sum(matmul(x)),CPU 参考路径)。 // // 纯 CPU、无 GPU 依赖。编译/运行: @@ -143,7 +145,7 @@ void test_matmul_bias_relu() CHECK(inputs.size() == 3, "折叠 spec 应有 3 个输入(A/B/bias)"); } -// ── 4) transA / transB 变体(转置标志是结构,进 key)──────────────────── +// ── 4) transA / transB 变体(运行期 operand layout,不进 key)───────────── void test_transpose_variants() { const std::size_t M = 4, K = 5, N = 3; @@ -181,8 +183,25 @@ void test_transpose_variants() const std::string k01 = run(false, true); const std::string k10 = run(true, false); const std::string k11 = run(true, true); - CHECK(k00 != k01 && k01 != k10 && k10 != k11 && k00 != k11, - "transA/transB 是结构:不同转置组合 key 必须不同"); + // 契约(2026-10-01 起):transA/transB 是**运行期 operand layout**、不进 key + // → 4 种转置组合共享同一个融合 shader(GPU 端经 PC `mm_trans` 的两条加载 + // 路径 + uniform 分支选择)。数值正确性由上面 4 次 eval_expr vs eng.matmul + // 对拍保证;这里锁死"key 必须相同",防止转置被重新折回结构。 + CHECK(k00 == k01 && k01 == k10 && k10 == k11, + "transA/transB 是运行期 operand layout:4 种转置组合的 key 必须相同"); + + // 运行期描述子:4 种组合必须编码成 mm_trans 的 4 个不同位模式 + // (GPU 端靠它选加载路径;编码错 = 静默读错布局) + const auto mk = [](bool a, bool b) { + nn::ExprSpec s; + s.views = {nn::expr::linear(), nn::expr::linear()}; + s.matmul = nn::MatmulSpec{0, 1, static_cast(a ? 1u : 0u), + static_cast(b ? 1u : 0u), 1}; + return nn::expr_spec_runtime_matmul_trans(s); + }; + CHECK(mk(false, false) == 0u && mk(true, false) == 1u && + mk(false, true) == 2u && mk(true, true) == 3u, + "expr_spec_runtime_matmul_trans 位编码(bit0=transA, bit1=transB)"); } // ── 5) key 与形状无关:k 不进 key,同结构不同 K 共享融合 shader ──────── diff --git a/tools/prec_backfill.txt b/tools/prec_backfill.txt index cbbce254..4b514400 100644 --- a/tools/prec_backfill.txt +++ b/tools/prec_backfill.txt @@ -14,8 +14,8 @@ # (幂等去重,不影响产物)。 # Linear matmul 段 run-only 混合签名(miss 时 (64,8192)/(256,8192) 物化大头) -644a67003cf02a4d 10001 -644a67003cf02a4d 10005 +ad5a483ea593210a 10001 +ad5a483ea593210a 10005 # Norm 归约链(raxis=1/含归约逐元素):输入 f16 统计量、输出 f32 e21f87a71850cc6d 0002 From 148d1d72b879447b261943b10efdabaadece7bec Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 14:42:01 +0800 Subject: [PATCH 03/17] =?UTF-8?q?=E8=9E=8D=E5=90=88=20AOT=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E2=91=A2=EF=BC=9A=E5=88=86=E7=BB=84=E5=BD=92=E7=BA=A6?= =?UTF-8?q?=20R=20=E9=80=80=E5=87=BA=20expr=5Fspec=5Fkey=EF=BC=88=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=9C=9F=E5=BE=AA=E7=8E=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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(row, col)` 调用; - 新增 `glsl_emit_grouped_reduce_helpers()`:为每个分组归约视图发射 `gr_r()`(内部 `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()` 运行期循环 + 调用点。 --- docs/development/03-ir-optimization.md | 2 +- .../12-compute-engine-inventory.md | 9 +- docs/usage/03-compute-engine-usage.md | 2 +- include/neuralnet.cpp/expr_glsl_gen.hpp | 84 +++++++++++++------ include/neuralnet.cpp/expr_spec.hpp | 21 +++-- src/maxpool_gradcheck.cpp | 4 + 6 files changed, 82 insertions(+), 40 deletions(-) diff --git a/docs/development/03-ir-optimization.md b/docs/development/03-ir-optimization.md index a0acaf99..6c86c9c2 100644 --- a/docs/development/03-ir-optimization.md +++ b/docs/development/03-ir-optimization.md @@ -111,7 +111,7 @@ expr_spec_key(spec) ≡ expr_spec_key(canonicalize_expr_spec(spec)) | `matmul.transA/transB` | ❌ | push constant `mm_trans`(bit0=transA / bit1=transB;shader 内两条加载路径 + uniform 分支,同一结构覆盖 4 种转置组合) | | fold 段 matmul 的 transA/transB | ✅(fold 的 mm 段生成期定死,**尚未收敛**) | — | | fold 的 `k` / `vec_state_len` | ❌ | push constant | -| 分组归约视图的 `R` | ✅(**待收敛**:目标改为运行期循环) | — | +| 分组归约视图的 `R` | ❌ | push constant `vp`(读取走 `gr_r()` 辅助函数内的运行期循环,累加顺序与 CPU 模板路径一致) | ### 4.2 确定性铁律 diff --git a/docs/development/12-compute-engine-inventory.md b/docs/development/12-compute-engine-inventory.md index 432f8b06..1934dcbb 100644 --- a/docs/development/12-compute-engine-inventory.md +++ b/docs/development/12-compute-engine-inventory.md @@ -88,9 +88,12 @@ scan_suffix_outer/outer_col——顺序状态机,DSL 无此语义)、`eval_e 3. **新增 ExprViewKind 的落点清单**(以 grouped_reduce 为参照):`expr_spec.hpp` 枚举+validate → `expr_dsl.hpp` SpecBuilder helper + 叶子 + 自由函数 + `has_reduction_v=false` 显式特化 → `expr_glsl_gen.hpp` `glsl_view_read` case - (param 编译期展开)→ `compute_cpu_engine.hpp` 解释器两处(形状校验 switch + - 读取 switch)。GPU 侧无 per-view 分派(只填 vp push constant,非 runtime param - 的 view 不涉及);`glsl_vec4_eligible` 白名单与 `glsl_view_uses_row/col` 排除集 + → `compute_cpu_engine.hpp` 解释器两处(形状校验 switch + 读取 switch)。 + **param 是否进 key 取决于 `expr_view_has_runtime_param`**:进(结构参数)= case + 内按 `v.param` 编译期展开;不进(运行期视图参数)= case 内只发一个读取调用, + 另需一处生成器侧辅助函数(如分组归约的 `glsl_emit_grouped_reduce_helpers` 发 + `gr_r()` 运行期循环)并在生成器 PC 之后调用。GPU 侧无 per-view 分派(只填 vp + push constant);`glsl_vec4_eligible` 白名单与 `glsl_view_uses_row/col` 排除集 自动正确——无需改。 ### 2.2 不被 Layer 直接调用(其余 28 个:DSL lowering / 序列化 / CLI / 适配层 / 测试) diff --git a/docs/usage/03-compute-engine-usage.md b/docs/usage/03-compute-engine-usage.md index af5c8520..2fa186da 100644 --- a/docs/usage/03-compute-engine-usage.md +++ b/docs/usage/03-compute-engine-usage.md @@ -821,7 +821,7 @@ template concept nn::dsl::DslExpr = > ⚠ `batch_col` 要求 `(1, BH*seq)` 这类按 `(b,h)` 块重复的形状;写 `(1, batch*seq)` 会**越界**。 -**`R`(分组归约长度)是结构参数,进 `expr_spec_key`**;它不是"归约视图",不参与归约轴判定,走 elementwise 路径。累加顺序固定为 `i` 升序左结合(三端一致,铁律 #8)。 +**`R`(分组归约长度)是运行期视图参数,不进 `expr_spec_key`**(经 push constant `vp` 槽传入,任一 shader 用运行期循环读取 → 任意池化窗口共享一个融合 shader);它不是"归约视图",不参与归约轴判定,走 elementwise 路径。累加顺序固定为 `i` 升序左结合(三端一致,铁律 #8)。 ```cpp // 典型:加偏置(每行同一个偏置) diff --git a/include/neuralnet.cpp/expr_glsl_gen.hpp b/include/neuralnet.cpp/expr_glsl_gen.hpp index 06ebf697..aaf6b59d 100644 --- a/include/neuralnet.cpp/expr_glsl_gen.hpp +++ b/include/neuralnet.cpp/expr_glsl_gen.hpp @@ -131,34 +131,14 @@ inline void glsl_view_read(std::ostringstream& os, } case static_cast(ExprViewKind::GroupedReduceSum): case static_cast(ExprViewKind::GroupedReduceMax): - { // 分组归约:输出行 r → 输入组行 [r*R, (r+1)*R),列不变。 - // R = v.param 是**进 key 的结构参数**(编译期已知)→ 展开 R 次 - // 读取链(sum 用左结合加法、max 用嵌套 max):累加顺序 t 升序, - // 与 CPU 模板路径 GroupedReduceRef::eval 逐元素一致(铁律 8)。 - // rw() 负责 f16 缓冲的 float() 包装(f16_native 变体保持原类型)。 - const std::uint32_t R = v.param; - const bool gmax = - v.kind == static_cast(ExprViewKind::GroupedReduceMax); - const auto elem = [&](std::uint32_t tt) -> std::string { - const std::string idx = "(" + row_var + " * " + std::to_string(R) - + "u + " + std::to_string(tt) + "u) * cols + " + col_var; - return rw(idx); - }; - std::string acc = elem(0); - if (gmax) - { - for (std::uint32_t tt = 1; tt < R; ++tt) - acc = "max(" + acc + ", " + elem(tt) + ")"; - } - else - { - for (std::uint32_t tt = 1; tt < R; ++tt) - acc += " + " + elem(tt); - } - os << "(" << acc << ")"; + // R 是**运行期**视图参数(vp 槽,不进 key → 任意池化窗口共享一个 + // shader),故读取走 per-view 辅助函数 `gr_r(row, col)` + // (内部为运行期循环;累加顺序与 CPU 模板路径 GroupedReduceRef::eval + // 逐元素一致,铁律 8)。辅助函数由 + // `glsl_emit_grouped_reduce_helpers()` 在 shader 顶部发射。 + os << "gr_r" << buf_id << "(" << row_var << ", " << col_var << ")"; return; - } case static_cast(ExprViewKind::RowBroadcast): os << buf << "[" << row_var << "]"; // 输入 (rows,1):每行一个值 return; @@ -187,6 +167,53 @@ inline void glsl_view_read(std::ostringstream& os, } } +// ── 分组归约视图的读取辅助函数发射(R 运行期 → 循环替代编译期展开链)────── +// R 是运行期视图参数(vp 槽,不进 key)→ 任意池化窗口共享一个 shader。 +// 循环内的累加顺序与 CPU 模板路径 GroupedReduceRef::eval 逐元素一致(铁律 8): +// sum = 左结合升序加;max = 嵌套 max(acc, elem)。读取点见 glsl_view_read 的 +// GroupedReduceSum/Max 分支(`gr_r(row, col)`),故**两者必须成对发射**。 +// 位置:shader 顶部(PC 声明之后、main 之前)。 +// 注意:分组归约视图不属于归约视图(expr_view_is_reduce=false)→ 只走 +// elementwise 生成路径;其它生成器不需要调用本函数。 +inline void glsl_emit_grouped_reduce_helpers(std::ostringstream& os, + const ExprSpec& spec, + ExprPrecSig sig = 0, + bool f16_native = false) +{ + const char* ty = f16_native ? "float16_t" : "float"; + for (std::size_t i = 0; i < spec.views.size(); ++i) + { + const auto k = static_cast(spec.views[i].kind); + if (k != ExprViewKind::GroupedReduceSum && + k != ExprViewKind::GroupedReduceMax) + continue; + // vp 槽 = 按视图序累计(与 glsl_view_read 调用点的算法逐字一致) + std::uint32_t vp = 0; + for (std::size_t j = 0; j < i; ++j) + if (expr_view_has_runtime_param( + static_cast(spec.views[j].kind))) + vp += expr_view_runtime_param_slots( + static_cast(spec.views[j].kind)); + const bool is_max = (k == ExprViewKind::GroupedReduceMax); + const bool f16_buf = expr_prec_sig_in_f16(sig, i) && !f16_native; + const std::string buf = "b" + std::to_string(i); + const auto elem = [&](const std::string& tt) -> std::string { + const std::string idx = "((row * R + " + tt + ") * cols + col)"; + return f16_buf ? ("float(" + buf + "[" + idx + "])") + : (buf + "[" + idx + "]"); + }; + os << "\n" << ty << " gr_r" << i << "(uint row, uint col)\n{\n"; + os << " const uint R = vp" << vp << ";\n"; + os << " " << ty << " acc = " << elem("0u") << ";\n"; + os << " for (uint tt = 1u; tt < R; ++tt)\n"; + if (is_max) + os << " acc = max(acc, " << elem("tt") << ");\n"; + else + os << " acc += " << elem("tt") << ";\n"; + os << " return acc;\n}\n"; + } +} + // ── 视图是否使用行/列索引(用于按需发射 row/col,省去每元素整数除法/取模)── // Linear 用扁平索引、RowBroadcast 用 row、ColBroadcast 用 col、 // RowMod/RotateHalf 用 row+col(索引映射逐元素变化,见 glsl_view_read)。 @@ -1485,6 +1512,11 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << " float rp" << i << ";\n"; L << "};\n\n"; + // 分组归约视图(MaxPool 等)的读取辅助函数:R 是运行期 vp 参数 → 循环。 + // 与 glsl_view_read 的 GroupedReduceSum/Max 分支成对(编译器把 `gr_r` + // 调用点连到这里;漏发 = 未声明函数,glslc 立刻报错而非静默错值)。 + glsl_emit_grouped_reduce_helpers(L, spec, sig, native16); + // native16 恒走标量 kernel(f16vec4 向量化路径留作后续优化)—— // vec_width 元数据必须同源为 1(gen_fused 对 ALU 变体同判,否则 // dispatch 按 4 宽派发而 kernel 按 1 宽算 → 静默只算 1/4 元素)。 diff --git a/include/neuralnet.cpp/expr_spec.hpp b/include/neuralnet.cpp/expr_spec.hpp index 34750d0e..bb127d6e 100644 --- a/include/neuralnet.cpp/expr_spec.hpp +++ b/include/neuralnet.cpp/expr_spec.hpp @@ -183,9 +183,9 @@ enum class ExprViewKind : uint8_t // ── 分组归约(segmented reduce;MaxPool 等按固定长度 R 分组的场景)── // 输出网格 (G, N),输入 (G*R, N):out[r][c] = Σ/max over t∈[0,R) of // in[(r*R + t)][c](沿行方向按固定长度 R 分组,组内归约)。 - // param = R(组长度):**非 runtime param → R 进 expr_spec_key**(不同 - // 组长度 = 不同结构,glsl_gen 编译期展开 R 次读取链;R 是池化窗口等 - // 结构参数,取值空间小)。**不属于归约视图**(expr_view_is_reduce=false): + // param = R(组长度):**运行期形状数据 → R 不进 expr_spec_key**(同一 + // 结构覆盖任意组长度,glsl_gen 用运行期循环而非编译期展开链;R 经 + // push constant vp 槽填入)。**不属于归约视图**(expr_view_is_reduce=false): // 输出是全网格而非归约向量,不参与 expr_spec_reduce_axis 的归约轴判定, // 也不进 reduce-shader 分派——每输出元素独立归约 R 个输入元素,与 // RowAccess 同属"逐元素索引/局部读取"类,走 elementwise 生成路径。 @@ -473,21 +473,24 @@ struct ExprSpec } // ── 运行时视图参数(形状无关融合的关键)─────────────────────────────── -// RowMod(周期)与 RotateHalf(块大小)的 param 是**运行时形状数据**(如 -// RoPE 的 d_k),不是表达式结构:同结构不同 param(不同 d_k)应共享一个 -// 融合 shader。因此: -// - expr_spec_key **不**把这两个 param 折进 key(结构相同 → 同 key) +// RowMod(周期)/RotateHalf(块)/RowAccess(偏移+模)与分组归约的 R 都是 +// **运行时形状数据**,不是表达式结构:同结构不同 param 应共享一个融合 shader。 +// 因此: +// - expr_spec_key **不**把这些 param 折进 key(结构相同 → 同 key) // - glsl_gen 把它们作为 push constant(vp 槽)读取,dispatch 时按实际 -// spec 填充 → 一个 shader 适配所有形状(任何 d_k) +// spec 填充 → 一个 shader 适配所有形状(任何 d_k / 任何池化窗口) [[nodiscard]] inline constexpr bool expr_view_has_runtime_param(ExprViewKind k) noexcept { // RowMod/RotateHalf 的形状参数(d_k)与 BatchMod/BatchCol 的形状参数 // (num_heads / seq)都是**运行时形状数据**:不进 expr_spec_key,作为 // push constant vp 槽由 dispatch 按实际 spec 填充 → 同结构不同形状 // (不同 d_k / num_heads / seq_len)共享一个融合 shader(形状无关融合)。 + // 分组归约的 R(= 池化窗口面积)同理:运行期循环取代编译期展开链。 return k == ExprViewKind::RowMod || k == ExprViewKind::RotateHalf || k == ExprViewKind::BatchMod || k == ExprViewKind::BatchCol || - k == ExprViewKind::RowAccess; + k == ExprViewKind::RowAccess || + k == ExprViewKind::GroupedReduceSum || + k == ExprViewKind::GroupedReduceMax; } // 该视图消耗的运行时视图参数槽位数(RowAccess 用 offset+mod 两个 vp 槽) [[nodiscard]] inline constexpr std::uint32_t expr_view_runtime_param_slots( diff --git a/src/maxpool_gradcheck.cpp b/src/maxpool_gradcheck.cpp index 90de0ff7..0006c713 100644 --- a/src/maxpool_gradcheck.cpp +++ b/src/maxpool_gradcheck.cpp @@ -134,6 +134,10 @@ int main(int argc, char* argv[]) {2, 6, 6, 2, 2, 2}, // 标准(stride == pool) {2, 6, 6, 2, 1, 2}, // 重叠窗口(梯度在同一位置累加) {3, 5, 7, 2, 2, 2}, // 非整除尺寸(尾部余数) + // pool=3(R = 9):分组归约的 R 是**运行期**视图参数(不进 expr_spec_key) + // → 任意池化窗口共享同一个融合 shader。此前 R 进 key,GPU 上非扫描值 + // (扫描只跑 pool=2)会闭合世界未命中硬报错;本用例是该回归的门禁。 + {2, 7, 7, 3, 3, 2}, }; const Scalar tol = 1e-6f; int failures = 0; From 31051a32a8b848302dfd072530e67b745278dd80 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 14:48:46 +0800 Subject: [PATCH 04/17] =?UTF-8?q?=E8=9E=8D=E5=90=88=20AOT=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E2=91=A4=E5=89=8D=E7=BD=AE=EF=BC=9A=E7=AC=A6=E5=8F=B7?= =?UTF-8?q?=E5=BC=A0=E9=87=8F=E4=B8=8D=E5=86=8D=E8=A7=A6=E5=8F=91=20CPU=20?= =?UTF-8?q?=E8=A7=86=E5=9B=BE=E7=BC=93=E5=AD=98=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AOT 自登记(`FusedAnchor`:按表达式**类型**折叠出结构)需要"从类型构造 符号实例"——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` 逐位不变。 --- include/neuralnet.cpp/expr_dsl.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/neuralnet.cpp/expr_dsl.hpp b/include/neuralnet.cpp/expr_dsl.hpp index df5b0095..7718e636 100644 --- a/include/neuralnet.cpp/expr_dsl.hpp +++ b/include/neuralnet.cpp/expr_dsl.hpp @@ -315,6 +315,11 @@ struct CpuViewCache { if (!t.is_cpu()) return; + // 符号张量(默认构造:未绑定、无存储)——AOT 自登记需要"从表达式类型 + // 折叠出结构",那条路径只看 to_spec 折出的视图/指令,不会 eval;此处 + // 直接返回,避免在无存储张量上取 CPU 视图(原会 NN_ASSERT 硬失败)。 + if (!t.valid()) + return; if (t.precision() == Precision::F16) { auto m = std::make_shared(t.rows(), t.cols()); From 41b437460fd693c45f11d895d68a646929a1cb56 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 14:58:44 +0800 Subject: [PATCH 05/17] =?UTF-8?q?=E8=9E=8D=E5=90=88=20AOT=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E2=91=A4=EF=BC=9Adsl::compute=20=E8=87=AA=E7=99=BB?= =?UTF-8?q?=E8=AE=B0=E9=94=9A=E7=82=B9=EF=BC=88=E7=BB=93=E6=9E=84=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E4=B8=8D=E5=86=8D=E4=BE=9D=E8=B5=96=E6=89=8B=E5=86=99?= =?UTF-8?q?=E6=B8=85=E5=8D=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 每个 `dsl::compute/_into/_reduce` 实例化 odr-use 一个 `FusedAnchor`,其 **静态初始化期**把"从表达式类型默认构造的符号实例"折叠出的结构登记进注册表。 调用点在函数体里 → 函数被编译即实例化 → 覆盖 = **编译期可达**(该分支/配置 没被跑到也会登记),正好补上 dry-run"只覆盖跑到的路径"的缺口。 前提(步骤①②③已落地):结构 = 表达式类型。本步补齐最后一块: - `GroupedReduceRef` 的 `is_max` 提为模板参数 `GroupedReduceRef`——它 决定视图 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(只影响登记集合,不影响语义)。 --- docs/development/02-operator-fusion.md | 10 +++ include/neuralnet.cpp/expr_dsl.hpp | 103 ++++++++++++++++++++----- 2 files changed, 94 insertions(+), 19 deletions(-) diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index 8328e1f5..130f6181 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -237,6 +237,16 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba `scan_exprs` 需覆盖所有 Layer 的 DSL 路径(Softmax/LN/RMSNorm fwd+bwd、CrossEntropy softmax 结构、**Attention fold 结构**——层 forward 直调 `engine.eval_expr(make_fold_attn_o(...))` 不经 DSL 钩子,scan 的显式登记块(3 掩码 × 2 偏置的 5 个组合)是 fold spec 唯一注册来源、漏组合即 GPU 闭合世界硬报错、Linear 的 matmul 段、optimizer 的 `compute_into` 原地表达式),使融合结构被收集。未命中 → `eval_expr` 现有"硬报错"逻辑,提示补进扫描(保持项目"GPU 硬报错、不降级"哲学)。 +### 自登记锚点:结构覆盖不再依赖手写清单(2026-10-01) + +手写 dry-run 清单只覆盖**跑到的**路径;配置分支没被跑到就漏(`MaxPool2D(pool≠2)` 曾如此)。为此 `expr_dsl.hpp` 增加 `FusedAnchor`:每个 `dsl::compute/_into/_reduce` 实例化 odr-use 一个锚点,其**静态初始化期**把"从表达式类型默认构造的符号实例"折叠出的结构登记进注册表。 + +- **前提 = "结构 = 表达式类型"**:为此步骤①②③把 `consts` 值、`matmul.transA/transB`、`GroupedReduce.R` 逐出 key(分别改走 push constant),并把 `GroupedReduceRef` 的 `is_max`(决定视图 kind)提为模板参数。 +- **覆盖语义**:调用点在函数体里,**函数被编译即实例化**——与运行期是否走到该分支无关,因此是"编译期可达"而非"运行期可达"。A/B 实测(`-DNN_SCAN_NO_ANCHOR`):结构数 **72(仅 dry-run)→ 84(+锚点)**,多出的 12 条都是编译进来的真实调用点(3 条含 matmul、1 条归约、1 条 16 个 vp 槽的宽表达式、其余逐元素)。 +- **只登记结构(sig=0)**:⚠ **精度签名登记不出来**——同一个表达式里"哪些输入是 f16"由运行期张量精度决定(类型层面不可见)。实测扫描出的 60 个带类型变体里只有 16 个是"全 f16 输入+输出",其余 44 个是混合签名(`0x0001/0x0003/0x10005…`)。故 f16 带类型变体**仍须**由 profile_f32/profile_f16 两遍 dry-run(或运行期 `NN_PREC_TRACE` + `prec_backfill.txt`)产生,**自登记无法取代它**。 +- 非法结构(裸视图作根 → 空指令表)在构建期即 `_Exit(3)`,与 dry-run 同一闸门。 +- `-DNN_SCAN_NO_ANCHOR` 可关掉锚点做 A/B(只影响登记集合,不影响语义)。 + --- ## 二期 S1-S7:手写算子收敛为 IR 融合 diff --git a/include/neuralnet.cpp/expr_dsl.hpp b/include/neuralnet.cpp/expr_dsl.hpp index 7718e636..4721d1b4 100644 --- a/include/neuralnet.cpp/expr_dsl.hpp +++ b/include/neuralnet.cpp/expr_dsl.hpp @@ -374,6 +374,9 @@ struct TensorRef : CpuViewCache Tensor t; TensorRef(Tensor tt) : t(std::move(tt)) { cache_cpu_view(t); } + // 符号实例(AOT 自登记 `FusedAnchor`):`t` 默认构造(未绑定、无存储, + // cache_cpu_view 对 !valid() 直接返回)——结构只由类型决定 + TensorRef() = default; [[nodiscard]] Scalar eval(std::size_t i) const { return at(i); } ExprOperand to_spec(SpecBuilder& b) const { return b.add_input_linear(t); } }; @@ -382,10 +385,12 @@ struct TensorRef : CpuViewCache struct RotateHalfRef : CpuViewCache { Tensor t; - std::uint32_t block; + std::uint32_t block = 1; // 运行时视图参数(不进 key);默认值供符号实例 RotateHalfRef(Tensor tt, std::uint32_t blk) : t(std::move(tt)), block(blk) { cache_cpu_view(t); } + // 符号实例(AOT 自登记):结构由类型决定,视图参数取中性值(不进 key) + RotateHalfRef() = default; [[nodiscard]] Scalar eval(std::size_t i) const { @@ -404,9 +409,11 @@ struct RotateHalfRef : CpuViewCache struct RowModRef : CpuViewCache { Tensor t; - std::uint32_t mod; + std::uint32_t mod = 1; // 运行时视图参数(不进 key);默认值供符号实例 RowModRef(Tensor tt, std::uint32_t m) : t(std::move(tt)), mod(m) { cache_cpu_view(t); } + // 符号实例(AOT 自登记):结构由类型决定,视图参数取中性值(不进 key) + RowModRef() = default; [[nodiscard]] Scalar eval(std::size_t i) const { @@ -424,11 +431,13 @@ struct RowModRef : CpuViewCache struct RowAccessRef : CpuViewCache { Tensor t; - std::uint32_t offset; - std::uint32_t mod; + std::uint32_t offset = 0; // 运行时视图参数(不进 key);默认值供符号实例 + std::uint32_t mod = 1; // 同上 RowAccessRef(Tensor tt, std::uint32_t off, std::uint32_t m) : t(std::move(tt)), offset(off), mod(m) { cache_cpu_view(t); } + // 符号实例(AOT 自登记):结构由类型决定,视图参数取中性值(不进 key) + RowAccessRef() = default; [[nodiscard]] Scalar eval(std::size_t i) const { @@ -449,36 +458,46 @@ struct RowAccessRef : CpuViewCache // elementwise 生成(glsl_view_read 展开 R 次读取链)。不触发 // expr_spec_reduce_axis / reduce-shader 分派(输出是全网格而非归约向量)。 // 累加顺序:t 升序左结合链,CPU/GPU/解释器三端一致(铁律 8)。 +// IsMax 是**模板参数**(决定视图 kind = 结构),不是运行期成员: +// 0-sum / 1-max 是两种不同视图(`ExprViewKind::GroupedReduceSum/Max`), +// 而"结构 = 表达式类型"是 AOT 自登记(`FusedAnchor` 默认构造符号实例) +// 的前提——运行期 bool 会让两种视图同类型、结构不可枚举。 +// r_len(R)是运行期视图参数,不进 expr_spec_key(见 expr_spec.hpp)。 +template struct GroupedReduceRef : CpuViewCache { Tensor t; - std::uint32_t r_len; // 组长度 R(进 expr_spec_key 的结构参数) - bool is_max; + std::uint32_t r_len = 1; // 组长度 R(运行期视图参数,不进 expr_spec_key) - GroupedReduceRef(Tensor tt, std::uint32_t R, bool mx) - : t(std::move(tt)), r_len(R), is_max(mx) + GroupedReduceRef(Tensor tt, std::uint32_t R) + : t(std::move(tt)), r_len(R) { cache_cpu_view(t); } + // 符号实例(AOT 自登记):结构由类型决定,R 取中性值(不进 key) + GroupedReduceRef() = default; // 输入 cols = 输出 cols(分组只沿行方向)→ 缓存 cols 即可分解 (g, c) [[nodiscard]] Scalar eval(std::size_t i) const { const std::size_t g = i / cols, c = i % cols; - Scalar acc = is_max ? std::numeric_limits::lowest() : Scalar{0}; + Scalar acc = IsMax ? std::numeric_limits::lowest() : Scalar{0}; const std::size_t base = g * static_cast(r_len); for (std::uint32_t tt = 0; tt < r_len; ++tt) { const Scalar v = at((base + tt) * cols + c); - if (is_max) { if (v > acc) acc = v; } - else { acc += v; } + if (IsMax) { if (v > acc) acc = v; } + else { acc += v; } } return acc; } // 2 参版本:cols 语义与 1 参相同(输入/输出同列),转发即可 [[nodiscard]] Scalar eval(std::size_t i, std::size_t) const { return eval(i); } ExprOperand to_spec(SpecBuilder& b) const - { return b.add_input_grouped_reduce(t, r_len, is_max); } + { return b.add_input_grouped_reduce(t, r_len, IsMax); } }; +using GroupedReduceSumRef = GroupedReduceRef; +using GroupedReduceMaxRef = GroupedReduceRef; + // ══════════════════════════════════════════════════════════════════════════ // 归约叶子:把"按行/按列归约出标量向量"接入表达式 // @@ -678,7 +697,7 @@ struct RowGatherRef struct BatchModRef { Tensor t; - std::uint32_t modulo; + std::uint32_t modulo = 1; // 运行时视图参数(不进 key);默认值供符号实例 [[nodiscard]] constexpr Scalar eval(std::size_t) const noexcept { return Scalar{0}; } ExprOperand to_spec(SpecBuilder& sb) const @@ -694,7 +713,7 @@ struct BatchModRef struct BatchColRef { Tensor t; - std::uint32_t per_batch_cols; + std::uint32_t per_batch_cols = 1; // 运行时视图参数(不进 key);默认值供符号实例 [[nodiscard]] constexpr Scalar eval(std::size_t) const noexcept { return Scalar{0}; } ExprOperand to_spec(SpecBuilder& sb) const @@ -719,6 +738,8 @@ struct BroadcastRef : CpuViewCache Tensor t; BroadcastRef(Tensor tt) : t(std::move(tt)) { cache_cpu_view(t); } + // 符号实例(AOT 自登记):结构由类型(Kind)与默认张量决定 + BroadcastRef() = default; [[nodiscard]] Scalar eval(std::size_t i) const { @@ -870,7 +891,9 @@ inline constexpr bool has_reduction_v> = false; // 分组归约视图:每输出元素独立归约 R 行、元素间零依赖 → 非归约(留 // 编译期模板路径;fail-safe 显式声明,防默认值漂移把它推进解释器) template <> -inline constexpr bool has_reduction_v = false; +inline constexpr bool has_reduction_v = false; +template <> +inline constexpr bool has_reduction_v = false; // matmul 叶子同样需走 eval_expr(matmul 预计算 + 逐元素链,引擎实现) template <> inline constexpr bool has_reduction_v = true; @@ -939,10 +962,10 @@ inline constexpr bool has_reduction_v> { return RowAccessRef{std::move(t), offset, mod}; } // 分组归约(MaxPool 窗口归约等):输入 (G*R, N) → 输出 (G, N) -[[nodiscard]] inline GroupedReduceRef grouped_reduce_sum(Tensor t, std::uint32_t R) -{ return GroupedReduceRef{std::move(t), R, /*is_max=*/false}; } -[[nodiscard]] inline GroupedReduceRef grouped_reduce_max(Tensor t, std::uint32_t R) -{ return GroupedReduceRef{std::move(t), R, /*is_max=*/true}; } +[[nodiscard]] inline GroupedReduceSumRef grouped_reduce_sum(Tensor t, std::uint32_t R) +{ return GroupedReduceSumRef{std::move(t), R}; } +[[nodiscard]] inline GroupedReduceMaxRef grouped_reduce_max(Tensor t, std::uint32_t R) +{ return GroupedReduceMaxRef{std::move(t), R}; } // ══════════════════════════════════════════════════════════════════════════ // 归约自由函数:对输入 Tensor 直接归约 → 归约**视图**(GPU 融合更友好); @@ -1331,6 +1354,36 @@ template std::fflush(nullptr); std::_Exit(3); } + +// ── AOT 自登记锚点("结构 = 表达式类型")────────────────────────────────── +// 每个 `dsl::compute*` 实例化 odr-use 一个 `FusedAnchor`:其**静态初始化期** +// 把"从类型默认构造的符号实例"折叠出的结构登记进全局注册表(去重)。 +// +// 与"执行路径捕获"(dry-run 跑 Layer)互补: +// · dry-run 只覆盖**跑到的**路径(配置分支没跑到就漏); +// · 锚点覆盖**编译进来的每个调用点**——调用点在函数体里,函数被编译即实例化, +// 与运行期是否走到该分支无关(因此这是"编译期可达"而非"运行期可达")。 +// +// ⚠ 只登记**结构**(sig=0):精度签名取决于运行期张量精度(同一表达式里哪些 +// 输入是 f16),类型层面推不出来 → f16 带类型变体仍由 profile_f32/profile_f16 +// 两遍 dry-run 负责(自登记无法取代它)。 +// +// 结构非法(裸视图作根 → 空指令表等)在构建期即 `_Exit(3)`,与 dry-run 同一闸门。 +// +// 可用 `-DNN_SCAN_NO_ANCHOR` 关掉锚点做 A/B(只影响登记集合,不影响语义)。 +template +struct FusedAnchor +{ + static inline const int reg = [] { + const Expr sym{}; + auto [spec, inputs] = to_expr_spec(sym); + (void)inputs; // 符号实例:inputs 全是占位张量,注册只用结构 + if (auto v = validate_expr_spec(spec, spec.views.size()); !v) + scan_reject(v.error()); + nn::fused::global_registry().add(spec, 0u); + return 0; + }(); +}; #endif // ══════════════════════════════════════════════════════════════════════════ @@ -1354,6 +1407,10 @@ template // 构建期扫描模式:折叠内联表达式的**结构**并登记进全局注册表, // 返回占位张量让 dry-run 流程继续(scan 只关心表达式集合,不真算)。 // 表达式文本仍只出现在 Layer;这里登记的是派生物 ExprSpec。 + // 自登记锚点:按**类型**登记结构(覆盖 dry-run 没跑到的调用点) +#if !defined(NN_SCAN_NO_ANCHOR) + (void)FusedAnchor::reg; +#endif (void)eng; auto [spec, inputs] = to_expr_spec(e); if (auto v = validate_expr_spec(spec, inputs.size()); !v) @@ -1458,6 +1515,10 @@ template { #ifdef NN_EXPR_SCAN // 构建期扫描:与 compute() 一样只登记结构(不真算、不关心 dst 的值) + // 自登记锚点(按类型登记结构)见 compute() 同名说明 +#if !defined(NN_SCAN_NO_ANCHOR) + (void)FusedAnchor::reg; +#endif (void)eng; auto [spec, inputs] = to_expr_spec(e); if (auto v = validate_expr_spec(spec, inputs.size()); !v) @@ -1532,6 +1593,10 @@ template // 构建期扫描:同 compute(),登记结构(归约轴由 gen_fused 判定)。 // 占位张量按归约轴取向量形状 (rows,1)/(1,cols),使 Layer 后续 // add_inplace 等形状相关操作在 dry-run 中不因形状失配而中断。 + // 自登记锚点(按类型登记结构)见 compute() 同名说明 +#if !defined(NN_SCAN_NO_ANCHOR) + (void)FusedAnchor::reg; +#endif (void)eng; auto [spec, inputs] = to_expr_spec(e); if (auto v = validate_expr_spec(spec, inputs.size()); !v) From 63b7d720b0fcbb0c09e269b00cbc6ef39f63bb3e Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 15:04:42 +0800 Subject: [PATCH 06/17] =?UTF-8?q?=E8=9E=8D=E5=90=88=20AOT=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E2=91=A5=EF=BC=88=E4=BF=AE=E8=AE=A2=EF=BC=89=EF=BC=9A?= =?UTF-8?q?=E9=94=9A=E7=82=B9=E8=A6=86=E7=9B=96=E5=8F=B0=E8=B4=A6=20+=20?= =?UTF-8?q?=E7=AD=BE=E5=90=8D=E7=BC=BA=E5=8F=A3=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原步骤⑥「删除 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` 逐位不变。 --- docs/development/02-operator-fusion.md | 1 + include/neuralnet.cpp/expr_dsl.hpp | 12 +++++++- tools/scan_exprs.cpp | 40 ++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index 130f6181..6ed251ca 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -243,6 +243,7 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba - **前提 = "结构 = 表达式类型"**:为此步骤①②③把 `consts` 值、`matmul.transA/transB`、`GroupedReduce.R` 逐出 key(分别改走 push constant),并把 `GroupedReduceRef` 的 `is_max`(决定视图 kind)提为模板参数。 - **覆盖语义**:调用点在函数体里,**函数被编译即实例化**——与运行期是否走到该分支无关,因此是"编译期可达"而非"运行期可达"。A/B 实测(`-DNN_SCAN_NO_ANCHOR`):结构数 **72(仅 dry-run)→ 84(+锚点)**,多出的 12 条都是编译进来的真实调用点(3 条含 matmul、1 条归约、1 条 16 个 vp 槽的宽表达式、其余逐元素)。 +- **常驻台账**:锚点登记进独立注册表 `anchor_registry()`,`scan_exprs` 末尾合并并打印 `结构来源:dry-run 72 + 锚点 59 → 合并 84(锚点独有 12)`。**锚点独有数突然变大 = 有新的层路径没纳入 dry-run**(该数字是回归信号)。 - **只登记结构(sig=0)**:⚠ **精度签名登记不出来**——同一个表达式里"哪些输入是 f16"由运行期张量精度决定(类型层面不可见)。实测扫描出的 60 个带类型变体里只有 16 个是"全 f16 输入+输出",其余 44 个是混合签名(`0x0001/0x0003/0x10005…`)。故 f16 带类型变体**仍须**由 profile_f32/profile_f16 两遍 dry-run(或运行期 `NN_PREC_TRACE` + `prec_backfill.txt`)产生,**自登记无法取代它**。 - 非法结构(裸视图作根 → 空指令表)在构建期即 `_Exit(3)`,与 dry-run 同一闸门。 - `-DNN_SCAN_NO_ANCHOR` 可关掉锚点做 A/B(只影响登记集合,不影响语义)。 diff --git a/include/neuralnet.cpp/expr_dsl.hpp b/include/neuralnet.cpp/expr_dsl.hpp index 4721d1b4..a81ca7af 100644 --- a/include/neuralnet.cpp/expr_dsl.hpp +++ b/include/neuralnet.cpp/expr_dsl.hpp @@ -1371,6 +1371,16 @@ template // 结构非法(裸视图作根 → 空指令表等)在构建期即 `_Exit(3)`,与 dry-run 同一闸门。 // // 可用 `-DNN_SCAN_NO_ANCHOR` 关掉锚点做 A/B(只影响登记集合,不影响语义)。 +// +// 锚点登记进**独立注册表**:scan_exprs 在结尾把两者合并并打印"锚点独有结构数" +// —— 这是"结构覆盖已不依赖手写清单"的常驻台账(锚点独有 = 手写 dry-run 覆盖 +// 不到的调用点;该数突然变大通常意味着新增了未纳入 dry-run 的层路径)。 +[[nodiscard]] inline nn::fused::ExprRegistry& anchor_registry() +{ + static nn::fused::ExprRegistry reg; + return reg; +} + template struct FusedAnchor { @@ -1380,7 +1390,7 @@ struct FusedAnchor (void)inputs; // 符号实例:inputs 全是占位张量,注册只用结构 if (auto v = validate_expr_spec(spec, spec.views.size()); !v) scan_reject(v.error()); - nn::fused::global_registry().add(spec, 0u); + anchor_registry().add(spec, 0u); return 0; }(); }; diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index 347a8883..bc36632c 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -643,6 +643,25 @@ int main(int argc, char* argv[]) } auto& reg = nn::fused::global_registry(); + // ── 合并自登记锚点的结构("结构 = 表达式类型",见 expr_dsl.hpp)───────── + // 锚点在静态初始化期按**类型**登记,覆盖"编译进来但 dry-run 未执行到"的 + // 调用点(配置分支、未被 dry-run 造过的层路径)。两者合并后写盘。 + { + auto& anchor = nn::dsl::anchor_registry(); + const std::size_t dry_only = reg.specs.size(); + for (const auto& s : anchor.specs) + reg.add(s, 0u); + std::printf("[scan] 结构来源:dry-run %zu + 锚点 %zu → 合并 %zu(锚点独有 %zu)\n", + dry_only, anchor.specs.size(), reg.specs.size(), + reg.specs.size() - dry_only); + // 锚点独有 = 手写 dry-run 覆盖不到的调用点(该数突然变大 → 有新层路径 + // 没纳入 dry-run)。锚点只登记结构(sig=0),故这些结构默认没有 f16 + // 带类型变体 —— 下面单独报告。 + if (anchor.specs.empty() || dry_only == reg.specs.size()) + std::fprintf(stderr, + "[scan][warn] 锚点未贡献任何结构:可能被 -DNN_SCAN_NO_ANCHOR 关闭," + "或所有调用点都已被 dry-run 覆盖\n"); + } if (!nn::fused::write_registry(out_path, reg)) { std::fprintf(stderr, "[FAIL] 无法写入 %s\n", out_path.c_str()); @@ -650,6 +669,27 @@ int main(int argc, char* argv[]) } std::printf("[scan] 收集到 %zu 条融合表达式 -> %s\n", reg.specs.size(), out_path.c_str()); + // ── 签名覆盖报告:哪些结构**没有任何带类型变体** ────────────────────── + // 自登记只能给结构(sig=0):精度签名取决于运行期张量精度,类型层面推不出来 + // → f16 带类型变体仍须 dry-run 的 f32/f16 两遍产生。缺变体不是错误 + // (运行时回退边界 cast,正确但慢),但新增融合表达式时应当看到这一行。 + { + std::size_t no_variant = 0; + for (const auto& s : reg.specs) + { + const std::string k = nn::expr_spec_key(s); + bool has = false; + for (const auto& v : reg.variants) + if (nn::expr_spec_key(v.spec) == k) { has = true; break; } + if (!has) ++no_variant; + } + if (no_variant) + std::printf("[scan][note] %zu/%zu 条结构没有带类型变体" + "(缺 f16 时运行时回退边界 cast:正确但更慢;" + "补法 = 让 dry-run 覆盖该 Layer 路径,或用 NN_PREC_TRACE " + "收集后加进 tools/prec_backfill.txt)\n", + no_variant, reg.specs.size()); + } // 带类型变体(in-kernel f16):结构相同、精度签名不同 —— 每个 (结构, 签名) // 需要一个独立 shader;打印分类统计便于确认扫描覆盖(哪些是纯逐元素、 // 含 matmul 段或 fold 段)。 From 03f9383a99951ac215986d255e34e621ca4a6151 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 15:12:41 +0800 Subject: [PATCH 07/17] =?UTF-8?q?=E8=9E=8D=E5=90=88=20AOT=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E2=91=A5=C2=B7=E8=A1=A5=EF=BC=9A=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E7=BA=A7=E7=AD=BE=E5=90=8D=20pass=EF=BC=88run-only=20=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=207/7=20=E2=86=92=202/7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一步证明"自登记只能给结构、给不了精度签名"——签名必须由执行产生。本步把 执行面从"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` 逐位不变。 --- docs/development/02-operator-fusion.md | 3 +- tools/scan_exprs.cpp | 107 +++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index 6ed251ca..34e1c668 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -244,7 +244,8 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba - **前提 = "结构 = 表达式类型"**:为此步骤①②③把 `consts` 值、`matmul.transA/transB`、`GroupedReduce.R` 逐出 key(分别改走 push constant),并把 `GroupedReduceRef` 的 `is_max`(决定视图 kind)提为模板参数。 - **覆盖语义**:调用点在函数体里,**函数被编译即实例化**——与运行期是否走到该分支无关,因此是"编译期可达"而非"运行期可达"。A/B 实测(`-DNN_SCAN_NO_ANCHOR`):结构数 **72(仅 dry-run)→ 84(+锚点)**,多出的 12 条都是编译进来的真实调用点(3 条含 matmul、1 条归约、1 条 16 个 vp 槽的宽表达式、其余逐元素)。 - **常驻台账**:锚点登记进独立注册表 `anchor_registry()`,`scan_exprs` 末尾合并并打印 `结构来源:dry-run 72 + 锚点 59 → 合并 84(锚点独有 12)`。**锚点独有数突然变大 = 有新的层路径没纳入 dry-run**(该数字是回归信号)。 -- **只登记结构(sig=0)**:⚠ **精度签名登记不出来**——同一个表达式里"哪些输入是 f16"由运行期张量精度决定(类型层面不可见)。实测扫描出的 60 个带类型变体里只有 16 个是"全 f16 输入+输出",其余 44 个是混合签名(`0x0001/0x0003/0x10005…`)。故 f16 带类型变体**仍须**由 profile_f32/profile_f16 两遍 dry-run(或运行期 `NN_PREC_TRACE` + `prec_backfill.txt`)产生,**自登记无法取代它**。 +- **只登记结构(sig=0)**:⚠ **精度签名登记不出来**——同一个表达式里"哪些输入是 f16"由运行期张量精度决定(类型层面不可见)。实测扫描出的 60 个带类型变体里只有 16 个是"全 f16 输入+输出",其余 44 个是混合签名(`0x0001/0x0003/0x10005…`)。故 f16 带类型变体**必须**由 profile_f32/profile_f16 两遍 dry-run(或运行期 `NN_PREC_TRACE` + `prec_backfill.txt`)产生,**自登记无法取代它**。 +- **模型级签名 pass**(补签名覆盖):per-layer dry-run 块只覆盖"层被单独造出来"的路径;有些调用点只在**完整模型**里才执行(GPT/RAPT 的 LM head、PatchEmbedding、模型级位置编码等)。`scan_exprs` 因此在两个 profile 下各加一遍"用 shipped 工厂建 5 个模型 + 跑 fwd/bwd"(`mnist_mlp`/`mnist_transformer`/`cnn`/`gpt`/`rapt`;id 输入须填合法 token 值)。实测 **精度变体 60 → 72**,`prec_backfill.txt` 的"运行期才发现的签名"由 **7/7 降到 2/7**(即 5 个原本 run-only 的 f16 混合签名现在构建期即可预测)。该 pass 失败只打 `[scan][warn]` 不中断构建(它是增量,主路径已由 dry-run + 锚点覆盖)。 - 非法结构(裸视图作根 → 空指令表)在构建期即 `_Exit(3)`,与 dry-run 同一闸门。 - `-DNN_SCAN_NO_ANCHOR` 可关掉锚点做 A/B(只影响登记集合,不影响语义)。 diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index bc36632c..29c3bd0d 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -25,6 +25,10 @@ #include "compute_layer.hpp" #include "compute_loss.hpp" #include "compute_optimizer.hpp" +#include "domain_cnn.hpp" +#include "domain_gpt.hpp" +#include "domain_mnist.hpp" +#include "domain_rla.hpp" #include "expr_registry.hpp" #include "neuralnet.cpp/expr_fold.hpp" @@ -588,6 +592,109 @@ int main(int argc, char* argv[]) dry_run(raw_engine, nn::profile_f32()); // f32 pass:sig == 0(基础结构表) dry_run(raw_engine, nn::profile_f16()); // Phase 2:收集 (结构, 精度签名); // f16 安全由基类边界 cast 入口保证(原 PrecisionEngine 适配层已下沉删除) + + // ── 模型级 pass:用 shipped 工厂建模型跑 fwd/bwd(补"整模型路径"的签名)── + // per-layer dry-run 块只覆盖"层被单独造出来"的路径;有些调用点只在**完整模型** + // 里才会执行到(GPT/RAPT 的 LM head、PatchEmbedding、模型级位置编码等), + // 它们此前既没有结构(现已由 FusedAnchor 自登记补齐)也没有精度签名 + // → `--f16` 下走边界 cast。本 pass 让这些路径也产出签名。 + // + // 失败不硬失败:本 pass 是**增量**,主路径已由 dry-run + 锚点覆盖; + // 某一模型配置跑不通不应让整个构建失败(但不吞掉信息 → 打 [scan][warn])。 + const auto model_pass = [&](nn::ComputeEngine& engine, + const nn::PrecisionProfile& prof) + { + g_scan_prof = prof; + const std::size_t B = 3; + // 扫描期占位张量(精度 = 当前 pass 的 compute 精度,与真实运行同源) + const auto scan_tensor = [&](std::size_t rows, std::size_t cols) + { return engine.create_tensor(rows, cols, g_scan_prof.compute); }; + // 跑一个模型:forward → 取其输出形状造梯度 → backward + const auto run = [&](nn::Model& model, const nn::Tensor& input, const char* name) + { + auto out = model.forward(input); + if (!out) + { + std::fprintf(stderr, "[scan][warn] %s forward 失败:%s\n", name, + out.error().message.c_str()); + return; + } + nn::Tensor grad = scan_tensor(out->rows(), out->cols()); + auto br = model.backward(grad); + if (!br) + std::fprintf(stderr, "[scan][warn] %s backward 失败:%s\n", name, + br.error().message.c_str()); + }; + const auto try_build = [&](const char* name, auto&& build, const nn::Tensor& input) + { + auto m = build(); + if (!m) + { + std::fprintf(stderr, "[scan][warn] %s 构建失败:%s\n", name, + m.error().message.c_str()); + return; + } + run(*m, input, name); + }; + + // 1) MNIST MLP(784 → …) + try_build("mnist_mlp", + [&] { return nn::build_mnist_mlp_model(engine, nn::MNIST_LAYER_DIMS, + nn::NormType::LayerNorm, prof); }, + scan_tensor(nn::MNIST_LAYER_DIMS.front(), B)); + // 2) MNIST Transformer(ViT 风格;输入 img²) + try_build("mnist_transformer", + [&] { return nn::build_mnist_transformer_model( + engine, nn::MNIST_IMG_SIZE, nn::MNIST_PATCH_SIZE, + nn::MNIST_TF_D_MODEL, nn::MNIST_TF_NUM_HEADS, + nn::MNIST_TF_D_FF, nn::MNIST_TF_NUM_LAYERS, prof); }, + scan_tensor(nn::MNIST_IMG_SIZE * nn::MNIST_IMG_SIZE, B)); + // 3) CNN(LeNet 风格;输入 C·H·W) + try_build("cnn", + [&] { + nn::CnnConfig cfg; + cfg.convs = nn::MNIST_CNN_CONVS; + cfg.fc_dims = nn::MNIST_CNN_FC; + // 注:CnnConfig 无精制度字段(CNN 走默认 f32 profile) + return nn::build_cnn_model(engine, cfg); + }, + scan_tensor(1 * 28 * 28, B)); + // 4) GPT(小配置,Learned 位置编码)——输入是 token id (seq, batch), + // gather_rows 会按值取行,必须填**合法** id(否则越界) + { + constexpr std::size_t V = 64, S = 8, D = 16, H = 2, F = 32, L = 2; + nn::GptConfig cfg; + cfg.vocab_size = V; cfg.d_model = D; cfg.seq_len = S; + cfg.num_heads = H; cfg.d_ff = F; cfg.num_layers = L; + cfg.pos_enc = nn::PosEncodingType::Learned; + cfg.precision = prof; + // id 输入恒 f32(概念上是整数索引;text_train 同口径) + nn::Tensor ids = engine.create_tensor(S, B, nn::Precision::F32); + std::vector idv(S * B); + for (std::size_t i = 0; i < idv.size(); ++i) + idv[i] = static_cast(i % V); + (void)engine.write(ids, std::span(idv)); + try_build("gpt", [&] { return nn::build_gpt_model(engine, cfg); }, ids); + } + // 5) RAPT(RoPE + causal) + { + constexpr std::size_t V = 64, S = 8, D = 16, H = 2, F = 32, L = 2; + nn::RAPTConfig cfg; + cfg.vocab_size = V; cfg.d_model = D; cfg.seq_len = S; + cfg.num_heads = H; cfg.d_ff = F; cfg.num_layers = L; + cfg.pos_enc = nn::PosEncodingType::RoPE; + cfg.causal = true; + cfg.precision = prof; + nn::Tensor ids = engine.create_tensor(S, B, nn::Precision::F32); + std::vector idv(S * B); + for (std::size_t i = 0; i < idv.size(); ++i) + idv[i] = static_cast(i % V); + (void)engine.write(ids, std::span(idv)); + try_build("rapt", [&] { return nn::build_rapt_model(engine, cfg); }, ids); + } + }; + model_pass(raw_engine, nn::profile_f32()); // 结构 + 全 f32 签名 + model_pass(raw_engine, nn::profile_f16()); // 整模型路径的 f16 签名 // ── fold v1(标量域)分块状态归约(表达式集合登记)────────────────────────── // 三个共享样例(expr_fold.hpp——与 fused_gpu_test 对拍**同源构造** → // key 一致、闭合世界命中):rowmax / rowsum / softmax_denom(online 双 From 484ae3577ff1e820400f7db00e94e314ce6eb235 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 15:21:21 +0800 Subject: [PATCH 08/17] =?UTF-8?q?=E8=9E=8D=E5=90=88=20AOT=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E2=91=A5=C2=B7=E8=A1=A52=EF=BC=9A=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=20pass=20=E9=85=8D=E7=BD=AE=E7=9F=A9=E9=98=B5=20+=20=E5=88=86?= =?UTF-8?q?=E9=A1=B9=E6=9D=A5=E6=BA=90=E5=8F=B0=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在上一轮"模型级签名 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 逐字节一致(确定性)。 --- docs/development/02-operator-fusion.md | 8 +- tools/scan_exprs.cpp | 134 +++++++++++++++++-------- 2 files changed, 98 insertions(+), 44 deletions(-) diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index 34e1c668..227f85b8 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -245,7 +245,13 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba - **覆盖语义**:调用点在函数体里,**函数被编译即实例化**——与运行期是否走到该分支无关,因此是"编译期可达"而非"运行期可达"。A/B 实测(`-DNN_SCAN_NO_ANCHOR`):结构数 **72(仅 dry-run)→ 84(+锚点)**,多出的 12 条都是编译进来的真实调用点(3 条含 matmul、1 条归约、1 条 16 个 vp 槽的宽表达式、其余逐元素)。 - **常驻台账**:锚点登记进独立注册表 `anchor_registry()`,`scan_exprs` 末尾合并并打印 `结构来源:dry-run 72 + 锚点 59 → 合并 84(锚点独有 12)`。**锚点独有数突然变大 = 有新的层路径没纳入 dry-run**(该数字是回归信号)。 - **只登记结构(sig=0)**:⚠ **精度签名登记不出来**——同一个表达式里"哪些输入是 f16"由运行期张量精度决定(类型层面不可见)。实测扫描出的 60 个带类型变体里只有 16 个是"全 f16 输入+输出",其余 44 个是混合签名(`0x0001/0x0003/0x10005…`)。故 f16 带类型变体**必须**由 profile_f32/profile_f16 两遍 dry-run(或运行期 `NN_PREC_TRACE` + `prec_backfill.txt`)产生,**自登记无法取代它**。 -- **模型级签名 pass**(补签名覆盖):per-layer dry-run 块只覆盖"层被单独造出来"的路径;有些调用点只在**完整模型**里才执行(GPT/RAPT 的 LM head、PatchEmbedding、模型级位置编码等)。`scan_exprs` 因此在两个 profile 下各加一遍"用 shipped 工厂建 5 个模型 + 跑 fwd/bwd"(`mnist_mlp`/`mnist_transformer`/`cnn`/`gpt`/`rapt`;id 输入须填合法 token 值)。实测 **精度变体 60 → 72**,`prec_backfill.txt` 的"运行期才发现的签名"由 **7/7 降到 2/7**(即 5 个原本 run-only 的 f16 混合签名现在构建期即可预测)。该 pass 失败只打 `[scan][warn]` 不中断构建(它是增量,主路径已由 dry-run + 锚点覆盖)。 +- **模型级签名 pass**(补签名覆盖):per-layer dry-run 块只覆盖"层被单独造出来"的路径;有些调用点只在**完整模型**里才执行(GPT/RAPT 的 LM head、PatchEmbedding、模型级位置编码等)。`scan_exprs` 因此在两个 profile 下各跑一遍**配置矩阵**:`mnist_mlp`×{LayerNorm,RMSNorm,BatchNorm}、`mnist_transformer`(ViT)、`cnn`×{pool2,pool3}、`gpt`×{Learned,ALiBi,Sinusoidal,RoPE}×{GeLU,SwiGLU}×{LayerNorm,RMSNorm}、`rapt`×{causal,bidir}(小配置 vocab=64/d_model=16/seq=8/H=2/d_ff=32/L=2;id 输入须填合法 token 值)。实测:**签名 +13**(73 条),`prec_backfill.txt` 的"运行期才发现的签名"由 **7/7 降到 2/7**。该 pass 失败只打 `[scan][warn]` 不中断构建(它是增量,主路径已由 dry-run + 锚点覆盖)。 +- **常驻分项台账**(每次构建打印): + ``` + [scan] 结构来源:dry-run 64(含模型 pass +0、显式登记 +8) + 锚点 59 → 合并 84(锚点独有 12) + [scan] 签名来源:dry-run 60 + 模型 pass 13 + 显式登记 0 = 73 + ``` + A/B(`-DNN_SCAN_NO_DRYRUN` 关掉手写 per-layer 块)只剩 **76 结构 / 50 签名** → 即那批手写块目前仍贡献 **8 结构 + 23 签名**(其中 5 个结构是 `scan_exprs` 内的**显式测试覆盖登记**(matmul+bias+relu / bmm_reduce / reduce_consts),不是层 dry-run)。这两行是"手写清单还差多少才能删"的度量。 - 非法结构(裸视图作根 → 空指令表)在构建期即 `_Exit(3)`,与 dry-run 同一闸门。 - `-DNN_SCAN_NO_ANCHOR` 可关掉锚点做 A/B(只影响登记集合,不影响语义)。 diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index 29c3bd0d..4274efaf 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -82,7 +82,7 @@ int main(int argc, char* argv[]) // P-1 下沉后 f16 边界 cast 由基类 NVI 入口统一处理(原 PrecisionEngine // 适配层已删除,见 docs/development/15 §4.1);原生 CpuEngine 从未见过 // f16 张量(历史上直交属 UB,实测 heap corruption 0xC0000374)。 - const auto dry_run = [&](nn::ComputeEngine& engine, const nn::PrecisionProfile& prof) + [[maybe_unused]] const auto dry_run = [&](nn::ComputeEngine& engine, const nn::PrecisionProfile& prof) { g_scan_prof = prof; // M1(docs/development/17 §4.1):库外不再直构 Tensor——scan 输入经 @@ -589,8 +589,13 @@ int main(int argc, char* argv[]) } }; // dry_run 结束 +#if !defined(NN_SCAN_NO_DRYRUN) dry_run(raw_engine, nn::profile_f32()); // f32 pass:sig == 0(基础结构表) dry_run(raw_engine, nn::profile_f16()); // Phase 2:收集 (结构, 精度签名); +#endif + // 分项台账快照(末尾合并时打印;见"结构来源"行) + const std::size_t n_after_dry = nn::fused::global_registry().specs.size(); + const std::size_t v_after_dry = nn::fused::global_registry().variants.size(); // f16 安全由基类边界 cast 入口保证(原 PrecisionEngine 适配层已下沉删除) // ── 模型级 pass:用 shipped 工厂建模型跑 fwd/bwd(补"整模型路径"的签名)── @@ -637,64 +642,99 @@ int main(int argc, char* argv[]) run(*m, input, name); }; - // 1) MNIST MLP(784 → …) - try_build("mnist_mlp", - [&] { return nn::build_mnist_mlp_model(engine, nn::MNIST_LAYER_DIMS, - nn::NormType::LayerNorm, prof); }, - scan_tensor(nn::MNIST_LAYER_DIMS.front(), B)); - // 2) MNIST Transformer(ViT 风格;输入 img²) + // 小配置(覆盖各"结构分支":位置编码 / 激活 / 归一化 / 池化 / causal) + constexpr std::size_t V = 64, S = 8, D = 16, H = 2, F = 32, L = 2; + // id 输入:概念上是整数索引 → 恒 f32(text_train 同口径); + // gather_rows 按值取行,必须填**合法** id(否则越界) + const auto id_input = [&](std::size_t seq, std::size_t vocab) + { + nn::Tensor t = engine.create_tensor(seq, B, nn::Precision::F32); + std::vector v(seq * B); + for (std::size_t i = 0; i < v.size(); ++i) + v[i] = static_cast(i % vocab); + (void)engine.write(t, std::span(v)); + return t; + }; + const nn::Tensor ids = id_input(S, V); + + // ── 1) MNIST MLP:归一化维(LayerNorm/RMSNorm/BatchNorm 是三条不同 + // 融合表达式:归约链形状与原地更新都不同)── + for (const auto nrm : {nn::NormType::LayerNorm, nn::NormType::RMSNorm, + nn::NormType::BatchNorm}) + { + const char* nm = (nrm == nn::NormType::LayerNorm) ? "mnist_mlp_ln" + : (nrm == nn::NormType::RMSNorm) ? "mnist_mlp_rms" + : "mnist_mlp_bn"; + try_build(nm, + [&] { return nn::build_mnist_mlp_model( + engine, nn::MNIST_LAYER_DIMS, nrm, prof); }, + scan_tensor(nn::MNIST_LAYER_DIMS.front(), B)); + } + // ── 2) MNIST Transformer(ViT 风格;输入 img²)── try_build("mnist_transformer", [&] { return nn::build_mnist_transformer_model( engine, nn::MNIST_IMG_SIZE, nn::MNIST_PATCH_SIZE, nn::MNIST_TF_D_MODEL, nn::MNIST_TF_NUM_HEADS, nn::MNIST_TF_D_FF, nn::MNIST_TF_NUM_LAYERS, prof); }, scan_tensor(nn::MNIST_IMG_SIZE * nn::MNIST_IMG_SIZE, B)); - // 3) CNN(LeNet 风格;输入 C·H·W) - try_build("cnn", - [&] { - nn::CnnConfig cfg; - cfg.convs = nn::MNIST_CNN_CONVS; - cfg.fc_dims = nn::MNIST_CNN_FC; - // 注:CnnConfig 无精制度字段(CNN 走默认 f32 profile) - return nn::build_cnn_model(engine, cfg); - }, - scan_tensor(1 * 28 * 28, B)); - // 4) GPT(小配置,Learned 位置编码)——输入是 token id (seq, batch), - // gather_rows 会按值取行,必须填**合法** id(否则越界) + // ── 3) CNN:池化窗口维(R 是运行期视图参数,pool=3 → R=9)── + for (const std::size_t pool : {std::size_t{2}, std::size_t{3}}) + { + nn::CnnConfig cfg; + cfg.convs = nn::MNIST_CNN_CONVS; + cfg.fc_dims = nn::MNIST_CNN_FC; + cfg.pool = pool; + // 注:CnnConfig 无精制度字段(CNN 走默认 f32 profile) + try_build(pool == 2 ? "cnn_pool2" : "cnn_pool3", + [&] { return nn::build_cnn_model(engine, cfg); }, + scan_tensor(1 * 28 * 28, B)); + } + // ── 4) GPT:位置编码 × 激活 × 归一化(每一维都换一组融合表达式: + // ALiBi 走分数侧偏置、Sinusoidal 走嵌入侧、RoPE 走 Q/K 侧、 + // SwiGLU ≠ GeLU、RMSNorm ≠ LayerNorm)── + struct GptCase + { + const char* name; + nn::PosEncodingType pe; + nn::ActivationType act; + nn::NormType norm; + }; + for (const GptCase& c : { + GptCase{"gpt_learned_gelu_ln", nn::PosEncodingType::Learned, + nn::ActivationType::GeLU, nn::NormType::LayerNorm}, + GptCase{"gpt_alibi_gelu_ln", nn::PosEncodingType::ALiBi, + nn::ActivationType::GeLU, nn::NormType::LayerNorm}, + GptCase{"gpt_sinusoidal_gelu_ln", nn::PosEncodingType::Sinusoidal, + nn::ActivationType::GeLU, nn::NormType::LayerNorm}, + GptCase{"gpt_rope_gelu_ln", nn::PosEncodingType::RoPE, + nn::ActivationType::GeLU, nn::NormType::LayerNorm}, + GptCase{"gpt_learned_swiglu_rms", nn::PosEncodingType::Learned, + nn::ActivationType::SwiGLU, nn::NormType::RMSNorm}}) { - constexpr std::size_t V = 64, S = 8, D = 16, H = 2, F = 32, L = 2; nn::GptConfig cfg; cfg.vocab_size = V; cfg.d_model = D; cfg.seq_len = S; cfg.num_heads = H; cfg.d_ff = F; cfg.num_layers = L; - cfg.pos_enc = nn::PosEncodingType::Learned; + cfg.pos_enc = c.pe; cfg.activation = c.act; cfg.norm_type = c.norm; cfg.precision = prof; - // id 输入恒 f32(概念上是整数索引;text_train 同口径) - nn::Tensor ids = engine.create_tensor(S, B, nn::Precision::F32); - std::vector idv(S * B); - for (std::size_t i = 0; i < idv.size(); ++i) - idv[i] = static_cast(i % V); - (void)engine.write(ids, std::span(idv)); - try_build("gpt", [&] { return nn::build_gpt_model(engine, cfg); }, ids); + try_build(c.name, [&] { return nn::build_gpt_model(engine, cfg); }, ids); } - // 5) RAPT(RoPE + causal) + // ── 5) RAPT:causal 维(RLA 的因果/双向是两组不同表达式)── + for (const bool causal : {true, false}) { - constexpr std::size_t V = 64, S = 8, D = 16, H = 2, F = 32, L = 2; nn::RAPTConfig cfg; cfg.vocab_size = V; cfg.d_model = D; cfg.seq_len = S; cfg.num_heads = H; cfg.d_ff = F; cfg.num_layers = L; - cfg.pos_enc = nn::PosEncodingType::RoPE; - cfg.causal = true; + cfg.pos_enc = nn::PosEncodingType::RoPE; // RAPT v1 仅支持 RoPE + cfg.causal = causal; cfg.precision = prof; - nn::Tensor ids = engine.create_tensor(S, B, nn::Precision::F32); - std::vector idv(S * B); - for (std::size_t i = 0; i < idv.size(); ++i) - idv[i] = static_cast(i % V); - (void)engine.write(ids, std::span(idv)); - try_build("rapt", [&] { return nn::build_rapt_model(engine, cfg); }, ids); + try_build(causal ? "rapt_causal" : "rapt_bidir", + [&] { return nn::build_rapt_model(engine, cfg); }, ids); } }; model_pass(raw_engine, nn::profile_f32()); // 结构 + 全 f32 签名 model_pass(raw_engine, nn::profile_f16()); // 整模型路径的 f16 签名 + const std::size_t n_after_model = nn::fused::global_registry().specs.size(); + const std::size_t v_after_model = nn::fused::global_registry().variants.size(); // ── fold v1(标量域)分块状态归约(表达式集合登记)────────────────────────── // 三个共享样例(expr_fold.hpp——与 fused_gpu_test 对拍**同源构造** → // key 一致、闭合世界命中):rowmax / rowsum / softmax_denom(online 双 @@ -749,22 +789,30 @@ int main(int argc, char* argv[]) } } + const std::size_t n_before_anchor = nn::fused::global_registry().specs.size(); + const std::size_t v_before_anchor = nn::fused::global_registry().variants.size(); auto& reg = nn::fused::global_registry(); // ── 合并自登记锚点的结构("结构 = 表达式类型",见 expr_dsl.hpp)───────── // 锚点在静态初始化期按**类型**登记,覆盖"编译进来但 dry-run 未执行到"的 // 调用点(配置分支、未被 dry-run 造过的层路径)。两者合并后写盘。 { auto& anchor = nn::dsl::anchor_registry(); - const std::size_t dry_only = reg.specs.size(); for (const auto& s : anchor.specs) reg.add(s, 0u); - std::printf("[scan] 结构来源:dry-run %zu + 锚点 %zu → 合并 %zu(锚点独有 %zu)\n", - dry_only, anchor.specs.size(), reg.specs.size(), - reg.specs.size() - dry_only); + std::printf("[scan] 结构来源:dry-run %zu(含模型 pass +%zu、显式登记 +%zu)" + " + 锚点 %zu → 合并 %zu(锚点独有 %zu)\n", + n_after_dry, + n_after_model - n_after_dry, + n_before_anchor - n_after_model, + anchor.specs.size(), reg.specs.size(), + reg.specs.size() - n_before_anchor); + std::printf("[scan] 签名来源:dry-run %zu + 模型 pass %zu + 显式登记 %zu = %zu\n", + v_after_dry, v_after_model - v_after_dry, + v_before_anchor - v_after_model, v_before_anchor); // 锚点独有 = 手写 dry-run 覆盖不到的调用点(该数突然变大 → 有新层路径 // 没纳入 dry-run)。锚点只登记结构(sig=0),故这些结构默认没有 f16 // 带类型变体 —— 下面单独报告。 - if (anchor.specs.empty() || dry_only == reg.specs.size()) + if (anchor.specs.empty() || n_before_anchor == reg.specs.size()) std::fprintf(stderr, "[scan][warn] 锚点未贡献任何结构:可能被 -DNN_SCAN_NO_ANCHOR 关闭," "或所有调用点都已被 dry-run 覆盖\n"); From 98f887a019a543e1d5ab1c620e096ceaf54df61e Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 15:35:17 +0800 Subject: [PATCH 09/17] =?UTF-8?q?=E8=9E=8D=E5=90=88=20AOT=20=E6=AD=A5?= =?UTF-8?q?=E9=AA=A4=E2=91=A5=C2=B7=E8=A1=A53=EF=BC=9A=E5=88=A0=E9=99=A4?= =?UTF-8?q?=2013=20=E4=B8=AA=E5=B7=B2=E8=A2=AB=E6=A8=A1=E5=9E=8B=20pass=20?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E7=9A=84=20per-layer=20dry-run=20=E5=9D=97?= =?UTF-8?q?=EF=BC=88=E2=88=92223=20=E8=A1=8C=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一轮把"手写块的残余贡献"量化到 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` 逐位不变。 --- docs/development/02-operator-fusion.md | 15 +- tools/scan_exprs.cpp | 223 ------------------------- 2 files changed, 10 insertions(+), 228 deletions(-) diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index 227f85b8..bb38c7d5 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -243,15 +243,20 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba - **前提 = "结构 = 表达式类型"**:为此步骤①②③把 `consts` 值、`matmul.transA/transB`、`GroupedReduce.R` 逐出 key(分别改走 push constant),并把 `GroupedReduceRef` 的 `is_max`(决定视图 kind)提为模板参数。 - **覆盖语义**:调用点在函数体里,**函数被编译即实例化**——与运行期是否走到该分支无关,因此是"编译期可达"而非"运行期可达"。A/B 实测(`-DNN_SCAN_NO_ANCHOR`):结构数 **72(仅 dry-run)→ 84(+锚点)**,多出的 12 条都是编译进来的真实调用点(3 条含 matmul、1 条归约、1 条 16 个 vp 槽的宽表达式、其余逐元素)。 -- **常驻台账**:锚点登记进独立注册表 `anchor_registry()`,`scan_exprs` 末尾合并并打印 `结构来源:dry-run 72 + 锚点 59 → 合并 84(锚点独有 12)`。**锚点独有数突然变大 = 有新的层路径没纳入 dry-run**(该数字是回归信号)。 +- **常驻台账**:锚点登记进独立注册表 `anchor_registry()`,`scan_exprs` 末尾合并并打印来源分项(见下)。**锚点独有数突然变大 = 有新的层路径没纳入任何驱动**(回归信号)。 - **只登记结构(sig=0)**:⚠ **精度签名登记不出来**——同一个表达式里"哪些输入是 f16"由运行期张量精度决定(类型层面不可见)。实测扫描出的 60 个带类型变体里只有 16 个是"全 f16 输入+输出",其余 44 个是混合签名(`0x0001/0x0003/0x10005…`)。故 f16 带类型变体**必须**由 profile_f32/profile_f16 两遍 dry-run(或运行期 `NN_PREC_TRACE` + `prec_backfill.txt`)产生,**自登记无法取代它**。 -- **模型级签名 pass**(补签名覆盖):per-layer dry-run 块只覆盖"层被单独造出来"的路径;有些调用点只在**完整模型**里才执行(GPT/RAPT 的 LM head、PatchEmbedding、模型级位置编码等)。`scan_exprs` 因此在两个 profile 下各跑一遍**配置矩阵**:`mnist_mlp`×{LayerNorm,RMSNorm,BatchNorm}、`mnist_transformer`(ViT)、`cnn`×{pool2,pool3}、`gpt`×{Learned,ALiBi,Sinusoidal,RoPE}×{GeLU,SwiGLU}×{LayerNorm,RMSNorm}、`rapt`×{causal,bidir}(小配置 vocab=64/d_model=16/seq=8/H=2/d_ff=32/L=2;id 输入须填合法 token 值)。实测:**签名 +13**(73 条),`prec_backfill.txt` 的"运行期才发现的签名"由 **7/7 降到 2/7**。该 pass 失败只打 `[scan][warn]` 不中断构建(它是增量,主路径已由 dry-run + 锚点覆盖)。 +- **模型级签名 pass**(补签名覆盖):per-layer dry-run 块只覆盖"层被单独造出来"的路径;有些调用点只在**完整模型**里才执行(GPT/RAPT 的 LM head、PatchEmbedding、模型级位置编码等)。`scan_exprs` 因此在两个 profile 下各跑一遍**配置矩阵**:`mnist_mlp`×{LayerNorm,RMSNorm,BatchNorm}、`mnist_transformer`(ViT)、`cnn`×{pool2,pool3}、`gpt`×{Learned,ALiBi,Sinusoidal,RoPE}×{GeLU,SwiGLU}×{LayerNorm,RMSNorm}、`rapt`×{causal,bidir}(小配置 vocab=64/d_model=16/seq=8/H=2/d_ff=32/L=2;id 输入须填合法 token 值)。该 pass 失败只打 `[scan][warn]` 不中断构建(它是增量,主路径已由 dry-run + 锚点覆盖)。 +- **已验证可删:13 个 per-layer dry-run 块(−223 行)**。模型 pass 覆盖了这些层路径后,`scan_exprs` 里 ReLU/SwiGLU/GeLU/Softmax/RMSNorm/LayerNorm/RLA(causal+bidir)/GPTBlock/TransformerEncoderLayer/Linear/Conv2D/MaxPool2D 的独立 dry-run 块**已删除**,判据是两条硬证据: + 1. **结构集合逐字节不变**(84 条,bin 内容一致); + 2. **运行时 miss 集合不变**:`NN_PREC_TRACE=1 mem_probe --f16` 的 5 个工作负载(默认 / `--doc-mask` / `--checkpoint-every 2` / `--optimizer muon` / `--activation-offload`)在删除前后都是同一个 miss(仅 fold doc-mask 一条,见下)。 + 保留的是**整模型跑不到**的来源:优化器(5 变体)、损失(MSE / CE 稠密 / CE 稀疏)、RoPE `apply_step`(增量推理)、CSA×4(掩码/偏置组合,含模型未覆盖的 doc 变体)、MHA、以及 `scan_exprs` 内的**显式测试覆盖登记**(matmul+bias+relu / bmm_reduce / reduce_consts)。 - **常驻分项台账**(每次构建打印): ``` - [scan] 结构来源:dry-run 64(含模型 pass +0、显式登记 +8) + 锚点 59 → 合并 84(锚点独有 12) - [scan] 签名来源:dry-run 60 + 模型 pass 13 + 显式登记 0 = 73 + [scan] 结构来源:dry-run 38(含模型 pass +25、显式登记 +8) + 锚点 59 → 合并 84(锚点独有 13) + [scan] 签名来源:dry-run 32 + 模型 pass 34 + 显式登记 0 = 66 ``` - A/B(`-DNN_SCAN_NO_DRYRUN` 关掉手写 per-layer 块)只剩 **76 结构 / 50 签名** → 即那批手写块目前仍贡献 **8 结构 + 23 签名**(其中 5 个结构是 `scan_exprs` 内的**显式测试覆盖登记**(matmul+bias+relu / bmm_reduce / reduce_consts),不是层 dry-run)。这两行是"手写清单还差多少才能删"的度量。 + A/B 开关:`-DNN_SCAN_NO_ANCHOR`(关锚点)/ `-DNN_SCAN_NO_DRYRUN`(关手写块),均只影响登记集合、不改语义。 +- **run-only 签名回填仍是兜底**:`--f16 --doc-mask` 会命中一条 fold 的 5 输入形态签名(`74a6eaacc0e5d766 10007`,`[f16,f16,f16,f32,f32] out=f16`)——scan 的 fold 显式块按"掩码×偏置"登记结构,但该形态的 f16 签名只有真实 doc-mask 训练才暴露;已加入 `prec_backfill.txt`,复跑 miss=0。这是"签名必须执行期发现"的最后兜底通道。 - 非法结构(裸视图作根 → 空指令表)在构建期即 `_Exit(3)`,与 dry-run 同一闸门。 - `-DNN_SCAN_NO_ANCHOR` 可关掉锚点做 A/B(只影响登记集合,不影响语义)。 diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index 4274efaf..30da9db0 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -113,151 +113,6 @@ int main(int argc, char* argv[]) (void)rope.apply_step(engine, q1, /*pos=*/3, /*backward=*/true); } - // ── ReLU forward + backward(DSL 融合,CNN/RAPT 模型使用)──────────────── - // forward: max(x, 0);backward: select(x>0, grad, 0)。 - // 结构不依赖形状,任取一个 R×C 即可;先 forward 填 input_cache_ 再 backward。 - { - const std::size_t R = 6, C = 9; - nn::ReLU relu; - relu.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)relu.init(engine); // M6 段 C:层由 init 绑定引擎 - nn::Tensor input = scan_tensor(R, C); - (void)relu.forward(input); - nn::Tensor grad = scan_tensor(R, C); - (void)relu.backward(grad); - } - - // ── SwiGLU backward(grad_gate / grad_up 两条内联表达式)────────────── - // 结构不依赖 d_ff,任取一个即可;先 forward 填充 cache 再 backward。 - { - const std::size_t d_ff = 8; - nn::SwiGLU swiglu(d_ff); - swiglu.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)swiglu.init(engine); // M6 段 C:层由 init 绑定引擎 - nn::Tensor input = scan_tensor(2 * d_ff, 5); - (void)swiglu.forward(input); - nn::Tensor grad = scan_tensor(d_ff, 5); - (void)swiglu.backward(grad); - } - - // ── GeLU forward + backward(QuickGeLU 单表达式 DSL 融合)───────────── - // forward: x / (1 + exp(-βx));backward: grad_out * s*(1 + βx*(1-s)), - // s = sigmoid(βx)。sigmoid 在 backward 用 input_cache_ 重算。 - // 结构不依赖形状,任取一个 R×C 即可;先 forward 填 input_cache_ 再 backward。 - { - const std::size_t R = 6, C = 9; - nn::GeLU gelu; - gelu.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)gelu.init(engine); // M6 段 C:层由 init 绑定引擎 - nn::Tensor input = scan_tensor(R, C); - (void)gelu.forward(input); - nn::Tensor grad = scan_tensor(R, C); - (void)gelu.backward(grad); - } - - // ── Softmax forward + backward(行归约融合)──────────────────────── - // forward: exp(x - row_max) / row_sum(exp(x - row_max)) - // backward: out * (grad - row_dot(out * grad)) - // 结构不依赖形状,任取一个 R×C 即可。 - { - const std::size_t R = 6, C = 9; - nn::Softmax softmax; - softmax.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)softmax.init(engine); // M6 段 C:层由 init 绑定引擎 - nn::Tensor input = scan_tensor(R, C); - (void)softmax.forward(input); // 填充 output_cache_ + 登记 fwd 结构 - nn::Tensor grad = scan_tensor(R, C); - (void)softmax.backward(grad); // 登记 bwd 结构 - } - - // ── RMSNorm forward + backward(融合)──────────────────── - // forward: s=col_sum(x²)*invF+eps → rms_inv=rsqrt(s) → normed=x*rms_inv → out=normed*gamma - // backward: grad_x 列归约表达式 + grad_gamma 行归约表达式 - { - const std::size_t F = 8, B = 5; - nn::RMSNorm rms(F); - rms.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)rms.init(engine); - nn::Tensor input = scan_tensor(F, B); - (void)rms.forward(input); // 填充 normed/rms_inv 缓存 + 登记 fwd 结构 - nn::Tensor grad = scan_tensor(F, B); - (void)rms.backward(grad); // 登记 bwd 结构 - } - - // ── LayerNorm forward + backward(融合)────────────────── - // forward: mean → diff → var → std_inv → normalized → out=normalized*gamma+beta - // backward: grad_x 列归约表达式 + grad_gamma/grad_beta 行归约表达式 - { - const std::size_t F = 8, B = 5; - nn::LayerNorm ln(F); - ln.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)ln.init(engine); - nn::Tensor input = scan_tensor(F, B); - (void)ln.forward(input); - nn::Tensor grad = scan_tensor(F, B); - (void)ln.backward(grad); - } - - // ── ReLULinearAttention forward + backward(RLA 原语组合版逐元素链)── - // forward: Sm=S·mask, S2=Sm·Sm, denom=sqrt(rowsum(S2)+1e-6), out=num/denom - // backward: dnum=dO/denom, prod=dO·num, ddenom=-dot/(denom²), - // ddenom2=ddenom/(2·denom), dSm_den=2·Sm·ddenom2, dSm=dSm_num+dSm_den, - // dS=dSm·mask - // 结构不依赖形状,任取一个小 d_model/seq 即可。 - { - const std::size_t d_model = 8, heads = 2, seq = 4, batch = 2; - nn::ReLULinearAttention attn(d_model, heads, seq, /*causal=*/true, - nn::PosEncodingType::RoPE); - attn.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)attn.init(engine); - nn::Tensor x = scan_tensor(d_model, batch * seq); - (void)attn.forward(x); // 填 cache + 登记 fwd 结构 - nn::Tensor grad = scan_tensor(d_model, batch * seq); - (void)attn.backward(grad); // 登记 bwd 结构 - } - - // ── ReLULinearAttention(**双向** causal=false)forward + backward ─── - // 双向分支与因果分支表达式不同(Bb = broadcast_row(dB_sum); - // tsq = row_reduce_sum(scale·q);gk = gK_B + row_broadcast(tsq)), - // 必须单独 dry-run:否则 GPU 运行到该分支未命中融合 shader 会硬报错。 - { - const std::size_t d_model = 8, heads = 2, seq = 4, batch = 2; - nn::ReLULinearAttention attn_nc(d_model, heads, seq, /*causal=*/false, - nn::PosEncodingType::RoPE); - attn_nc.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)attn_nc.init(engine); - nn::Tensor x = scan_tensor(d_model, batch * seq); - (void)attn_nc.forward(x); - nn::Tensor grad = scan_tensor(d_model, batch * seq); - (void)attn_nc.backward(grad); - } - - // ── GPTBlock forward + backward(残差相加 A+B)──────────────────────── - { - const std::size_t d_model = 16, heads = 2, d_ff = 32, seq = 4, batch = 2; - nn::GPTBlock block(d_model, heads, d_ff, /*max_len=*/1024, /*seq_len=*/seq, - nn::PosEncodingType::Learned, nn::ActivationType::GeLU, - nn::NormType::LayerNorm); - block.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)block.init(engine); - nn::Tensor x = scan_tensor(d_model, batch * seq); - (void)block.forward(x); - nn::Tensor grad = scan_tensor(d_model, batch * seq); - (void)block.backward(grad); - } - - // ── TransformerEncoderLayer forward + backward(残差相加 + 位置编码)── - { - const std::size_t d_model = 16, heads = 2, d_ff = 32, seq = 4, batch = 2; - nn::TransformerEncoderLayer enc(d_model, heads, d_ff, seq); - enc.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)enc.init(engine); - nn::Tensor x = scan_tensor(d_model, batch * seq); - (void)enc.forward(x); - nn::Tensor grad = scan_tensor(d_model, batch * seq); - (void)enc.backward(grad); - } - // ── MSELoss forward(diff = pred-target;diff_sq = diff*diff)──────── { const std::size_t R = 8, C = 5; @@ -307,24 +162,6 @@ int main(int argc, char* argv[]) } } - // ── Linear forward + backward(算子融合二期 S4:matmul+bias 融合路径)── - // Linear::forward 实现为 dsl::compute(matmul(W,x) + row_broadcast(b)): - // 折叠出前置 matmul 段 + 尾逐元素链(Add + RowBroadcast 视图)。 - // Linear::backward 的 grad_w 累加实现为 - // dsl::compute(grad_w + matmul(grad_out, input^T))(matmul 段 + Add 融合)。 - // 两者都必须 dry-run 覆盖:GPU 运行时同一结构命中 AOT 融合 shader(闭合 - // 世界两端一致)。结构不依赖形状,任取一个 in/out/batch 即可。 - { - const std::size_t in_f = 8, out_f = 5, B = 4; - nn::Linear linear(in_f, out_f); - linear.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)linear.init(engine); - nn::Tensor input = scan_tensor(in_f, B); - (void)linear.forward(input); - nn::Tensor grad_out = scan_tensor(out_f, B); - (void)linear.backward(grad_out); // 登记 grad_w 融合结构 - } - // ── 算子融合二期(docs/development/02-operator-fusion.md):matmul 参与 IR 融合 ── // 结构 = Layer 内 dsl::matmul(A,B)+bias+relu 折叠后的派生物: // 前置 matmul 段(MatmulSpec)+ 尾逐元素链(Add + Max)。 @@ -511,66 +348,6 @@ int main(int argc, char* argv[]) (void)ce.forward_sparse(engine, logits, labels2, {}, C); } - // ── Conv2D forward + backward(matmul 段融合 bias / 融合累加)────────── - // forward : Z = matmul(W, im2col(x)) + row_broadcast(b) —— matmul 段 + 尾链 - // backward: grad_w += matmul(gZ, col^T) —— matmul 段 + 原地累加(compute_into) - // 两者都必须 dry-run:GPU 运行时同一结构才能命中 AOT 融合 shader。 - // 结构不依赖形状,任取小尺寸即可。 - { - const std::size_t c_in = 1, c_out = 2, k = 3, in_h = 5, in_w = 5, batch = 2; - nn::Conv2D conv(c_in, c_out, k, /*stride=*/1, /*padding=*/0, in_h, in_w); - conv.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)conv.init(engine); - const std::size_t oh = in_h - k + 1, ow = in_w - k + 1; - nn::Tensor x = scan_tensor(c_in * in_h * in_w, batch); - if (auto fr = conv.forward(x); !fr) - { - std::fprintf(stderr, "[scan] Conv2D forward FAILED: %s\n", - fr.error().message.c_str()); - std::fflush(stderr); - std::abort(); - } - nn::Tensor grad = scan_tensor(c_out * oh * ow, batch); - if (auto br = conv.backward(grad); !br) - { - std::fprintf(stderr, "[scan] Conv2D backward FAILED: %s\n", - br.error().message.c_str()); - std::fflush(stderr); - std::abort(); - } - } - - // ── MaxPool2D forward + backward(分组归约视图 + 窗口 mask 表达式)──────── - // forward : im2col → grouped_reduce_max 视图(+ rparam(0) 使根节点合法) - // backward: mask = select(窗口 == 广播(窗口max), 广播(g/cnt), 0), - // cnt = grouped_reduce_sum 视图(同样 + rparam(0)) - // —— **必须 dry-run**:该表达式未被扫描覆盖时 - // GPU 运行到它会因闭合世界未命中而硬报错。 - { - const std::size_t c = 2, in_h = 6, in_w = 6, pool = 2, stride = 2, batch = 2; - nn::MaxPool2D mp(c, in_h, in_w, pool, stride); - mp.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) - (void)mp.init(engine); // M6 段 C:层由 init 绑定引擎 - const std::size_t oh = (in_h - pool) / stride + 1; - const std::size_t ow = (in_w - pool) / stride + 1; - nn::Tensor x = scan_tensor(c * in_h * in_w, batch); - if (auto fr = mp.forward(x); !fr) - { - std::fprintf(stderr, "[scan] MaxPool2D forward FAILED: %s\n", - fr.error().message.c_str()); - std::fflush(stderr); - std::abort(); - } - nn::Tensor grad = scan_tensor(c * oh * ow, batch); - if (auto br = mp.backward(grad); !br) - { - std::fprintf(stderr, "[scan] MaxPool2D backward FAILED: %s\n", - br.error().message.c_str()); - std::fflush(stderr); - std::abort(); - } - } - // ── 归约表达式内联常量(push-constant 头长度回归)──────────────────── // `col_reduce_sum(select(cond, 1, 0))` 是**带常量池的归约**结构:GPU 侧 // push-constant 固定头长度必须按形态算(归约且无 matmul = 4 个 uint)。 From 226b80f48e2a177f1f19e30120a0a6b7a3a28f6c Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 15:35:30 +0800 Subject: [PATCH 10/17] =?UTF-8?q?run-only=20=E7=AD=BE=E5=90=8D=E5=9B=9E?= =?UTF-8?q?=E5=A1=AB=EF=BC=9Afold=20=C2=B7=20=E6=96=87=E6=A1=A3=E6=8E=A9?= =?UTF-8?q?=E7=A0=81=20f16=20=E5=8F=98=E4=BD=93=EF=BC=88--f16=20--doc-mask?= =?UTF-8?q?=20=E7=9A=84=20miss=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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` 逐位不变(本提交不影响运行期数值)。 --- tools/prec_backfill.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/prec_backfill.txt b/tools/prec_backfill.txt index 4b514400..273674e5 100644 --- a/tools/prec_backfill.txt +++ b/tools/prec_backfill.txt @@ -28,3 +28,9 @@ c0006fc8ea6a0e6b 0002 # 注意力 forward 单 fold kernel(Phase D2:fold 带类型变体;3 输入全 f16、 # 输出 f16 = Q/K/V 直读 + O 落回,算术/状态进位全 f32) 66b03ef936bb97c7 10007 + +# 注意力 forward 单 fold kernel · 文档掩码变体(5 输入 [f16,f16,f16,f32,f32]: +# Q/K/V f16 直读 + 文档 id/边界 f32,输出 f16)——`mem_probe --f16 --doc-mask` +# 的 [prec][miss] 复现;scan 的 fold 显式块按 (掩码×偏置) 组合登记结构, +# 但该 5 输入形态的 f16 签名只有真实 doc-mask 训练才暴露 +74a6eaacc0e5d766 10007 From 145a267dc211156a36d9f21d753b47353dcb946e Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 15:51:25 +0800 Subject: [PATCH 11/17] =?UTF-8?q?run-only=20=E7=AD=BE=E5=90=8D=E5=9B=9E?= =?UTF-8?q?=E5=A1=AB=EF=BC=9AMNIST/CNN=20f16=20=E8=AE=AD=E7=BB=83=E7=9A=84?= =?UTF-8?q?=208=20=E6=9D=A1=20miss=EF=BC=88=E5=90=AB=20per-layer=20?= =?UTF-8?q?=E5=9D=97=E5=88=A0=E9=99=A4=E5=BC=95=E5=85=A5=E7=9A=84=202=20?= =?UTF-8?q?=E6=9D=A1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对上一轮的块删除做了**跨路径**的 miss 验证(此前只验了 GPT 路径的 mem_probe), 结果发现 MNIST/CNN f16 训练本来就有 miss,且删除又新增了 2 条: 基线对比(`NN_PREC_TRACE=1 mnist_train --arch --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` 逐位不变(本提交不触碰库代码,只增加构建期变体)。 --- docs/development/02-operator-fusion.md | 5 ++++- tools/prec_backfill.txt | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index bb38c7d5..013182d3 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -256,7 +256,10 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba [scan] 签名来源:dry-run 32 + 模型 pass 34 + 显式登记 0 = 66 ``` A/B 开关:`-DNN_SCAN_NO_ANCHOR`(关锚点)/ `-DNN_SCAN_NO_DRYRUN`(关手写块),均只影响登记集合、不改语义。 -- **run-only 签名回填仍是兜底**:`--f16 --doc-mask` 会命中一条 fold 的 5 输入形态签名(`74a6eaacc0e5d766 10007`,`[f16,f16,f16,f32,f32] out=f16`)——scan 的 fold 显式块按"掩码×偏置"登记结构,但该形态的 f16 签名只有真实 doc-mask 训练才暴露;已加入 `prec_backfill.txt`,复跑 miss=0。这是"签名必须执行期发现"的最后兜底通道。 +- **run-only 签名回填仍是兜底**:`--f16` 下有些签名只有真实训练才暴露,靠 `NN_PREC_TRACE=1` 的 `[prec][miss]` 收集后写进 `tools/prec_backfill.txt`。已完成两批: + 1. 注意力 forward 单 fold kernel 的**文档掩码** 5 输入形态(`74a6eaacc0e5d766 10007`,`[f16,f16,f16,f32,f32] out=f16`),复现 `mem_probe --f16 --doc-mask`; + 2. **MNIST/CNN f16 训练**的 8 条(`59e079001367d11b 10000`、`fe93c0d99c16113f 10000/10002`、`cb3d830f25b84895 10001`、`b7e6d363af1963ac 10000/10002`、`8a29d02213a0e196 10003`、`27c7edd7a0a4f808 10007`)。成因:MLP/CNN 的输入与部分中间张量在 master-weights 配方下是 **f32 存储** → 出现"f32 入 + f16 出"的混合签名,占位张量按 compute 精度造的 dry-run 预测不到;这些 miss 正是 `--f16` 训练里边界 cast(物化 f32 副本)的大头。 + 逐条复现命令写在清单注释里。**验证**:`mnist_train --arch {mlp,cnn,transformer} --f16` × `mem_probe --f16`(默认 / `--doc-mask`)共 5 个负载 **miss 全为 0**。 - 非法结构(裸视图作根 → 空指令表)在构建期即 `_Exit(3)`,与 dry-run 同一闸门。 - `-DNN_SCAN_NO_ANCHOR` 可关掉锚点做 A/B(只影响登记集合,不影响语义)。 diff --git a/tools/prec_backfill.txt b/tools/prec_backfill.txt index 273674e5..1ffbb5f1 100644 --- a/tools/prec_backfill.txt +++ b/tools/prec_backfill.txt @@ -34,3 +34,19 @@ c0006fc8ea6a0e6b 0002 # 的 [prec][miss] 复现;scan 的 fold 显式块按 (掩码×偏置) 组合登记结构, # 但该 5 输入形态的 f16 签名只有真实 doc-mask 训练才暴露 74a6eaacc0e5d766 10007 + +# ── MNIST / CNN f16 训练的 run-only 签名(2026-10-01)───────────────────── +# 复现:NN_PREC_TRACE=1 mnist_train --arch {mlp,cnn,transformer} --f16 +# --epochs 1 --batch-size 8 --max-samples 32 --gpu= +# 成因:MLP/CNN 的输入与部分中间张量在 master-weights 配方下是 **f32 存储** +# (compute=f16 但输入来自数据集/上一层的 f32 缓冲)→ 出现"f32 入 + f16 出" +# 的混合签名,scan 的 dry-run(占位张量按 compute 精度造)预测不到。 +# 收益:这些是 `--f16` 训练里 miss 时走边界 cast(物化 f32 副本)的大头。 +59e079001367d11b 10000 +fe93c0d99c16113f 10000 +cb3d830f25b84895 10001 +fe93c0d99c16113f 10002 +b7e6d363af1963ac 10000 +8a29d02213a0e196 10003 +b7e6d363af1963ac 10002 +27c7edd7a0a4f808 10007 From 0c681975975a35feb8affdec74a0805302d191b5 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 15:55:54 +0800 Subject: [PATCH 12/17] =?UTF-8?q?AOT=20=E8=9E=8D=E5=90=88=20key=20?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=E6=B5=8B=E8=AF=95=EF=BC=9A=E6=8A=8A"?= =?UTF-8?q?=E7=BB=93=E6=9E=84=20=3D=20=E8=A1=A8=E8=BE=BE=E5=BC=8F=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B"=E9=94=81=E8=BF=9B=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FusedAnchor` 按表达式**类型**登记结构,其正确性完全依赖"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` 逐位不变。 --- docs/development/02-operator-fusion.md | 4 + src/expr_cpu_test.cpp | 10 ++ src/expr_fused_key_test.cpp | 152 +++++++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 src/expr_fused_key_test.cpp diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index 013182d3..ce3da1c2 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -260,6 +260,10 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba 1. 注意力 forward 单 fold kernel 的**文档掩码** 5 输入形态(`74a6eaacc0e5d766 10007`,`[f16,f16,f16,f32,f32] out=f16`),复现 `mem_probe --f16 --doc-mask`; 2. **MNIST/CNN f16 训练**的 8 条(`59e079001367d11b 10000`、`fe93c0d99c16113f 10000/10002`、`cb3d830f25b84895 10001`、`b7e6d363af1963ac 10000/10002`、`8a29d02213a0e196 10003`、`27c7edd7a0a4f808 10007`)。成因:MLP/CNN 的输入与部分中间张量在 master-weights 配方下是 **f32 存储** → 出现"f32 入 + f16 出"的混合签名,占位张量按 compute 精度造的 dry-run 预测不到;这些 miss 正是 `--f16` 训练里边界 cast(物化 f32 副本)的大头。 逐条复现命令写在清单注释里。**验证**:`mnist_train --arch {mlp,cnn,transformer} --f16` × `mem_probe --f16`(默认 / `--doc-mask`)共 5 个负载 **miss 全为 0**。 +- **契约回归**:上述三条 key 语义 + "符号实例 key ≡ 真实实例 key" 由 + `src/expr_fused_key_test.cpp` 锁定(并入 `expr_cpu_test` 聚合目标,ctest 目标数不变)。 + 任一条被违反(有人重新引入"运行期值决定结构")→ 运行时 key 与登记 key 不一致 + → 闭合世界 miss;测试会立刻红。 - 非法结构(裸视图作根 → 空指令表)在构建期即 `_Exit(3)`,与 dry-run 同一闸门。 - `-DNN_SCAN_NO_ANCHOR` 可关掉锚点做 A/B(只影响登记集合,不影响语义)。 diff --git a/src/expr_cpu_test.cpp b/src/expr_cpu_test.cpp index 9008ef87..61d80f90 100644 --- a/src/expr_cpu_test.cpp +++ b/src/expr_cpu_test.cpp @@ -43,6 +43,13 @@ // ── expr_fold_test(fold 分块状态归约;独立函数,无宏重命名)─────────────── #include "expr_fold_test.cpp" +// ── expr_fused_key_test("结构 = 表达式类型":AOT 自登记的 key 契约)──────── +#define main test_fused_key +#define g_fail_fk g_fail_fused_key +#include "expr_fused_key_test.cpp" +#undef g_fail_fk +#undef main + int main() { int failures = 0; @@ -62,6 +69,9 @@ int main() std::puts("=== expr_fold (block-state reduction) ==="); failures += test_expr_fold(); + std::puts("=== expr_fused_key (structure == expression type) ==="); + failures += test_fused_key(); + std::printf("\nexpr_cpu_test: %d failure(s)\n", failures); return failures != 0 ? 1 : 0; } \ No newline at end of file diff --git a/src/expr_fused_key_test.cpp b/src/expr_fused_key_test.cpp new file mode 100644 index 00000000..aad787c9 --- /dev/null +++ b/src/expr_fused_key_test.cpp @@ -0,0 +1,152 @@ +// ── expr_fused_key_test — "结构 = 表达式类型" 不变量(AOT 自登记的前提)──────── +// 背景(docs/development/02 §自登记锚点):`FusedAnchor` 在构建期按表达式 +// **类型**默认构造一个"符号实例"折叠出结构并登记;运行时同一调用点折叠出的 key +// 必须与它一致,否则闭合世界 miss。成立条件 = **key 只描述结构**: +// ① 常量池**值**不进 key(只喂个数;值经 push constant `c` 传给 shader) +// ② matmul `transA/transB` 不进 key(运行期 operand layout,PC `mm_trans`) +// ③ 分组归约 `R` 不进 key(运行期视图参数,PC `vp`) +// 本测试同时锁死**符号实例 key ≡ 真实实例 key** —— 这是自登记机制的全部依据。 +// 若有人重新引入"运行期值决定结构",这里会立刻红。 +// +// 纯 CPU、无 GPU 依赖。 +// ─────────────────────────────────────────────────────────────────────────── + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ +int g_fail_fk = 0; + +[[nodiscard]] std::string fk_key(const nn::ExprSpec& s) +{ return nn::expr_spec_key(nn::canonicalize_expr_spec(s)); } + +void fk_eq(const char* name, const std::string& a, const std::string& b, + const char* why) +{ + if (a == b) + { + std::printf("[PASS] %s(%s)key=%s\n", name, why, a.c_str()); + return; + } + ++g_fail_fk; + std::printf("[FAIL] %s:key 应相同但不同(%s)\n a=%s\n b=%s\n", + name, why, a.c_str(), b.c_str()); +} +} // namespace + +int main() +{ + nn::CpuEngine eng; + // 真实实例的操作数(形状/精度都真实) + const nn::Tensor x = eng.create_tensor(8, 4, nn::Precision::F32); + const nn::Tensor w = eng.create_tensor(5, 8, nn::Precision::F32); + const nn::Tensor b = eng.create_tensor(5, 1, nn::Precision::F32); + const nn::Tensor g = eng.create_tensor(5, 4, nn::Precision::F32); + const nn::Tensor S{}; // 符号张量(默认构造:未绑定、无存储) + + using namespace nn; + + // ── ① 符号实例 key ≡ 真实实例 key(FusedAnchor 的全部依据)────────────── + struct Pair { const char* name; std::string real; std::string sym; }; + const Pair pairs[] = { + {"relu_fwd", fk_key(dsl::to_expr_spec( + dsl::max(dsl::leaf(x), Scalar{0})).first), + fk_key(dsl::to_expr_spec( + dsl::max(dsl::leaf(S), Scalar{0})).first)}, + {"linear_fwd(matmul+row_broadcast)", + fk_key(dsl::to_expr_spec( + dsl::matmul(w, x, false, false) + dsl::row_broadcast(b)).first), + fk_key(dsl::to_expr_spec( + dsl::matmul(S, S, false, false) + dsl::row_broadcast(S)).first)}, + {"matmul_transA", + fk_key(dsl::to_expr_spec(dsl::matmul(w, g, true, false)).first), + fk_key(dsl::to_expr_spec(dsl::matmul(S, S, true, false)).first)}, + {"matmul_transB", + fk_key(dsl::to_expr_spec(dsl::matmul(g, x, false, true)).first), + fk_key(dsl::to_expr_spec(dsl::matmul(S, S, false, true)).first)}, + {"row_reduce_sum", + fk_key(dsl::to_expr_spec(dsl::row_reduce_sum(dsl::leaf(g))).first), + fk_key(dsl::to_expr_spec(dsl::row_reduce_sum(dsl::leaf(S))).first)}, + {"grouped_reduce_max", + fk_key(dsl::to_expr_spec( + dsl::grouped_reduce_max(g, 4u) + dsl::rparam(Scalar{0})).first), + fk_key(dsl::to_expr_spec( + dsl::grouped_reduce_max(S, 1u) + dsl::rparam(Scalar{0})).first)}, + {"rotate_half", + fk_key(dsl::to_expr_spec(dsl::rotate_half(x, 16u)).first), + fk_key(dsl::to_expr_spec(dsl::rotate_half(S, 1u)).first)}, + {"row_access", + fk_key(dsl::to_expr_spec(dsl::row_access(x, 8u, 8u)).first), + fk_key(dsl::to_expr_spec(dsl::row_access(S, 1u, 1u)).first)}, + }; + for (const Pair& p : pairs) + fk_eq(p.name, p.real, p.sym, "符号实例 key ≡ 真实实例 key"); + + // 默认构造可用性(锚点要求整棵表达式树可符号构造) + static_assert(std::is_default_constructible_v); + static_assert(std::is_default_constructible_v); + static_assert(std::is_default_constructible_v); + + // ── ② 常量池**值**不进 key(只喂个数)──────────────────────────────── + fk_eq("const_value_not_in_key", + fk_key(dsl::to_expr_spec(dsl::max(dsl::leaf(x), Scalar{1e-6f})).first), + fk_key(dsl::to_expr_spec(dsl::max(dsl::leaf(x), Scalar{1e-5f})).first), + "常量池只喂个数"); + + // ── ③ matmul 转置不进 key(运行期 operand layout)───────────────────── + { + const std::string k00 = fk_key(dsl::to_expr_spec( + dsl::matmul(w, x, false, false) + dsl::row_broadcast(b)).first); + const std::string k01 = fk_key(dsl::to_expr_spec( + dsl::matmul(w, x, false, true) + dsl::row_broadcast(b)).first); + const std::string k10 = fk_key(dsl::to_expr_spec( + dsl::matmul(w, x, true, false) + dsl::row_broadcast(b)).first); + const std::string k11 = fk_key(dsl::to_expr_spec( + dsl::matmul(w, x, true, true) + dsl::row_broadcast(b)).first); + fk_eq("matmul_trans_not_in_key", k00, k01, "4 种转置组合共享一个 shader"); + fk_eq("matmul_trans_not_in_key", k00, k10, "4 种转置组合共享一个 shader"); + fk_eq("matmul_trans_not_in_key", k00, k11, "4 种转置组合共享一个 shader"); + + // 运行期描述子位编码(GPU 端据此选加载路径;编码错 = 静默读错布局) + const auto trans_of = [](bool a, bool c) { + nn::ExprSpec s; + s.views = {nn::expr::linear(), nn::expr::linear()}; + s.matmul = nn::MatmulSpec{0, 1, static_cast(a ? 1u : 0u), + static_cast(c ? 1u : 0u), 1}; + return nn::expr_spec_runtime_matmul_trans(s); + }; + const bool enc_ok = trans_of(false, false) == 0u && trans_of(true, false) == 1u + && trans_of(false, true) == 2u && trans_of(true, true) == 3u; + if (enc_ok) std::printf("[PASS] expr_spec_runtime_matmul_trans 位编码\n"); + else { ++g_fail_fk; std::printf("[FAIL] matmul trans 位编码不是 bit0/bit1\n"); } + } + + // ── ④ 分组归约 R 不进 key(运行期视图参数)──────────────────────────── + fk_eq("grouped_reduce_R_not_in_key", + fk_key(dsl::to_expr_spec( + dsl::grouped_reduce_max(x, 4u) + dsl::rparam(Scalar{0})).first), + fk_key(dsl::to_expr_spec( + dsl::grouped_reduce_max(x, 9u) + dsl::rparam(Scalar{0})).first), + "R 经 vp 槽运行期传入"); + // sum / max 是**两种视图**(结构)→ 必须仍然区分 + { + const std::string ks = fk_key(dsl::to_expr_spec( + dsl::grouped_reduce_sum(x, 4u) + dsl::rparam(Scalar{0})).first); + const std::string km = fk_key(dsl::to_expr_spec( + dsl::grouped_reduce_max(x, 4u) + dsl::rparam(Scalar{0})).first); + if (ks != km) std::printf("[PASS] sum/max 是不同视图(key 不同)\n"); + else { ++g_fail_fk; std::printf("[FAIL] grouped_reduce sum/max key 应不同\n"); } + } + + std::printf("\nexpr_fused_key_test: %d failure(s)\n", g_fail_fk); + return g_fail_fk != 0 ? 1 : 0; +} From 0bdaa97affc0fb6af9721e274e250862ed598037 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 16:09:58 +0800 Subject: [PATCH 13/17] =?UTF-8?q?AOT=20=E8=9E=8D=E5=90=88=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E6=9C=9F=E6=94=B6=E4=B8=BA=E5=8D=95=E6=AD=A5=EF=BC=9A?= =?UTF-8?q?gen=5Ffused=20=E5=B9=B6=E5=85=A5=20scan=5Fexprs=EF=BC=8C?= =?UTF-8?q?=E5=88=A0=E9=99=A4=20bin=20=E5=BA=8F=E5=88=97=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原构建期两步(`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 从 `` 改为 ` []`;收集完直接调用生成函数。 - `CMakeLists.txt`:删除 `gen_fused` target 与第二条 custom command; `fused_registry.hpp` 由单条命令(`scan_exprs`)产出。 - `include/neuralnet.cpp/expr_registry.hpp`:删除 `write_registry` / `read_registry` / `kExprBinVersion` 及 ``/`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; - 生成头与旧流程逐字节相同(见上)。 --- CMakeLists.txt | 33 +- docs/development/02-operator-fusion.md | 26 +- docs/development/03-ir-optimization.md | 6 +- docs/development/05-mixed-precision.md | 6 +- docs/introduction/01-architecture.md | 10 +- docs/introduction/04-innovative-designs.md | 2 +- docs/usage/03-compute-engine-usage.md | 2 +- include/neuralnet.cpp/expr_registry.hpp | 322 +------------------- src/expr_fold_test.cpp | 47 ++- tools/{gen_fused.cpp => fused_generate.hpp} | 84 +++-- tools/scan_exprs.cpp | 34 ++- 11 files changed, 131 insertions(+), 441 deletions(-) rename tools/{gen_fused.cpp => fused_generate.hpp} (91%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3b34bb4d..6aea3c0f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -467,40 +467,29 @@ if(Vulkan_FOUND) nn_embed_shader(im2col_f16 ${CMAKE_SOURCE_DIR}/shaders/im2col.comp -DNN_SHADER_F16=1) nn_embed_shader(col2im_f16 ${CMAKE_SOURCE_DIR}/shaders/col2im.comp -DNN_SHADER_F16=1) - # 2. AOT 融合 shader:scan_exprs + gen_fused - set(NN_EXPR_SCAN_BIN ${CMAKE_BINARY_DIR}/generated/expr_specs.bin) + # 2. AOT 融合 shader:scan_exprs(**单步** = 收集 + 生成) + # 收集:FusedAnchor 自登记(按表达式类型)+ dry-run + 模型 pass + # 生成:expr_emitter(默认 GLSL)→ glslc → 内联 SPIR-V → fused_registry.hpp + # 原 gen_fused 已并入 scan_exprs 的生成阶段(tools/fused_generate.hpp), + # 不再经 expr_specs.bin 中间文件。 set(NN_FUSED_REGISTRY_HPP ${CMAKE_BINARY_DIR}/generated/fused_registry.hpp) - # 2a. scan_exprs:dry-run Layer 收集内联表达式结构 → expr_specs.bin - set(SCAN_SRC ${CMAKE_SOURCE_DIR}/tools/scan_exprs.cpp) + set(SCAN_SRC ${CMAKE_SOURCE_DIR}/tools/scan_exprs.cpp) + set(FUSED_GEN_SRC ${CMAKE_SOURCE_DIR}/tools/fused_generate.hpp) add_executable(scan_exprs ${SCAN_SRC}) target_include_directories(scan_exprs PRIVATE ${CMAKE_SOURCE_DIR}/include/neuralnet.cpp) target_link_libraries(scan_exprs PRIVATE nn_core Vulkan::Vulkan) add_custom_command( - OUTPUT ${NN_EXPR_SCAN_BIN} + OUTPUT ${NN_FUSED_REGISTRY_HPP} # scan_exprs 是原生可执行文件,直接写文件、不会自建目录。 # Ninja 会自动创建自定义命令的输出目录,但 Makefiles 生成器不会, # 全新构建时 generated/ 尚未存在会写入失败(见 08-pitfalls)。 COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/generated - COMMAND scan_exprs ${NN_EXPR_SCAN_BIN} - DEPENDS scan_exprs ${SCAN_SRC} - COMMENT "[GPU] Scanning Layer expressions (scan_exprs) → expr_specs.bin" - ) - - # 2b. gen_fused:读 bin → glsl_gen → glslc → 内联 SPIR-V → fused_registry.hpp - set(FUSED_GEN_SRC ${CMAKE_SOURCE_DIR}/tools/gen_fused.cpp) - add_executable(gen_fused ${FUSED_GEN_SRC}) - target_include_directories(gen_fused PRIVATE ${CMAKE_SOURCE_DIR}/include/neuralnet.cpp) - target_link_libraries(gen_fused PRIVATE nn_core) - add_custom_command( - OUTPUT ${NN_FUSED_REGISTRY_HPP} - # 同 scan_exprs:gen_fused 也是原生可执行文件,需先确保输出目录存在 - COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/generated - COMMAND gen_fused ${CMAKE_BINARY_DIR}/generated ${GLSLC} ${NN_EXPR_SCAN_BIN} + COMMAND scan_exprs ${CMAKE_BINARY_DIR}/generated ${GLSLC} ${CMAKE_SOURCE_DIR}/tools/prec_backfill.txt - DEPENDS gen_fused ${FUSED_GEN_SRC} ${NN_EXPR_SCAN_BIN} + DEPENDS scan_exprs ${SCAN_SRC} ${FUSED_GEN_SRC} ${CMAKE_SOURCE_DIR}/tools/prec_backfill.txt - COMMENT "[GPU] Synthesizing fused shaders (gen_fused) → fused_registry.hpp" + COMMENT "[GPU] Scan + synthesize fused shaders (scan_exprs) → fused_registry.hpp" ) list(APPEND GPU_SHADER_HPPS ${NN_FUSED_REGISTRY_HPP}) diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index ce3da1c2..78b3be48 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -59,7 +59,7 @@ |------|------| | **引擎只提供 op-level 原语** | `ComputeEngine` 只能有 `matmul`/`reduce`/`broadcast`/`elementwise` 这类通用原语;**绝不允许出现 `softmax`/`attention`/`layernorm` 命名的接口** | | **算法文本只在 Layer** | ReLU/GeLU/Softmax/LayerNorm/Attention/CrossEntropy 的公式只写在 `compute_layer.hpp` / `compute_loss.hpp` | -| **融合逻辑归工具/引擎内部** | `glsl_gen` / `gen_fused` / 各引擎实现负责"怎么融",Layer 只写"是什么" | +| **融合逻辑归工具/引擎内部** | `glsl_gen` / 生成阶段 / 各引擎实现负责"怎么融",Layer 只写"是什么" | | **Shader 是引擎内部实现** | 融合 shader 只存在于 `shaders/` + 各引擎,用户不可见 | > 设计原则:**原语可以多、可以专(matmul+归约、matmul+exp+sum 都是合法原语),但原语必须"通用可复用、不叫算法名"。** 引擎可以认"结构"(`reduce(matmul(A,B))`、`matmul→softmax→matmul`),绝不认"算法名"。 @@ -82,7 +82,7 @@ graph LR G[GPU 实现 + 融合 shader] end E --> F & G - G -->|结构 key| I[scan_exprs + gen_fused 闭合世界] + G -->|结构 key| I[scan_exprs + 生成阶段 闭合世界] ``` **核心机制**:Layer 用 `dsl::compute` / `compute_reduce` / `compute_into`(GPU 上折叠为 `ExprSpec`)表达算法;引擎/工具按**结构**合成融合 kernel。所有中间 Tensor 由融合 kernel 内部消解,不落 VRAM。注意力 / 稀疏 CE 等结构一律由 IR 表达(`MatmulSpec` 段、归约视图与归约指令、`FoldSpec` fold 段),`glsl_gen` 从 IR 结构统一合成。**不存在"跨表达式融合"**——运行时没有跨表达式录制机制(IR-C 不在库中,见 §表达式录制与融合边界)。 @@ -158,7 +158,7 @@ struct MatmulSpec { ## 表达式录制与融合边界 -本文的"表达式录制"指**构建期扫描**:`scan_exprs` dry-run 跑各 Layer 的 forward/backward,把折叠出的 `ExprSpec` 结构去重录进 `expr_specs.bin`(见 §构建工具链与闭合世界)——这是库里唯一的表达式收集机制。 +本文的"表达式录制"指**构建期收集**:`scan_exprs` 收集各 Layer 折叠出的 `ExprSpec`——**结构**由 `FusedAnchor` 在静态初始化期按表达式类型自登记(编译期可达),**精度签名**由 dry-run / 模型 pass 执行产生(见 §构建工具链与闭合世界与 §自登记锚点)——这是库里唯一的表达式收集机制。 **运行时没有跨表达式录制**:`ComputeEngine::begin_expr/end_expr`、`dsl::start_expr/end_expr`、`expr_graph.hpp`、演示层 `FusedChainLayer` 均不存在(IR-C 已移除;取舍记录见 `docs/history.md`,另见 `03-ir-optimization.md` §5.3)。融合边界 = 单个表达式的输入/输出;需要融合的长链直接写成**一个** `dsl::compute` 表达式(单个 AOT 融合 kernel)。CPU 侧表达式求值只有两套机制:**DSL 编译期模板路径 + IR 解释器**(见 `12-compute-engine-inventory.md`)。 @@ -230,12 +230,14 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba ## 构建工具链与闭合世界 -构建期两步(CMake 自动编排,改 Layer 内联表达式后重跑构建即可): +构建期**一步**(CMake 自动编排,改 Layer 内联表达式后重跑构建即可): -1. **`tools/scan_exprs.cpp`**:dry-run 跑各 Layer forward/backward,收集折叠出的 `ExprSpec` 结构(去重)→ `build/generated/expr_specs.bin`。 -2. **`tools/gen_fused.cpp`**:读 bin → `glsl_gen` 生成 GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。 +1. **`tools/scan_exprs.cpp`**(收集 + 生成同一进程): + · **收集**:`FusedAnchor` 在静态初始化期按表达式**类型**登记结构(编译期可达),再由 dry-run per-layer 块 + 模型级配置矩阵补精度签名;每个 `dsl::compute*` 在记录模式下把折叠出的 `ExprSpec` 登记进注册表(按 key 去重)。 + · **生成**:`tools/fused_generate.hpp`(原 `gen_fused.cpp`)对每条 spec 出 `glsl_gen` GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。 + · **不经 `.bin` 中间序列化**(`expr_registry.hpp` 的 `write_registry/read_registry/kExprBinVersion` 已删除)。 -`scan_exprs` 需覆盖所有 Layer 的 DSL 路径(Softmax/LN/RMSNorm fwd+bwd、CrossEntropy softmax 结构、**Attention fold 结构**——层 forward 直调 `engine.eval_expr(make_fold_attn_o(...))` 不经 DSL 钩子,scan 的显式登记块(3 掩码 × 2 偏置的 5 个组合)是 fold spec 唯一注册来源、漏组合即 GPU 闭合世界硬报错、Linear 的 matmul 段、optimizer 的 `compute_into` 原地表达式),使融合结构被收集。未命中 → `eval_expr` 现有"硬报错"逻辑,提示补进扫描(保持项目"GPU 硬报错、不降级"哲学)。 +`scan_exprs` 需覆盖所有 Layer 的 DSL 路径(Softmax/LN/RMSNorm fwd+bwd、CrossEntropy softmax 结构、**Attention fold 结构**——层 forward 直调 `engine.eval_expr(make_fold_attn_o(...))` 不经 DSL 钩子,scan 的显式登记块(3 掩码 × 2 偏置的 5 个组合)是 fold spec 唯一注册来源、漏组合即 GPU 闭合世界硬报错、Linear 的 matmul 段、optimizer 的 `compute_into` 原地表达式),使融合签名被收集。**结构**已由自登记锚点保证(见下节),未命中 → `eval_expr` 硬报错(保持项目"GPU 硬报错、不降级"哲学)。 ### 自登记锚点:结构覆盖不再依赖手写清单(2026-10-01) @@ -277,7 +279,7 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba |------|------|----------------| | **S1 IR 地基** | `ExprSpec` 增加 `MatmulSpec` + `ExprOperandKind::Matmul`;`validate_expr_spec`/`expr_spec_key`/`expr_spec_reduce_axis` 兼容 matmul 段;key 与形状无关(`k` 不进 key,`transA/transB/a_input/b_input` 进 key) | 现行;相关子测试并入聚合目标 `expr_cpu_test` | | **S2 CPU 正确性** | `CpuEngine::eval_expr` 支持 matmul 段(matmul 预计算 + 逐元素链,`eval_expr_reduce` 经归约指令消费 matmul 输出) | `expr_matmul_test`(`expr_cpu_test` 内):`matmul+bias` 融合 vs 参考 | -| **S3 GLSL 生成** | `generate_glsl_matmul`(共享内存分块 + vec4);`gen_fused`/`scan_exprs`/`run_fused_gpu` 接入 | `expr_gpu_test`(`fused_gpu_test` 并入其中)matmul 融合用例(GPU vs CPU);AOT 命中 | +| **S3 GLSL 生成** | `generate_glsl_matmul`(共享内存分块 + vec4);生成阶段/`scan_exprs`/`run_fused_gpu` 接入 | `expr_gpu_test`(`fused_gpu_test` 并入其中)matmul 融合用例(GPU vs CPU);AOT 命中 | | **S4 Layer 迁移(线性)** | `Linear`/`FeedForward` 的 `matmul+bias+activation` 走 `dsl::compute`(含 matmul 段) | gradcheck / MNIST/GPT 训练回归 | | **S5 跨归约/跨 matmul 链** | 融合分块矩阵乘法(16×16 线程/64×64 块/4×4 寄存器分块/vec4 转置共享内存 + `eval_tail` 函数);matmul+归约(`row_max(matmul)`、`row_sum(exp(matmul-rm))`)经 `generate_glsl_reduce` 内联点积 | 注意力相关 gradcheck | | **S6 自动窗口** | **不接线**——运行时没有跨表达式并入窗口(见 §跨 kernel 自动融合) | — | @@ -290,13 +292,13 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba 1. **运行时值禁进表达式常量池**:常量池的值虽已不进 `expr_spec_key`(只喂个数,`glsl_gen` 经 push constant `c` 读取),但**个数**仍进 key,而 canonicalize 会**按值去重**常量——把运行期标量塞进常量池会让同一结构的 key 随取值漂移,等于每个取值组合一份 shader。运行时标量一律用 **`rparam`** 承载——注意力 `scale_`(1/√d_k)折进 Q:`dsl::compute_into(engine, leaf(Q) * rparam(scale_), Q)`(backward 的 grad_Q 相应补乘),使表达式结构与 d_k 无关;稀疏 CE 的 `inv_num_valid` 用 `dsl::rparam(inv_num_valid)` 尾链。rparam 值不进 key、个数固定,同结构不同值共享一个 shader。 2. **BatchCol 视图要求 `(1, BH*seq)`**(doc_ids 按 (b,h) 块重复),`(1, batch*seq)` 会越界。 3. **RowGather 主输入行数≠网格行数**(loss_vec 在 (1,N) 读 (C,N) logits),校验只查 cols。 -4. `gen_fused` `emit_spec` 的 ±inf 常量必须用 `numeric_limits`。 -5. **matmul + 列归约已支持**:`generate_glsl_reduce` 列归约分支按元素分解 batch(`batch = row/m_per`,列归约遍历全部 `rows = batch*m_per` 行,与 CPU `matmul_out` 逐列归约语义一致),`gen_fused` 不跳过该形态(扫描到的 spec 全部生成);`expr_cpu_test::col_max(matmul)`(独立标量参考,batch=2)+ `expr_gpu_test::col_max(matmul)` 广播/归约向量/batch=2 对拍锁死(err≈1e-7)。 +4. 生成阶段 `emit_spec` 的 ±inf 常量必须用 `numeric_limits`。 +5. **matmul + 列归约已支持**:`generate_glsl_reduce` 列归约分支按元素分解 batch(`batch = row/m_per`,列归约遍历全部 `rows = batch*m_per` 行,与 CPU `matmul_out` 逐列归约语义一致),生成阶段 不跳过该形态(扫描到的 spec 全部生成);`expr_cpu_test::col_max(matmul)`(独立标量参考,batch=2)+ `expr_gpu_test::col_max(matmul)` 广播/归约向量/batch=2 对拍锁死(err≈1e-7)。 6. **PS 删大文件段行号易漂移**、`-replace` 多行静默失败——先 read 再 edit,删前 `git diff` 核对。 7. `dispatch_compute`(`compute_vk_backend.hpp` 的融合 dispatch 分发函数)有多处调用点——改签名或删除时按调用点逐一核对重建。 8. **IR 扩展**:MatmulSpec.batch(不进 key,dispatch z)、MatmulSpec.transA/transB(不进 key,运行期 operand layout → PC `mm_trans`;见"运行期 operand layout"节)、Row/Col/Batch 操作数(6/7/8)、RowGather(9)/BatchMod(10)/BatchCol(11);注意力 forward 现为单 fold kernel(`FoldSpec`,掩码 `AttnScoreMask::mask_kind()` × 位置偏置 `PositionEncoder::has_score_bias()` 两个正交入参),bwd=掩码 → 位置偏置(独立一步)→ softmax 的 R/X 表达式+3 个 `batched_matmul`(m/l/W 表达式+bm(W,V_t) 的 S7 forward 结构已删);CE 稠密 `denom=col_sum(exp(logits-cb(col_max)))`,稀疏 grad/loss_vec 用 Row+RowGather。 ⚠ `dsl::row()` 是"批内行号",其分解来自**同一 spec 里的 matmul 段**;把原本融在 matmul 表达式里的项拆成独立一步后 batch 退化为 1、`row()` 变全局行号 → 静默错值。需要批内位置时用 `(rows,1)` 行表 + `dsl::row_broadcast`(`AlibiPositionEncoder::apply_score_bias` 即此写法)。 -9. **IR-D 现只有 `GlslEmitter` 一个注册后端**(`gen_fused --list-backends` 可列);`cpu_emitter.hpp` 不存在。 +9. **IR-D 现只有 `GlslEmitter` 一个注册后端**(`scan_exprs --list-backends` 可列);`cpu_emitter.hpp` 不存在。 --- @@ -341,7 +343,7 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba 1. `expr_spec_key` 剔除 RowMod/RotateHalf 的 param(保留 kind 与 negate_first_half)→ 同结构不同 d_k 共享一个 shader。 2. `glsl_gen` 把这两个参数作为 push constant `vpN` 槽读取。 -3. `gen_fused`/`vk_backend` 记录并传递 vp 槽数(FusedShader 带 `view_param_count`)。 +3. 生成阶段/`vk_backend` 记录并传递 vp 槽数(FusedShader 带 `view_param_count`)。 4. `GpuEngine::eval_expr`/`eval_expr_reduce` 按实际 spec 提取 vp 参数(`expr_spec_runtime_view_params`)传入 dispatch。 5. 未命中硬报错,不静默回退 CPU。 diff --git a/docs/development/03-ir-optimization.md b/docs/development/03-ir-optimization.md index 6c86c9c2..795bd603 100644 --- a/docs/development/03-ir-optimization.md +++ b/docs/development/03-ir-optimization.md @@ -167,9 +167,9 @@ IR → GlslEmitter(当前唯一注册后端) - `ExprEmitter` 纯接口(name/generate/generate_reduce)+ `emitter_registry`(按后端名选择工厂)。 - `GlslEmitter`(`expr_glsl_gen.hpp` 的 generate_glsl/generate_glsl_reduce 封装)。 -- `gen_fused` 经 emitter 注册表选择后端(默认 glsl);`--list-backends` 展示可用后端。 +- 生成阶段 经 emitter 注册表选择后端(默认 glsl);`--list-backends` 展示可用后端。 -> **当前只有 `glsl` 一个注册后端**(`GlslEmitter`);`gen_fused --list-backends` 可列出。 +> **当前只有 `glsl` 一个注册后端**(`GlslEmitter`);`scan_exprs --list-backends` 可列出。 > 接口 + 注册表即 IR-D 的全部交付:新后端实现 `ExprEmitter` 并注册即可,运行时无需包含该头(与 `glsl_gen` 一致)。 --- @@ -186,7 +186,7 @@ IR → GlslEmitter(当前唯一注册后端) ### IR-A / IR-B 实现要点(`expr_opt.hpp`) - `fold_constants_and_algebra`(常量池去重 + 保守常量折叠 + 代数化简)、`dead_code_elimination`、`renumber_registers`(寄存器连续重编号 + 常量池清理)、`common_subexpression_elimination`(Fanout 归一化 + 哈希复用)、`allocate_registers_liveness`(liveness 线性扫描,确定性贪心)、`canonicalize_expr_spec`(完整链)。 -- **canonical IR 接入 key**:`ExprRegistry::add/contains`、CPU `eval_expr_impl`、GPU `eval_expr/eval_expr_reduce` 全部先 `canonicalize_expr_spec` 再算 key;scan/gen_fused 存 canonical spec → shader 按 canonical 合成。**dispatch 必须用 canonical 的 consts**(折叠可能增删常量池)。 +- **canonical IR 接入 key**:`ExprRegistry::add/contains`、CPU `eval_expr_impl`、GPU `eval_expr/eval_expr_reduce` 全部先 `canonicalize_expr_spec` 再算 key;scan/生成阶段 存 canonical spec → shader 按 canonical 合成。**dispatch 必须用 canonical 的 consts**(折叠可能增删常量池)。 - **关键不变量**:canonicalize 不改变 views/inputs(顺序、内容),只优化 instrs/consts/num_regs → 运行时输入绑定布局不变;输出指令(最后一条)恒为真实寄存器(glsl_gen 输出 `r`)。 - **glsl_gen 配套**:寄存器"先声明、后赋值"(`float r0, r1, ...;` + 纯赋值),兼容 liveness 复用同号寄存器(否则 GLSL redefinition)。 - **关键坑**:① regalloc 复用后归约指令 dst 与逐元素 dst 必须**区段分离**(validate 要求 reduce_dst/elem_dst 按号互斥);② 各 pass 重映射只处理 `expr_instr_num_operands(op)` 实际使用的操作数(未用 b/c 是默认哨兵 {0,0},不得当 Reg(0) 重编号)。 diff --git a/docs/development/05-mixed-precision.md b/docs/development/05-mixed-precision.md index 0599fb7e..58a02f79 100644 --- a/docs/development/05-mixed-precision.md +++ b/docs/development/05-mixed-precision.md @@ -579,7 +579,7 @@ loss = ce.forward_sparse(engine, logits, labels, mask, vocab, | `scan_exprs` 双 pass | 整段 dry-run 收进 `dry_run(engine, profile)`:`profile_f32`(sig==0,旧行为)与 `profile_f16`(输入张量按 compute 精度创建 → 与运行时同源)。f16 pass **直接传原生引擎即可**——P-1 后 f16 边界 cast 由基类 NVI 入口统一处理(历史上直喂原生 `CpuEngine` = heap corruption(0xC0000374,实测);适配层已删除、职责并入基类)。 | | bin v9 | 规格表之后的**变体段**:每个变体只存 `{sig, 基础结构下标}`——变体与基础结构同 key(精度不进 `expr_spec_key`),故不重复序列化 spec 体(也免读写不对称风险)。 | | `GlslEmitter` | `generate/generate_reduce` 带 `sig` 形参(默认 0 → **GLSL 与不带变体时逐字节相同**)。带类型输出 = 缓冲声明 `float16_t` + `#extension GL_EXT_shader_16bit_storage` + 读 `float(x)` / 写 `float16_t(v)`;算术默认全 f32(§7.2)。视图内部有算术的分支(RotateHalf 的取负、RowGather 的 `uint(...)` 索引)必须**在叶子处**转换——否则 glslc 报 `'-' : wrong operand type ... float16_t`。 | -| `gen_fused` | 每变体独立 shader(文件名/标识符 `key_sighex`,注册键 `key#sig`),`FusedShader` 带 `prec_sig`;生成器不支持的形态 → **跳过并告警**(不是失败)。 | +| 生成阶段 | 每变体独立 shader(文件名/标识符 `key_sighex`,注册键 `key#sig`),`FusedShader` 带 `prec_sig`;生成器不支持的形态 → **跳过并告警**(不是失败)。 | | 设备 | 查询并启用 `storageBuffer16BitAccess`(`NN_VULKAN_NO_16BIT_STORAGE=1` 可强制回退);未启用时后端跳过 `key#sig` 键(运行时自然回退边界 cast)。 | | 运行时 | `run_fused_gpu` 输入是**类型擦除的 buffer 视图**;`FusedInputs{owners, bufs}` **必须同时持有 owner**——只存裸 `GpuBuffer*` 会在录制中途释放上传缓冲(铁律 6,实测 `A+=B err=0.5`)。`GpuEngine::eval_expr` 按 `(key,sig)` 优先命中变体,**f16 输出按 2B/元素分配缓冲并重贴 `GpuTensorF16`**(漏了 = shader 只写前半 + f32 标签 → 训练 loss=NaN);f16 进原生引擎却无变体 → 明确报错(绝不把 f16 buffer 绑到 f32 shader)。适配层 `supports_expr_precision_variant` 前置查询:命中则直吃 f16、未命中回退边界 cast。 | @@ -625,7 +625,7 @@ loss = ce.forward_sparse(engine, logits, labels, mask, vocab, | **归约 kernel** | `generate_glsl_reduce(name, spec, sig)`:输入 `float16_t` + `rd()/wr()` 在读写点统一转换(含 `emit_mm_decl` 点积、行/列两个 pass 的直接索引读、`operand()` 的视图读与广播读、输出写) | 含归约的变体可生成 | | **目标传递** | `GpuEngine::eval_expr_into` / `eval_expr_reduce` 带 `(key,sig)` 变体匹配(输出精度 = `dst.precision()` / `P`),f16 输出重贴 `GpuTensorF16`(归约向量形状按 raxis 取 `(rows,1)/(1,cols)`);适配层这两入口先查 `supports_expr_precision_variant` 再回退 cast | `compute_into` / `compute_reduce` 不再必然走边界 cast | -`gen_fused` 的基础结构数与变体条目数以构建输出 `[scan]`/`[gen]` 为准(变体含 scan 预测 + 回填清单,见 ⑤)。 +生成阶段 的基础结构数与变体条目数以构建输出 `[scan]`/`[gen]` 为准(变体含 scan 预测 + 回填清单,见 ⑤)。 #### ④ 实测收益 @@ -633,7 +633,7 @@ matmul 段 / 归约 / 目标传递三类变体落地后的同窗交错实测(f #### ⑤ 运行时签名回填(当前) -`[prec][miss]`(`NN_PREC_TRACE=1`)打印"请求了非零签名却没命中带类型变体"的 `(key, sig, 形态)`——这类**扫描时看不到的运行时签名**(如 Linear matmul 段的 `in=[f16,f32,f32]` 混合签名、Norm 归约链 `[f32,f16]` 输入)miss 时走边界 cast。**当前机制 = 回填清单** `tools/prec_backfill.txt`(Phase D3):miss 复现 → 追加一行 ` ` → `gen_fused` 对命中的 `(结构, 签名)` 额外发射变体(与 scan 变体同一代码路径,重复条目幂等去重;结构不在 bin / 生成器不支持时告警跳过)。**fold 的带类型变体已生成**(Phase D2,键 `fkey#sig`,3 输入全 f16 + 输出 f16,算术/状态进位 f32)。 +`[prec][miss]`(`NN_PREC_TRACE=1`)打印"请求了非零签名却没命中带类型变体"的 `(key, sig, 形态)`——这类**扫描时看不到的运行时签名**(如 Linear matmul 段的 `in=[f16,f32,f32]` 混合签名、Norm 归约链 `[f32,f16]` 输入)miss 时走边界 cast。**当前机制 = 回填清单** `tools/prec_backfill.txt`(Phase D3):miss 复现 → 追加一行 ` ` → 生成阶段 对命中的 `(结构, 签名)` 额外发射变体(与 scan 变体同一代码路径,重复条目幂等去重;结构不在注册表 / 生成器不支持时告警跳过)。**fold 的带类型变体已生成**(Phase D2,键 `fkey#sig`,3 输入全 f16 + 输出 f16,算术/状态进位 f32)。 --- diff --git a/docs/introduction/01-architecture.md b/docs/introduction/01-architecture.md index 00bbdbd5..6ad6d117 100644 --- a/docs/introduction/01-architecture.md +++ b/docs/introduction/01-architecture.md @@ -283,14 +283,14 @@ class Tensor { |------|------| | `expr_spec.hpp` | `ExprSpec` 逐元素表达式扁平 IR(纯数据结构,跨后端可序列化,GPU AOT 契约)+ 可选 `fold` 段(`FoldSpec` 分块状态归约——通用折叠表达式机制,含 `tri_skip`、`EXPR_FOLD_BLOCK/ROWS_PER_WG` 共享常量)+ `expr_spec_key`(规范结构 key,AOT 收集/匹配依据) | | `expr_dsl.hpp` | 统一表达式 DSL(`nn::dsl`):编译期模板,普通数学写法;`compute` / `compute_reduce` / `compute_into`(原地目标传递)。CPU 直接求值(内联+SIMD);GPU 经 `to_expr_spec` 折叠出 `ExprSpec` → 按 `expr_spec_key` 匹配预编译融合 shader。 | -| `expr_registry.hpp` | 构建期表达式注册表(`scan_exprs` 收集折叠出的结构,按 key 去重;二进制 dump/load 供 `gen_fused` 消费) | -| `fused_registry.hpp` | **生成物**(构建期 `gen_fused` 产出):`key → {ExprSpec 结构, 内联 SPIR-V}` 融合 shader 注册表;运行时按 key 精确匹配 | -| `tools/scan_exprs.cpp` | 构建期工具:dry-run 跑 Layer 的 forward/backward,收集内联表达式的结构(派生物)→ `expr_specs.bin` | -| `tools/gen_fused.cpp` | 构建期工具:读 bin → `glsl_gen` → glslc → 内联 SPIR-V → `fused_registry.hpp` | +| `expr_registry.hpp` | 构建期表达式注册表(收集折叠出的结构 + 精度变体,按 key 去重;**不再有 bin 序列化**——收集与生成同一进程) | +| `fused_registry.hpp` | **生成物**(构建期 生成阶段 产出):`key → {ExprSpec 结构, 内联 SPIR-V}` 融合 shader 注册表;运行时按 key 精确匹配 | +| `tools/scan_exprs.cpp` | 构建期工具(**单步** = 收集 + 生成):`FusedAnchor` 按类型自登记结构 + dry-run/模型 pass 补签名,随后原地生成 `fused_registry.hpp` | +| `tools/fused_generate.hpp` | 生成阶段(原 `gen_fused.cpp`,现为头文件):注册表 → `glsl_gen` → glslc → 内联 SPIR-V → `fused_registry.hpp` | | `eval_expr` | `ComputeEngine` 虚接口:CPU 编译期模板求值(经 `dsl::compute`);Vulkan 按 `expr_spec_key` 查 `fused_registry`(闭合世界,未命中硬报错,无 eager、无运行时编译) | | `dsl::compute(engine, expr, rows, cols)` | 统一求值入口:CPU 走编译期模板;GPU 折叠成 `ExprSpec` → `eval_expr` 按 key AOT 分发 | -> **AOT 收集原则**:表达式**文本只出现在 Layer**(内联写进 `forward/backward`)。构建期 `scan_exprs` dry-run 执行 Layer 代码触达它们,把折叠后的 `ExprSpec` 结构(派生物,非手写定义)收集进注册表;`gen_fused` 据此合成融合 shader 并内联进 `fused_registry.hpp`。运行时按 key 精确匹配 dispatch,最终程序自包含、无运行时编译器。**登记方式例外**:注意力 fold spec 由 `make_fold_attn_o`(定义在 `compute_layer_attention.hpp`,与 Layer 其余表达式同住)构造、层直调 `engine.eval_expr` 不经 DSL 钩子——其登记来自 `scan_exprs` 的显式块,**掩码(`AttnMaskKind`)× 位置偏置(`bool score_bias`)两个正交维度的 5 个实际组合必须全部列出**(漏登记 = GPU 闭合世界硬报错)。通用 fold 样例构造在 `expr_fold.hpp`,不含注意力语义。 +> **AOT 收集原则**:表达式**文本只出现在 Layer**(内联写进 `forward/backward`)。构建期 `scan_exprs` 收集折叠后的 `ExprSpec` 结构(派生物,非手写定义):**结构**由 `FusedAnchor` 按表达式类型自登记(编译期可达,dry-run 跑不到的分支也覆盖),**精度签名**由 dry-run/模型 pass 执行产生;随后同进程合成融合 shader 并内联进 `fused_registry.hpp`。运行时按 key 精确匹配 dispatch,最终程序自包含、无运行时编译器。**登记方式例外**:注意力 fold spec 由 `make_fold_attn_o`(定义在 `compute_layer_attention.hpp`,与 Layer 其余表达式同住)构造、层直调 `engine.eval_expr` 不经 DSL 钩子——其登记来自 `scan_exprs` 的显式块,**掩码(`AttnMaskKind`)× 位置偏置(`bool score_bias`)两个正交维度的 5 个实际组合必须全部列出**(漏登记 = GPU 闭合世界硬报错)。通用 fold 样例构造在 `expr_fold.hpp`,不含注意力语义。 ### L3 实现层 diff --git a/docs/introduction/04-innovative-designs.md b/docs/introduction/04-innovative-designs.md index 170d4db9..f48b3eab 100644 --- a/docs/introduction/04-innovative-designs.md +++ b/docs/introduction/04-innovative-designs.md @@ -80,7 +80,7 @@ Shader / 融合逻辑是引擎内部实现,用户不可见 ``` Layer 内联表达式 → to_expr_spec 折叠 → ExprSpec(扁平 SSA IR) → expr_spec_key(规范结构 key)→ scan_exprs 收集 - → gen_fused 用 glsl_gen 合成 GLSL → glslc → 内联 SPIR-V → fused_registry.hpp + → 生成阶段 用 glsl_gen 合成 GLSL → glslc → 内联 SPIR-V → fused_registry.hpp 运行时:fold → key → find_fused(key) → 精确 dispatch(闭合世界) ``` diff --git a/docs/usage/03-compute-engine-usage.md b/docs/usage/03-compute-engine-usage.md index 2fa186da..d0e2c0d2 100644 --- a/docs/usage/03-compute-engine-usage.md +++ b/docs/usage/03-compute-engine-usage.md @@ -1598,7 +1598,7 @@ if (nn::dsl::env_flag("NN_PREC_TRACE")) | `read/write: 元素数与张量形状不匹配` | span 长度 ≠ `rows*cols` | 修正长度 | | `bind_check_ mixed engines` | 两个不同引擎创建的张量进了同一个算子 | 用 `import` 把张量拉到目标引擎 | | `dsl::compute_into: dst not on CPU` | CPU 引擎下 `dst` 是 GPU 张量 | 检查 `dst.device()` | -| GPU 报闭合世界未命中 | 表达式结构或精度签名没有预生成 shader | 检查是否把运行时值写成了 `ConstLeaf`(应改 `rparam`);改 Layer 表达式后需重新构建(`scan_exprs` → `gen_fused` 自动跑) | +| GPU 报闭合世界未命中 | 表达式结构或精度签名没有预生成 shader | 检查是否把运行时值写成了 `ConstLeaf`(应改 `rparam`);改 Layer 表达式后需重新构建(`scan_exprs` → 生成阶段 自动跑) | | `validate_expr_spec: empty instruction list` | 表达式根是裸视图/叶子 | 尾接 `+ dsl::rparam(0.0f)` | | 构建期 `[scan] validate_expr_spec 失败` + `_Exit(3)` | 新增表达式非法(最常是空指令表 / 裸分组归约视图作根) | 同上;这是**构建期闸门**,不是运行期 bug | | `VK_ERROR_DEVICE_LOST` | 录制期张量提前析构(铁律 #6),或 TDR | 确认张量存活到 `end_batch()`;TDR 不可重试,存 checkpoint 退出;`VK_TIMEOUT` 可减半 batch 重试 | diff --git a/include/neuralnet.cpp/expr_registry.hpp b/include/neuralnet.cpp/expr_registry.hpp index 0446d6f5..f49bb5d3 100644 --- a/include/neuralnet.cpp/expr_registry.hpp +++ b/include/neuralnet.cpp/expr_registry.hpp @@ -1,30 +1,28 @@ #pragma once // ═══════════════════════════════════════════════════════════════════════════ -// expr_registry.hpp — 可融合表达式注册表(AOT 收集的两端共用) +// expr_registry.hpp — 可融合表达式注册表(AOT 收集的载体) // // AOT 收集架构(表达式只在 Layer 里,别处一律不出现): -// ① 构建期 scan_exprs 用假张量 dry-run Layer 的 forward/backward, -// 每个 dsl::compute / compute_reduce / compute_into 在记录模式下把折叠出的 ExprSpec -// **结构**登记进全局注册表(按 expr_spec_key 去重)→ dump 成 bin。 -// ② 构建期 gen_fused 读 bin → 每 spec 生成 GLSL(glsl_gen.hpp)→ -// glslc → SPIR-V → 内联进生成头 fused_registry.hpp(key → spirv)。 +// ① 构建期 `scan_exprs` 收集结构:`FusedAnchor` 在静态初始化期按 +// 表达式**类型**登记(编译期可达,覆盖 dry-run 跑不到的分支),再由 +// dry-run / 模型 pass 补精度签名——每个 dsl::compute* 在记录模式下把 +// 折叠出的 ExprSpec 登记进本注册表(按 expr_spec_key 去重)。 +// ② 同一次运行内直接生成:`tools/fused_generate.hpp` 对每条 spec 出 +// GLSL(glsl_gen.hpp)→ glslc → SPIR-V → 内联进 fused_registry.hpp +// (key → spirv)。**不经 .bin 中间序列化**(原 gen_fused 已并入)。 // ③ 运行时 eval_expr 折叠内联表达式 → expr_spec_key → 查嵌入映射 -// → dispatch。未命中硬报错(闭合世界,提示扫描未覆盖该路径)。 +// → dispatch。未命中硬报错(闭合世界,见 expr_dsl.hpp 的锚点说明)。 // -// 表达式**文本**只出现在 Layer;bin / 生成头都是折叠后的**派生物**, +// 表达式**文本**只出现在 Layer;生成头是折叠后的**派生物**, // 非手写定义,故不违反"表达式只在 Layer"的约束。 // ═══════════════════════════════════════════════════════════════════════════ #include -#include -#include -#include #include #include #include -#include "core_file.hpp" // 二进制 POD 读写(cast 边界收敛点,docs/17 §2.1) #include "expr_spec.hpp" #include "expr_opt.hpp" @@ -46,7 +44,7 @@ struct ExprRegistry std::vector specs; std::unordered_set keys; // sig != 0(带类型变体):按 (结构 key, 精度签名) 去重。bin 只序列化 - // {sig, 基础结构下标}(见 write_registry 的变体段):变体与基础结构 + // {sig, 基础结构下标}:变体与基础结构**同结构同 key**,故只需下标引用。 // 同结构同 key,spec 体复用基础表条目,不重复落盘。 std::vector variants; std::unordered_set variant_keys; @@ -86,303 +84,5 @@ struct ExprRegistry return reg; } -// ── 二进制序列化(dump/load 共用同一格式)─────────────────────────────── -// 格式(小端,x86/ARM 通用): -// magic "NNEXP" (5B) + version (u8=kExprBinVersion,读回时严格相等匹配) -// count (u32) -// 每 spec:num_regs(u32) -// instrs: count(u32) × {op(u8) dst(u8) a.kind a.idx b.kind b.idx c.kind c.idx} -// views: count(u32) × {kind(u8) negate(u8) param(u32)} -// consts: count(u32) × Scalar -// rparams: count(u32) × Scalar (运行时标量参数,不进 key) -// matmul: has(u8=0/1);1 时 {a_input(u8) b_input(u8) transA(u8) -// transB(u8) k(u32) batch(u32)} -// fold: has(u8=0/1);1 时 {num_state(u8), k(u32), -// inits: count(u32) × Scalar, -// body: count(u32) × 指令(8B), -// finalize: count(u32) × 指令(8B) -// -- vec_state_len(u32), -// matmul: has(u8);1 时 {a,b,tA,tB(4B) k(u32) batch(u32)}, -// vecacc: has(u8);1 时 {vec_state,weight_reg,b_input, -// scale_reg,has_scale(5B)} -// -- tri_skip(u8)} -// 各段读写必须逐字段对称(不对称 = 后续 spec 全部错位读废)。 -inline constexpr std::uint8_t kExprBinVersion = 9; // 读回严格要求与该值相等,否则拒绝;当前格式 = 基础 spec 表 + 尾部精度变体段(每个变体 {sig, 基础结构下标}——变体与基础结构**同结构同 key**,故只存 sig + 下标,不重复存 spec 体);丢段 / 读写不对称 = 结构损坏(fold 段丢段 → 读回 spec 变"空指令表无段",多个 fold key 撞车合并 + gen_fused 对空表 UB 崩溃;tri_skip 不对称 → 静默不跳或错位读废) - -[[nodiscard]] inline bool write_registry(const std::string& path, - const ExprRegistry& reg) -{ - std::ofstream f(path, std::ios::binary); - if (!f) return false; - f.write("NNEXP", 5); - if (!write_pod(f, kExprBinVersion)) return false; - const std::uint32_t count = static_cast(reg.specs.size()); - if (!write_pod(f, count)) return false; - for (const auto& s : reg.specs) - { - if (!write_pod(f, s.num_regs)) return false; - std::uint32_t n = static_cast(s.instrs.size()); - if (!write_pod(f, n)) return false; - for (const auto& in : s.instrs) - { - std::uint8_t bytes[8] = { in.op, in.dst, in.a.kind, in.a.idx, - in.b.kind, in.b.idx, in.c.kind, in.c.idx }; - if (!write_pod_span(f, std::span(bytes, 8))) return false; - } - n = static_cast(s.views.size()); - if (!write_pod(f, n)) return false; - for (const auto& v : s.views) - { - if (!write_pod(f, v.kind)) return false; - if (!write_pod(f, v.negate_first_half)) return false; - if (!write_pod(f, v.param)) return false; - if (!write_pod(f, v.param2)) return false; // param2:RowAccess offset - } - n = static_cast(s.consts.size()); - if (!write_pod(f, n)) return false; - for (const auto& c : s.consts) - if (!write_pod(f, c)) return false; - n = static_cast(s.rparams.size()); - if (!write_pod(f, n)) return false; - for (const auto& r : s.rparams) - if (!write_pod(f, r)) return false; - const std::uint8_t has_mm = s.matmul ? 1 : 0; - if (!write_pod(f, has_mm)) return false; - if (s.matmul) - { - std::uint8_t mbytes[4] = { s.matmul->a_input, s.matmul->b_input, - s.matmul->transA, s.matmul->transB }; - if (!write_pod_span(f, std::span(mbytes, 4))) return false; - if (!write_pod(f, s.matmul->k)) return false; - if (!write_pod(f, s.matmul->batch)) return false; // batch 必须持久化 - } - // fold 段——丢段会让读回 spec 变"空指令表无段", - // 多个 fold key 撞车合并 + gen_fused 对空表 UB 崩溃 - const auto write_instr_seq = [&](const std::vector& seq) -> bool - { - std::uint32_t m = static_cast(seq.size()); - if (!write_pod(f, m)) return false; - for (const auto& in : seq) - { - std::uint8_t bytes[8] = { in.op, in.dst, in.a.kind, in.a.idx, - in.b.kind, in.b.idx, in.c.kind, in.c.idx }; - if (!write_pod_span(f, std::span(bytes, 8))) return false; - } - return true; - }; - const std::uint8_t has_fold = s.fold ? 1 : 0; - if (!write_pod(f, has_fold)) return false; - if (s.fold) - { - if (!write_pod(f, s.fold->num_state)) return false; - if (!write_pod(f, s.fold->k)) return false; - std::uint32_t m = static_cast(s.fold->inits.size()); - if (!write_pod(f, m)) return false; - for (const auto& iv : s.fold->inits) - if (!write_pod(f, iv)) return false; - if (!write_instr_seq(s.fold->body)) return false; - if (!write_instr_seq(s.fold->finalize)) return false; - // 双域字段(vec_state_len + 自带 matmul 段 + vecacc)—— - // 与 struct 声明序一致;漏写=读回结构损坏 - if (!write_pod(f, s.fold->vec_state_len)) return false; - const std::uint8_t has_fmm = s.fold->matmul ? 1 : 0; - if (!write_pod(f, has_fmm)) return false; - if (s.fold->matmul) - { - std::uint8_t mbytes[4] = { s.fold->matmul->a_input, - s.fold->matmul->b_input, - s.fold->matmul->transA, - s.fold->matmul->transB }; - if (!write_pod_span(f, std::span(mbytes, 4))) return false; - if (!write_pod(f, s.fold->matmul->k)) return false; - if (!write_pod(f, s.fold->matmul->batch)) return false; - } - const std::uint8_t has_va = s.fold->vecacc ? 1 : 0; - if (!write_pod(f, has_va)) return false; - if (s.fold->vecacc) - { - const VecAccSpec& va = *s.fold->vecacc; - std::uint8_t vbytes[5] = { va.vec_state, va.weight_reg, - va.b_input, va.scale_reg, - va.has_scale }; - if (!write_pod_span(f, std::span(vbytes, 5))) return false; - } - // tri_skip(1B,与 read 对称) - const std::uint8_t cskip = s.fold->tri_skip ? 1u : 0u; - if (!write_pod(f, cskip)) return false; - } - } - // 精度变体段({sig, 基础结构下标})—— 变体与其基础结构同 key(精度不进 - // expr_spec_key),故只需下标引用,避免重复序列化 spec 体(也免除读写不对称 - // 的风险:spec 体的读写已在上面单一实现)。找不到基础结构 → 写 0xFFFFFFFF - // (gen_fused 会跳过并告警,运行时不命中即回退边界 cast,正确性不受影响)。 - { - const std::uint32_t nvar = static_cast(reg.variants.size()); - if (!write_pod(f, nvar)) return false; - for (const auto& v : reg.variants) - { - if (!write_pod(f, v.sig)) return false; - std::uint32_t base = 0xFFFFFFFFu; - const std::string k = expr_spec_key(v.spec); - for (std::uint32_t i = 0; i < reg.specs.size(); ++i) - if (expr_spec_key(reg.specs[i]) == k) { base = i; break; } - if (!write_pod(f, base)) return false; - } - } - return static_cast(f); -} - -// 读回注册表(gen_fused 用)。返回 false 表示格式/路径错误。 -[[nodiscard]] inline bool read_registry(const std::string& path, ExprRegistry& out) -{ - std::ifstream f(path, std::ios::binary); - if (!f) return false; - char magic[5]; - f.read(magic, 5); - if (std::string(magic, 5) != "NNEXP") return false; - std::uint8_t ver = 0; - if (!read_pod(f, ver)) return false; - if (ver != kExprBinVersion) return false; - std::uint32_t count = 0; - if (!read_pod(f, count)) return false; - out.specs.clear(); - out.keys.clear(); - out.variants.clear(); - out.variant_keys.clear(); - out.specs.reserve(count); - for (std::uint32_t i = 0; i < count; ++i) - { - ExprSpec s; - if (!read_pod(f, s.num_regs)) return false; - std::uint32_t n = 0; - if (!read_pod(f, n)) return false; - s.instrs.resize(n); - for (auto& in : s.instrs) - { - std::uint8_t bytes[8]; - if (!read_pod_span(f, std::span(bytes, 8))) return false; - in.op = bytes[0]; in.dst = bytes[1]; - in.a.kind = bytes[2]; in.a.idx = bytes[3]; - in.b.kind = bytes[4]; in.b.idx = bytes[5]; - in.c.kind = bytes[6]; in.c.idx = bytes[7]; - } - if (!read_pod(f, n)) return false; - s.views.resize(n); - for (auto& v : s.views) - { - if (!read_pod(f, v.kind)) return false; - if (!read_pod(f, v.negate_first_half)) return false; - if (!read_pod(f, v.param)) return false; - if (!read_pod(f, v.param2)) return false; // param2:RowAccess offset - } - if (!read_pod(f, n)) return false; - s.consts.resize(n); - for (auto& c : s.consts) - if (!read_pod(f, c)) return false; - if (!read_pod(f, n)) return false; - s.rparams.resize(n); - for (auto& r : s.rparams) - if (!read_pod(f, r)) return false; - std::uint8_t has_mm = 0; - if (!read_pod(f, has_mm)) return false; - if (has_mm) - { - MatmulSpec mm; - std::uint8_t mbytes[4]; - if (!read_pod_span(f, std::span(mbytes, 4))) return false; - mm.a_input = mbytes[0]; mm.b_input = mbytes[1]; - mm.transA = mbytes[2]; mm.transB = mbytes[3]; - if (!read_pod(f, mm.k)) return false; - if (!read_pod(f, mm.batch)) return false; // batch 必须读回 - s.matmul = mm; - } - // fold 段读回(与 write 对称——不对称会让后续 spec 错位读废) - std::uint8_t has_fold = 0; - if (!read_pod(f, has_fold)) return false; - if (has_fold) - { - FoldSpec fs; - if (!read_pod(f, fs.num_state)) return false; - if (!read_pod(f, fs.k)) return false; - std::uint32_t m = 0; - if (!read_pod(f, m)) return false; - fs.inits.resize(m); - for (auto& iv : fs.inits) - if (!read_pod(f, iv)) return false; - const auto read_instr_seq = [&](std::vector& seq) -> bool - { - std::uint32_t c = 0; - if (!read_pod(f, c)) return false; - seq.resize(c); - for (auto& in : seq) - { - std::uint8_t bytes[8]; - if (!read_pod_span(f, std::span(bytes, 8))) return false; - in.op = bytes[0]; in.dst = bytes[1]; - in.a.kind = bytes[2]; in.a.idx = bytes[3]; - in.b.kind = bytes[4]; in.b.idx = bytes[5]; - in.c.kind = bytes[6]; in.c.idx = bytes[7]; - } - return true; - }; - if (!read_instr_seq(fs.body)) return false; - if (!read_instr_seq(fs.finalize)) return false; - // 双域字段读回(与 write 对称——不对称=后续 spec 错位读废) - if (!read_pod(f, fs.vec_state_len)) return false; - std::uint8_t has_fmm = 0; - if (!read_pod(f, has_fmm)) return false; - if (has_fmm) - { - MatmulSpec mm2; - std::uint8_t mbytes[4]; - if (!read_pod_span(f, std::span(mbytes, 4))) return false; - mm2.a_input = mbytes[0]; mm2.b_input = mbytes[1]; - mm2.transA = mbytes[2]; mm2.transB = mbytes[3]; - if (!read_pod(f, mm2.k)) return false; - if (!read_pod(f, mm2.batch)) return false; - fs.matmul = mm2; - } - std::uint8_t has_va = 0; - if (!read_pod(f, has_va)) return false; - if (has_va) - { - VecAccSpec va; - std::uint8_t vbytes[5]; - if (!read_pod_span(f, std::span(vbytes, 5))) return false; - va.vec_state = vbytes[0]; va.weight_reg = vbytes[1]; - va.b_input = vbytes[2]; va.scale_reg = vbytes[3]; - va.has_scale = vbytes[4]; - fs.vecacc = va; - } - // tri_skip 读回(与 write 对称——错一位=后续全错位) - { - std::uint8_t cskip = 0; - if (!read_pod(f, cskip)) return false; - fs.tri_skip = cskip != 0; - } - s.fold = fs; - } - out.add(s); - } - // 精度变体段读回(与 write 对称) - std::uint32_t nvar = 0; - if (!read_pod(f, nvar)) return false; - for (std::uint32_t i = 0; i < nvar; ++i) - { - ExprPrecSig sig = 0; - std::uint32_t base = 0xFFFFFFFFu; - if (!read_pod(f, sig)) return false; - if (!read_pod(f, base)) return false; - if (base < out.specs.size()) - { - const ExprSpec& bs = out.specs[base]; - out.variants.push_back(ExprVariant{bs, sig}); - out.variant_keys.insert( - expr_prec_sig_key(expr_spec_key(bs), sig)); - } - } - return true; -} - } // namespace nn::fused diff --git a/src/expr_fold_test.cpp b/src/expr_fold_test.cpp index e4761da1..69f57fbd 100644 --- a/src/expr_fold_test.cpp +++ b/src/expr_fold_test.cpp @@ -456,35 +456,30 @@ int test_expr_fold() } } - // ── 7) bin 序列化 roundtrip(v8 tri_skip 写读对称)───────────────── + // ── 7) 注册表去重 + key 契约(原 bin 序列化 roundtrip 的接替者)──────── + // 原先这里验证 write_registry/read_registry 往返不丢 tri_skip 位。bin 序列化 + // 已删除(构建期单步:scan_exprs 直接调 `generate_fused_registry`),改验 + // 注册表自身的两条不变量:① 同结构重复 add 只留一条;② tri_skip 进 key。 { - const std::string tmp = "nn_expr_fold_roundtrip.tmp.bin"; nn::fused::ExprRegistry reg; - reg.add(nn::expr::make_fold_attn_o(64, 4, 2, nn::expr::AttnMaskKind::Causal, /*score_bias=*/false)); // tri_skip=true - reg.add(nn::expr::make_fold_attn_o(64, 4, 2, nn::expr::AttnMaskKind::Plain, /*score_bias=*/false)); // tri_skip=false + const nn::ExprSpec sk = nn::expr::make_fold_attn_o( + 64, 4, 2, nn::expr::AttnMaskKind::Causal, /*score_bias=*/false); // tri_skip=true + const nn::ExprSpec pl = nn::expr::make_fold_attn_o( + 64, 4, 2, nn::expr::AttnMaskKind::Plain, /*score_bias=*/false); // tri_skip=false + reg.add(sk); + reg.add(pl); reg.add(nn::expr::make_fold_rowsum(64)); - const bool wok = nn::fused::write_registry(tmp, reg); - nn::fused::ExprRegistry back; - const bool rok = wok && nn::fused::read_registry(tmp, back); - std::remove(tmp.c_str()); - check(wok && rok && back.specs.size() == reg.specs.size(), - "registry roundtrip 写读成功 n=" + std::to_string(back.specs.size())); - if (wok && rok && back.specs.size() == reg.specs.size()) - { - // key 含 tri_skip 位 → 往返 key 全等即证该位与整段结构未丢 - bool keys_ok = true; - bool saw_skip = false; - for (std::size_t i = 0; i < reg.specs.size(); ++i) - { - if (nn::expr_spec_key(back.specs[i]) != - nn::expr_spec_key(reg.specs[i])) - keys_ok = false; - if (back.specs[i].fold && back.specs[i].fold->tri_skip) - saw_skip = true; - } - check(keys_ok, "registry roundtrip key 全等(结构+tri_skip 未丢)"); - check(saw_skip, "registry roundtrip tri_skip=true 读回仍 true"); - } + const std::size_t n1 = reg.specs.size(); + reg.add(sk); // 重复 add → 按 key 去重 + check(n1 == 3 && reg.specs.size() == 3, + "registry add 去重 n=" + std::to_string(reg.specs.size())); + check(nn::expr_spec_key(nn::canonicalize_expr_spec(sk)) != + nn::expr_spec_key(nn::canonicalize_expr_spec(pl)), + "fold tri_skip(Causal vs Plain)进 key:两者 key 必须不同"); + bool saw_skip = false; + for (const auto& s : reg.specs) + if (s.fold && s.fold->tri_skip) saw_skip = true; + check(saw_skip, "fold Causal → tri_skip=true 登记保留"); } return fail; diff --git a/tools/gen_fused.cpp b/tools/fused_generate.hpp similarity index 91% rename from tools/gen_fused.cpp rename to tools/fused_generate.hpp index 54796e0c..88eee4e0 100644 --- a/tools/gen_fused.cpp +++ b/tools/fused_generate.hpp @@ -1,23 +1,28 @@ // ─────────────────────────────────────────────────────────────────────────── -// gen_fused.cpp — AOT 算子融合:构建期合成(读 scan_exprs 的 bin) +// fused_generate.hpp — AOT 算子融合:构建期合成(scan_exprs 的生成阶段) // -// 读取 scan_exprs dump 的 expr_specs.bin(表达式结构集合),对每条 -// 用 emitter 抽象(IR-D:expr_emitter.hpp)展开为单个融合 kernel 源码, -// 再经 glslc 编译成 SPIR-V,最后内联进单个生成头 fused_registry.hpp -// (key → {ExprSpec, SPIR-V})。 +// 对注册表里的每条表达式,用 emitter 抽象(IR-D:expr_emitter.hpp)展开为 +// 单个融合 kernel 源码,再经 glslc 编译成 SPIR-V,最后内联进单个生成头 +// fused_registry.hpp(key → {ExprSpec, SPIR-V})。 // -// IR-D 落地:本工具经 nn::emitter_registry 选择后端(默认 "glsl" = -// GlslEmitter),不直接绑定 GLSL 生成函数——同一份 canonical IR 可由 -// 其它后端展开(--list-backends 查看已登记后端)。 +// **不再经由中间的 .bin 序列化**:本文件是头文件,由 `tools/scan_exprs.cpp` +// 直接 include 并在收集完注册表后原地调用 `nn::tool::generate_fused_registry` +// —— 构建期因此只有一步(scan + generate),`expr_registry.hpp` 的 +// `write_registry/read_registry/kExprBinVersion` 随之删除。 // -// 表达式**文本只出现在 Layer**;本工具只消费折叠后的结构(派生物)。 +// IR-D 落地:经 nn::emitter_registry 选择后端(默认 "glsl" = GlslEmitter), +// 不直接绑定 GLSL 生成函数——同一份 canonical IR 可由其它后端展开 +// (`scan_exprs --list-backends` 查看已登记后端)。 +// +// 表达式**文本只出现在 Layer**;这里只消费折叠后的结构(派生物)。 // 产物 fused_registry.hpp 供运行时(GpuEngine::eval_expr / vk_backend) // 按 expr_spec_key 精确匹配 dispatch——闭合世界,未命中硬报错。 // -// 用法: gen_fused [--list-backends] // 产物: /fused_registry.hpp(+ 调试用 .comp/.spv) // ─────────────────────────────────────────────────────────────────────────── +#pragma once + #include #include #include @@ -32,6 +37,7 @@ #include #include +#include "core_file.hpp" // nn::read_pod_span(读 glslc 产物 SPIR-V) #include "expr_spec.hpp" #include "expr_registry.hpp" #include "expr_emitter.hpp" // IR-D:emitter 抽象(后端选择) @@ -169,42 +175,24 @@ namespace } // namespace -int main(int argc, char* argv[]) +namespace nn::tool { - // --list-backends:列出可用 emitter 后端(IR-D 多后端验证) - for (int i = 1; i < argc; ++i) - if (std::string(argv[i]) == "--list-backends") - { - std::printf("[gen] 可用 emitter 后端(IR-D):\n"); - for (const auto& n : nn::emitter_registry::names()) - std::printf(" - %s\n", n.c_str()); - return 0; - } - - if (argc < 4) - { - std::fprintf(stderr, - "用法: gen_fused " - "[] [--list-backends]\n"); - return 2; - } - const std::string out_dir = argv[1]; - const std::string glslc = argv[2]; - const std::string bin_path = argv[3]; +// 生成融合 shader 注册表(见文件头说明)。reg 由调用方(scan_exprs)收集好后 +// 传入;prec_manifest 可为空(空 = 不做 run-only 签名回填)。 +[[nodiscard]] inline bool generate_fused_registry( + const std::string& out_dir, + const std::string& glslc, + const nn::fused::ExprRegistry& reg, + const std::string& prec_manifest = {}) +{ std::error_code ec; std::filesystem::create_directories(out_dir, ec); - nn::fused::ExprRegistry reg; - if (!nn::fused::read_registry(bin_path, reg)) - { - std::fprintf(stderr, "[FAIL] 无法读取表达式集合 %s\n", bin_path.c_str()); - return 1; - } if (reg.specs.empty()) { std::fprintf(stderr, "[FAIL] 表达式集合为空(scan_exprs 未覆盖任何路径)\n"); - return 1; + return false; } std::ostringstream H; @@ -267,7 +255,7 @@ int main(int argc, char* argv[]) if (!emitter) { std::fprintf(stderr, "[FAIL] 无法创建 GLSL emitter(IR-D 注册表异常)\n"); - return 1; + return false; } // 含归约(raxis >= 0)→ 归约 kernel(workgroup 级共享内存归约);否则逐元素 const std::string glsl = (raxis >= 0) @@ -276,23 +264,23 @@ int main(int argc, char* argv[]) if (glsl.empty()) { std::fprintf(stderr, "[FAIL] 生成 %s 失败(归约结构不支持?)\n", key.c_str()); - return 1; + return false; } { std::ofstream f(comp_path); - if (!f) { std::fprintf(stderr, "[FAIL] 无法写入 %s\n", comp_path.c_str()); return 1; } + if (!f) { std::fprintf(stderr, "[FAIL] 无法写入 %s\n", comp_path.c_str()); return false; } f << glsl; } if (!run_glslc(glslc, comp_path, spv_path)) { std::fprintf(stderr, "[FAIL] glslc 编译 %s 失败\n", key.c_str()); - return 1; + return false; } const auto spv = read_spv(spv_path); if (spv.empty()) { std::fprintf(stderr, "[FAIL] 读取 %s 失败\n", spv_path.c_str()); - return 1; + return false; } H << "inline constexpr std::uint32_t kSpirv_" << key << "[] = {"; @@ -402,9 +390,9 @@ int main(int argc, char* argv[]) // (tools/prec_backfill.txt,NN_PREC_TRACE=1 mem_probe 复现),对命中的 // (结构, 签名) 走与 scan 变体完全相同的 emit_one 发射(含 ALU 谓词、 // 重复去重);结构不在 bin 中 / 生成器不支持时告警跳过(不失败)。 - if (argc >= 5) + if (!prec_manifest.empty()) { - const std::string manifest = argv[4]; + const std::string& manifest = prec_manifest; std::ifstream mf(manifest); if (!mf) { @@ -502,10 +490,12 @@ int main(int argc, char* argv[]) const std::string reg_path = out_dir + "/fused_registry.hpp"; { std::ofstream f(reg_path); - if (!f) { std::fprintf(stderr, "[FAIL] 无法写入 %s\n", reg_path.c_str()); return 1; } + if (!f) { std::fprintf(stderr, "[FAIL] 无法写入 %s\n", reg_path.c_str()); return false; } f << H.str(); } std::printf("[gen] %zu 条融合表达式 + %zu 条精度变体 -> %s\n", reg.specs.size(), emitted_variants.size(), reg_path.c_str()); - return 0; + return true; } + +} // namespace nn::tool diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index 30da9db0..0f3eba15 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -30,6 +30,7 @@ #include "domain_mnist.hpp" #include "domain_rla.hpp" #include "expr_registry.hpp" +#include "fused_generate.hpp" // 生成阶段(原 gen_fused.cpp;不经 .bin 中间文件) #include "neuralnet.cpp/expr_fold.hpp" #include @@ -67,12 +68,27 @@ static nn::PrecisionProfile g_scan_prof{}; int main(int argc, char* argv[]) { - if (argc < 2) + // --list-backends:列出可用 emitter 后端(IR-D 多后端验证) + for (int i = 1; i < argc; ++i) + if (std::string(argv[i]) == "--list-backends") + { + std::printf("[scan] 可用 emitter 后端(IR-D):\n"); + for (const auto& n : nn::emitter_registry::names()) + std::printf(" - %s\n", n.c_str()); + return 0; + } + if (argc < 3) { - std::fprintf(stderr, "用法: scan_exprs \n"); + std::fprintf(stderr, + "用法: scan_exprs [] " + "[--list-backends]\n" + " 收集 Layer 内联表达式(锚点自登记 + dry-run + 模型 pass),\n" + " 原地合成 /fused_registry.hpp(不经 .bin 中间文件)。\n"); return 2; } - const std::string out_path = argv[1]; + const std::string out_dir = argv[1]; + const std::string glslc = argv[2]; + const std::string manifest = (argc >= 4) ? argv[3] : std::string{}; std::signal(SIGABRT, &on_abort); // NN_ASSERT → abort 带栈(见文件头) nn::CpuEngine raw_engine; @@ -594,13 +610,7 @@ int main(int argc, char* argv[]) "[scan][warn] 锚点未贡献任何结构:可能被 -DNN_SCAN_NO_ANCHOR 关闭," "或所有调用点都已被 dry-run 覆盖\n"); } - if (!nn::fused::write_registry(out_path, reg)) - { - std::fprintf(stderr, "[FAIL] 无法写入 %s\n", out_path.c_str()); - return 1; - } - std::printf("[scan] 收集到 %zu 条融合表达式 -> %s\n", - reg.specs.size(), out_path.c_str()); + std::printf("[scan] 收集到 %zu 条融合表达式\n", reg.specs.size()); // ── 签名覆盖报告:哪些结构**没有任何带类型变体** ────────────────────── // 自登记只能给结构(sig=0):精度签名取决于运行期张量精度,类型层面推不出来 // → f16 带类型变体仍须 dry-run 的 f32/f16 两遍产生。缺变体不是错误 @@ -643,5 +653,9 @@ int main(int argc, char* argv[]) { std::printf("[scan] 精度变体 0 条(f16 pass 未产生非零签名)\n"); } + // ── 生成阶段(原 gen_fused):同一进程内直接消费注册表 → fused_registry.hpp + // 构建期因此只有一步;不再经 expr_specs.bin 中间序列化。 + if (!nn::tool::generate_fused_registry(out_dir, glslc, reg, manifest)) + return 1; return 0; } From 2c29e02b160ab68f65b0acf7866a322889a1a3bb Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 16:12:53 +0800 Subject: [PATCH 14/17] =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=EF=BC=9A=E5=8E=BB=E6=8E=89=E5=AF=B9=E5=B7=B2=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=96=87=E4=BB=B6=20gen=5Ffused.cpp=20=E7=9A=84=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E6=80=81=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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)**。纯文档改动,无代码影响。 --- docs/development/02-operator-fusion.md | 2 +- docs/introduction/01-architecture.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index 78b3be48..22e5acf6 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -234,7 +234,7 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba 1. **`tools/scan_exprs.cpp`**(收集 + 生成同一进程): · **收集**:`FusedAnchor` 在静态初始化期按表达式**类型**登记结构(编译期可达),再由 dry-run per-layer 块 + 模型级配置矩阵补精度签名;每个 `dsl::compute*` 在记录模式下把折叠出的 `ExprSpec` 登记进注册表(按 key 去重)。 - · **生成**:`tools/fused_generate.hpp`(原 `gen_fused.cpp`)对每条 spec 出 `glsl_gen` GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。 + · **生成**:`tools/fused_generate.hpp`(原独立工具 `gen_fused`,现已并入本工具)对每条 spec 出 `glsl_gen` GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。 · **不经 `.bin` 中间序列化**(`expr_registry.hpp` 的 `write_registry/read_registry/kExprBinVersion` 已删除)。 `scan_exprs` 需覆盖所有 Layer 的 DSL 路径(Softmax/LN/RMSNorm fwd+bwd、CrossEntropy softmax 结构、**Attention fold 结构**——层 forward 直调 `engine.eval_expr(make_fold_attn_o(...))` 不经 DSL 钩子,scan 的显式登记块(3 掩码 × 2 偏置的 5 个组合)是 fold spec 唯一注册来源、漏组合即 GPU 闭合世界硬报错、Linear 的 matmul 段、optimizer 的 `compute_into` 原地表达式),使融合签名被收集。**结构**已由自登记锚点保证(见下节),未命中 → `eval_expr` 硬报错(保持项目"GPU 硬报错、不降级"哲学)。 diff --git a/docs/introduction/01-architecture.md b/docs/introduction/01-architecture.md index 6ad6d117..5c1669e5 100644 --- a/docs/introduction/01-architecture.md +++ b/docs/introduction/01-architecture.md @@ -286,7 +286,7 @@ class Tensor { | `expr_registry.hpp` | 构建期表达式注册表(收集折叠出的结构 + 精度变体,按 key 去重;**不再有 bin 序列化**——收集与生成同一进程) | | `fused_registry.hpp` | **生成物**(构建期 生成阶段 产出):`key → {ExprSpec 结构, 内联 SPIR-V}` 融合 shader 注册表;运行时按 key 精确匹配 | | `tools/scan_exprs.cpp` | 构建期工具(**单步** = 收集 + 生成):`FusedAnchor` 按类型自登记结构 + dry-run/模型 pass 补签名,随后原地生成 `fused_registry.hpp` | -| `tools/fused_generate.hpp` | 生成阶段(原 `gen_fused.cpp`,现为头文件):注册表 → `glsl_gen` → glslc → 内联 SPIR-V → `fused_registry.hpp` | +| `tools/fused_generate.hpp` | 生成阶段(原独立工具 `gen_fused`,现为 `scan_exprs` include 的头文件):注册表 → `glsl_gen` → glslc → 内联 SPIR-V → `fused_registry.hpp` | | `eval_expr` | `ComputeEngine` 虚接口:CPU 编译期模板求值(经 `dsl::compute`);Vulkan 按 `expr_spec_key` 查 `fused_registry`(闭合世界,未命中硬报错,无 eager、无运行时编译) | | `dsl::compute(engine, expr, rows, cols)` | 统一求值入口:CPU 走编译期模板;GPU 折叠成 `ExprSpec` → `eval_expr` 按 key AOT 分发 | From b48dd5a04118d13e36f8e030967261726a5af740 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 17:04:49 +0800 Subject: [PATCH 15/17] =?UTF-8?q?run-only=20=E7=AD=BE=E5=90=8D=E5=9B=9E?= =?UTF-8?q?=E5=A1=AB=EF=BC=9Atext=5Ftrain=EF=BC=88tinystories=20smoke?= =?UTF-8?q?=EF=BC=89=C3=97=20f16=20=E7=9A=84=202=20=E6=9D=A1=20miss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用真实训练负载(用户口径)跑 `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` 逐位不变(本提交只增加构建期变体,不触碰库代码)。 --- tools/prec_backfill.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/prec_backfill.txt b/tools/prec_backfill.txt index 1ffbb5f1..03ba74cb 100644 --- a/tools/prec_backfill.txt +++ b/tools/prec_backfill.txt @@ -50,3 +50,10 @@ b7e6d363af1963ac 10000 8a29d02213a0e196 10003 b7e6d363af1963ac 10002 27c7edd7a0a4f808 10007 + +# ── text_train(tinystories smoke)× f16 的 run-only 签名(2026-10-01)─────── +# 复现:NN_PREC_TRACE=1 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] +b7e6d363af1963ac 0001 # GPT:纯 matmul 段,in=[f16,f32] out=f32 +a1d46a05bb060c47 10001 # RAPT:8 指令链(含 4 个列归约视图),in=[f16,f32×4] out=f16 From 9300bf847461f84195c15c06c40b65271ecbc0f3 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 17:16:57 +0800 Subject: [PATCH 16/17] =?UTF-8?q?=E9=94=9A=E7=82=B9=E4=BB=B7=E5=80=BC?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E9=AA=8C=E8=AF=81=20+=20=E9=97=AD?= =?UTF-8?q?=E5=90=88=E4=B8=96=E7=95=8C=E6=8A=A5=E9=94=99=E5=B8=A6=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本轮把"锚点覆盖"从集合对比升级为**运行时证据**,顺带修掉闭合世界报错不可定位的问题。 ## 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` 是登记/查表两侧的唯一标识,可直接去 `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。 --- docs/development/02-operator-fusion.md | 8 ++++++++ include/neuralnet.cpp/compute_gpu_engine.hpp | 16 +++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index 22e5acf6..9b51555b 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -266,6 +266,14 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba `src/expr_fused_key_test.cpp` 锁定(并入 `expr_cpu_test` 聚合目标,ctest 目标数不变)。 任一条被违反(有人重新引入"运行期值决定结构")→ 运行时 key 与登记 key 不一致 → 闭合世界 miss;测试会立刻红。 +- **运行时证据(2026-10-01)**:关掉锚点(`-DNN_SCAN_NO_ANCHOR`,结构 84 → 71)后,`text_train` / `mnist_train --f16` / `gpu_stability_probe` 全部照常通过,但 **`text_infer`(KV-cache 增量解码)在 GPU 上闭合世界硬报错**: + + ``` + GpuEngine::eval_expr: 未找到该内联表达式的 AOT 融合 shader(闭合世界);… + key=9ca81b4967cdfa20 + ``` + + 该 key = `mm=1 + rparam×1 + 两个 Linear 输入`,即 `forward_step` 的注意力打分链——**手写 dry-run 从未覆盖过的路径**。打开锚点后同一命令正常(8 tokens / 0.2s)。⇒ 锚点不是纯理论安全网:它挡住了一个**已发布负载**的硬报错。(闭合世界报错现在都会打印 `key=` 以便定位。) - 非法结构(裸视图作根 → 空指令表)在构建期即 `_Exit(3)`,与 dry-run 同一闸门。 - `-DNN_SCAN_NO_ANCHOR` 可关掉锚点做 A/B(只影响登记集合,不影响语义)。 diff --git a/include/neuralnet.cpp/compute_gpu_engine.hpp b/include/neuralnet.cpp/compute_gpu_engine.hpp index 7837333f..6ca205a1 100644 --- a/include/neuralnet.cpp/compute_gpu_engine.hpp +++ b/include/neuralnet.cpp/compute_gpu_engine.hpp @@ -1433,7 +1433,7 @@ class GpuEngine final : public ComputeEngine #endif return std::unexpected(Error{ "GpuEngine::eval_expr: fold 表达式未命中 AOT 融合 shader" - "(闭合世界);请将该 fold 结构纳入 scan_exprs"}); + "(闭合世界);请将该 fold 结构纳入 scan_exprs;key=" + fkey}); } // ── canonical IR:canonicalize 为引擎内部优化(IR-A/IR-B), // key 与 shader 合成两端一致;dispatch 用 canonical 的 consts ── @@ -1483,11 +1483,15 @@ class GpuEngine final : public ComputeEngine if (psig != 0) return std::unexpected(Error{ "GpuEngine::eval_expr: 该 (结构,精度) 变体未预生成(in-kernel f16 覆盖不足)" - ";NVI 入口本应在此前抬到 f32(supports_expr_precision_variant 与 shader 查询不一致)"}); + ";NVI 入口本应在此前抬到 f32(supports_expr_precision_variant 与 shader 查询不一致)" + ";key=" + key}); // ── 闭合世界:未命中任何 AOT 融合 shader → 硬报错(绝不静默回退) ── + // 带上 key:闭合世界报错必须可定位——key 是登记/查表两侧的唯一标识, + // 可用来查 fused_registry.hpp 里有没有该条目、以及它属于哪个 Layer 路径。 return std::unexpected(Error{ "GpuEngine::eval_expr: 未找到该内联表达式的 AOT 融合 shader(闭合世界);" - "请将对应表达式纳入构建期扫描(scan_exprs dry-run 需覆盖该 Layer 路径)"}); + "请将对应表达式纳入构建期扫描(scan_exprs dry-run 需覆盖该 Layer 路径);" + "key=" + key}); } // ── 归约向量原生形状输出(LayerNorm/RMSNorm 小向量缓存) ──────── @@ -1606,7 +1610,8 @@ class GpuEngine final : public ComputeEngine // ── 闭合世界:未命中归约融合 shader → 硬报错(绝不静默回退) ── return std::unexpected(Error{ "GpuEngine::eval_expr_reduce: 未找到该归约表达式的 AOT 融合 shader(闭合世界);" - "请将对应表达式纳入构建期扫描(scan_exprs dry-run 需覆盖该 Layer 路径)"}); + "请将对应表达式纳入构建期扫描(scan_exprs dry-run 需覆盖该 Layer 路径);" + "key=" + key}); } // ── 目标传递(destination-passing):结果直接写回已有张量 ───────────── @@ -1693,7 +1698,8 @@ class GpuEngine final : public ComputeEngine // ── 闭合世界:未命中 AOT 融合 shader → 硬报错(绝不静默回退) ── return std::unexpected(Error{ "GpuEngine::eval_expr_into: 未找到该表达式的 AOT 融合 shader(闭合世界);" - "请将对应表达式纳入构建期扫描(scan_exprs dry-run 需覆盖该 Layer 路径)"}); + "请将对应表达式纳入构建期扫描(scan_exprs dry-run 需覆盖该 Layer 路径);" + "key=" + key}); } private: From 06157d570d940cd3d35bbb53a4cef44ff5cef0ee Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 17:24:59 +0800 Subject: [PATCH 17/17] =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=EF=BC=9AAGENTS.md=20=E8=B7=9F=E8=BF=9B=20AOT=20=E8=9E=8D?= =?UTF-8?q?=E5=90=88=E7=8E=B0=E7=8A=B6=20+=2013=20=E5=8F=B7=E6=B8=85?= =?UTF-8?q?=E5=8D=95=E5=BD=92=E6=A1=A3=E5=85=A5=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两部分(用户裁定"完成",故一并提交): ## 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` 按类型自登记**结构** + 模型 配置矩阵产生**精度签名**,同进程直接生成 `fused_registry.hpp`,不经 `.bin`)。 - §12 约束 4:改写为"**key 只描述结构**"——常量池**值** / `MatmulSpec.transA·transB` / 分组归约 **R** 三者不进 key(分别走 PC `c` / `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`(数据产物,与本次文档同步无关)。 --- AGENTS.md | 27 +- docs/development/13-refactor-backlog.md | 334 +----------------------- docs/development/18-roadmap.md | 296 +++++++++++++++++++++ docs/history.md | 87 ++++++ 4 files changed, 411 insertions(+), 333 deletions(-) create mode 100644 docs/development/18-roadmap.md diff --git a/AGENTS.md b/AGENTS.md index 62733d01..b7810a45 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ cmake -B build -G Ninja -DNN_ENABLE_TESTS=ON && cmake --build build && ctest --t | 模型容器/规格/序列化 | `model_container.hpp` / `model_spec.hpp` / `model_serialization.hpp` / `model_keyvalue_record.hpp` | | MNIST / GPT / CNN / RLA / 分词器 模型工厂 | `domain_mnist.hpp` / `domain_gpt.hpp` / `domain_cnn.hpp` / `domain_rla.hpp` / `domain_tokenizer{,_base,_bpe,_charbpe}.hpp` | | 训练/推理 CLI 入口 | `src/mnist_train.cpp` 等;公共 CLI 逻辑在 `include/neuralnet.cpp/cli/`(**`cli_help.hpp` = 各 `--help` 的统一排版器**:宽度感知对齐、统一版式;改帮助/加选项前先读其文件头的审计约定) | -| 构建期工具(AOT 融合) | `tools/scan_exprs.cpp` / `tools/gen_fused.cpp`(另有 `tools/decode_fused.py` 调试用) | +| 构建期工具(AOT 融合) | `tools/scan_exprs.cpp`(**单步** = 收集 + 生成)/ `tools/fused_generate.hpp`(生成阶段,原独立工具 `gen_fused`;另有 `tools/decode_fused.py` 调试用) | | 批量改写 / 一致性审计(改多处时用,均带 `-DryRun`) | `tools/edit_ranges.ps1`(行区间删除:四重断言 + **花括号平衡护栏**)/ `tools/test_refactor.ps1`(删定义块 / 插 include / 正则替换)/ `tools/apply_nn_try.ps1`(`auto X = f(); if (!X) …` → `NN_TRY`/`NN_TRY_CHECK`,L2 层,带 `-DryRun` 计数)/ `tools/doc_rename.ps1`(文档词法改名)/ `bench/doc_align_audit.ps1`(文档↔代码对齐审计:文件/符号/CLI/数字/测试名)/ `bench/doc_inventory.ps1`(引擎接口盘点 + **第 [4] 节 L2+ 分层审计:`Matrix`/Matrix 型 I/O 动词零命中门禁,铁律 #12**) | | 与 PyTorch 对拍 | `compare_with_torch/`(model.py / text_train.py / text_infer.py) | | GUI ↔ CLI 一致性(改 `gui.py`/`cli_controllers.py`/任一 CLI 选项后跑) | `bench/gui_cli_audit.py`(三节:GUI→CLI flag 差集 + 反向缺失、幽灵选项=帮助声明 vs 解析分支、`gui.py` 的 `self.X` 赋值静态校验;退出码 0 = 无偏差。复现:`python bench/gui_cli_audit.py`) | @@ -175,11 +175,11 @@ GPT 序列展平: 列序 i = b*seq + t(batch-major,全局唯一约定) ## 7. 表达式 DSL 与 AOT 融合管线(GPU 开发必读) - Layer 内用 `nn::dsl`(`expr_dsl.hpp`)写普通数学表达式;CPU 编译期模板直接求值(内联+SIMD),GPU 折叠成 `ExprSpec`(扁平 IR,`expr_spec.hpp`)→ 按 key 查预编译融合 shader。 -- 主要入口:`dsl::compute(engine, expr, rows, cols)`(一行表达式,最常用——**必带输出形状**,没有 2 参重载);把结果写进既有张量(原地更新,零分配)用 `dsl::compute_into(engine, expr, dst)`;归约语义用 `dsl::compute_reduce`。**跨表达式融合(IR-C:`start_expr/end_expr`、`begin_expr/end_expr`、`expr_graph.hpp`)未采用**,当前没有任何跨表达式录制机制——取舍记录与重新立项前提见 `docs/development/03-ir-optimization.md` §5.3。分组归约(MaxPool 窗口等)用 `dsl::grouped_reduce_sum/max(tensor, R)` 视图(R 进 key);**alpha 缩放**写成 `dsl::matmul(..., batch) * dsl::rparam(alpha)` 尾链(rparam 值不进 key)。 +- 主要入口:`dsl::compute(engine, expr, rows, cols)`(一行表达式,最常用——**必带输出形状**,没有 2 参重载);把结果写进既有张量(原地更新,零分配)用 `dsl::compute_into(engine, expr, dst)`;归约语义用 `dsl::compute_reduce`。**跨表达式融合(IR-C:`start_expr/end_expr`、`begin_expr/end_expr`、`expr_graph.hpp`)未采用**,当前没有任何跨表达式录制机制——取舍记录与重新立项前提见 `docs/development/03-ir-optimization.md` §5.3。分组归约(MaxPool 窗口等)用 `dsl::grouped_reduce_sum/max(tensor, R)` 视图(**R 走运行期视图参数 `vp`、不进 key**——同一结构不同 R 共享一个 shader);**alpha 缩放**写成 `dsl::matmul(..., batch) * dsl::rparam(alpha)` 尾链(rparam 值不进 key)。 - **fold 段(P-C1/C2)**:`ExprSpec.fold = FoldSpec`(分块状态归约:键域逐块 body + 行标量态跨块进位 + `vecacc` 行向量态块累加 + 向量域 finalize)——**通用折叠表达式机制,不含任何注意力专属语义**(与注意力无关的通用样例 rowmax/rowsum/softmax_denom 在 `expr_fold.hpp`)。注意力 forward 直调 `engine.eval_expr(make_fold_attn_o(...))`(**不经 DSL 钩子——scan 显式登记块是 fold spec 唯一注册来源**);该构造与**掩码种类 `AttnMaskKind`(Plain/Causal/CausalDoc)定义在 `compute_layer_attention.hpp`**(按 AOT 原则"表达式文本只出现在 Layer"归位)——位置偏置是**正交的第二入参** `bool score_bias`(由 `PositionEncoder::has_score_bias()` 给出),实际登记 5 个组合(3 掩码 × 2 偏置 − 1;漏登记即 GPU 闭合世界硬报错);`FoldSpec.tri_skip`(行界整块跳过)进 key(codegen 分歧点);`EXPR_FOLD_BLOCK=128`/`EXPR_FOLD_ROWS_PER_WG=2` 为 CPU/GPU 共享常量(改则两侧同改)。详见 `expr_fold.hpp` 与 `expr_spec.hpp` 的 FoldSpec 注释。 -- **构建期两步**(CMake 自动编排,改 Layer 内联表达式后重跑构建即可): - 1. `scan_exprs`:dry-run 跑 Layer forward/backward,收集折叠出的 `ExprSpec` 结构(去重)→ `build/generated/expr_specs.bin` - 2. `gen_fused`:读 bin → 经 `emitter_registry` 选后端(默认 `"glsl"` = `GlslEmitter`)生成 GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp` +- **构建期单步**(CMake 自动编排,改 Layer 内联表达式后重跑构建即可)——`scan_exprs` 同进程**收集 + 生成**: + 1. **收集**:`FusedAnchor` 在静态初始化期按表达式**类型**自登记**结构**(编译期可达,dry-run 跑不到的分支也覆盖),再由 per-layer dry-run + 模型级配置矩阵(f32/f16 两遍)执行产生**精度签名**——**结构能从类型推、签名必须执行期发现**。 + 2. **生成**:`tools/fused_generate.hpp`(原独立工具 `gen_fused`)经 `emitter_registry` 选后端(默认 `"glsl"` = `GlslEmitter`)生成 GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。**不经 `.bin` 中间序列化**(`write_registry/read_registry` 已删除)。 - **IR-D emitter 抽象**(`expr_emitter.hpp`):把后端代码生成从 GLSL 专用抽象为 emitter 接口(一份 canonical IR → 多后端代码),`--list-backends` 可列出注册后端。目前仅 `glsl` 后端注册(无 `CpuEmitter`/`CudaEmitter` 实现)。 - 手写原语 shader 在 `shaders/*.comp`(matmul、matmul_tiled、matmul_gemv、batched_matmul、reduce、elementwise_v2、transpose、gather、scatter_add、rearrange_3d、im2col、col2im、group_reduce、scan_prefix_outer、scan_suffix_outer、scan_prefix_outer_gen、scan_suffix_outer_gen(gen = 通用 dk 版,dk>64 后端走它)、outer_col、cast),构建期 glslc 编译并嵌入 C++ 头文件;多数 shader 另用 `-DNN_SHADER_F16=1` 编一份 f16 变体(`NN_ETYPE=float16_t`,f32 分支留在 `#else`)。 - IR 优化 pass(canonicalize/CSE/寄存器分配)见 `expr_opt.hpp`,设计文档 `docs/development/03-ir-optimization.md`(IR-C 取舍记录在其 §5.3,归档正文在 `docs/history.md`)。 @@ -260,10 +260,11 @@ optimizer.step(); | `development/08-pitfalls-and-lessons.md` | **踩坑警示录,改代码前读** | | `development/10-development-standards.md` | C++ 编码规范全文 | | `development/12-compute-engine-inventory.md` | **引擎接口盘点(复现:`bench/doc_inventory.ps1`):49 个 virtual 方法、Layer 直调 21 个(基础设施/数据搬运/状态扫描/fold 登记)、两套 CPU 求值机制(DSL 模板 + IR 解释器)、ctest 19 个测试;演进记录见 `docs/history.md`** | -| `development/13-refactor-backlog.md` | **重构与性能机会清单(2026-09-25 审查):只记录方案不实施;误报/已修复项对照表与已执行记录在 `docs/history.md`,重复立项前先读本文件** | +| `development/13-refactor-backlog.md` | **已合并的重定向 stub(2026-10-01):未完成项与裁定台账迁入 `development/18-roadmap.md`;已完成 / 被否决 / 已执行记录在 `docs/history.md`** | | `development/15-computeengine-refresh.md` | **ComputeEngine Refresh 详细设计(13 §10 展开):张量出生绑定 + `import` + 存储多态;含 P-1(PrecisionEngine 下沉删除)与 P1(出生绑定 + `bind_check_` 跨引擎检查 + `adopt` 内部通道,**两项均已实施 2026-09-29**)与 D1-D9 未决点裁定;**未实施的 P2-P6 已被 17 吸收改期(M1-M7),后续立项读 17** | | `development/16-computeengine-p0-inventory.md` | **Refresh P0 盘点结果(2026-09-28):ensure_gpu 43 分类 / ComputeEngine& 175 打标 / 宿主中转 381 清单 / ctest 双基线(Lavapipe 20 20、Mali offload 非确定)/ GPU 稳定性探针与未决 7 项** | | `development/17-unified-tensor-engine.md` | **统一 Tensor/ComputeEngine/MemoryPool 底层架构总纲(2026-09-30 裁定;M1-M6 全部已实施 2026-09-30,仅 M7 未立项):访问不变量(Tensor 存储私有、一切经引擎,已立为铁律 #11)、InitSpec 声明式初始化、批量 read/write、Matrix 降级为宿主 I/O 载体(L2+ 禁用 = 铁律 #12)、内存池契约统一、`import` 跨设备拉取、Layer 删每调用 engine 形参(M6 段 C,Loss/DSL/辅助对象保留形参见 §8);吸收 15 未实施的 P2-P6(改期 M1-M7)。立项前先读本文件 + §5 分期 + §8** | +| `development/18-roadmap.md` | **未来方向路线图 + 未完成项裁定台账(2026-10-01,13 号清单的承接者):P0-P3 分期、13 号逐条裁定(做/条件触发/不做)、触发式立项条件、非目标、门禁与验收口径。要规划下一步、查某件事该不该做、避免重复立项时读本文** | | `development/14-f16-stable-gpu-loss-frozen.md` | **故障报告(2026-09-26,未修):GPU `stable=f16` 训练 loss 打印冻结(权重不冻结)——触发矩阵、测试覆盖缺口、证据与复现** | ### 使用类(docs/usage/) @@ -291,13 +292,13 @@ optimizer.step(); - **混合精度**:`precision.hpp` 的 `Precision`/`PrecisionProfile{param/compute/stable/optimizer}`;CPU/GPU f16 路径 = 边界 cast(`ComputeEngine` 基类 NVI 入口,**原 `PrecisionEngine` 装饰器已删除下沉**,见 15 §4.1)+ in-kernel f16 带类型变体 + op-level f16 GEMM(一份 .comp 用 `-DNN_SHADER_F16=1` 编第二份 SPIR-V);`--f16` = `profile_f16()` = {param:F16, compute:F16, stable:F32, optimizer:F32},实测峰值显存低于 f32(`docs/development/05` §12.11)。 - **线性注意力**:RLA-2 / RAPT(`docs/development/06`)。 - **张量出生绑定(Refresh P1,2026-09-29)**:`Tensor::engine_` observer + `bound()`/`engine()`;库内产物出生即绑定——引擎公共入口(含 `create_tensor`/`from_matrix`/`cast` 等已 NVI 化的工厂)统一 `stamp_`,`dsl::compute`/`compute_reduce` 静态工厂出口经内部 `adopt` 通道;公共入口带 `bind_check_` 跨引擎检查(**双方都绑定且指针不同 → 硬错误**,单侧未绑定放行;`NN_BIND_DEBUG=1` 时未绑定输入也报错,用于抓库内漏网与库外迁移清单)。**库内新增 Tensor 出生点必须走引擎入口或 `adopt`,新引擎入口须带 `bind_check_`+`stamp_`**(模板见 `docs/development/01` 步骤 1,取舍见 15 §3.1/§4.3)。 -- **访问收口(统一总纲 M1,2026-09-30)**:Tensor 存储访问器与静态直构工厂**私有化**(铁律 #11),库外(9 测试文件 + text_train + scan_exprs)全部迁 `engine.create_tensor/from_matrix/to_matrix`;`ComputeEngine::reshape` 落地(D10,元素数不匹配返回 Result 错误);DSL 求值器经 `detail::TensorAccess` 域内通道。验收:ctest 20/20 + CPU 探针逐位一致 + scan 双 hash 不变(详见 17 §5 M1 行与 `docs/history.md`)。 +- **访问收口(统一总纲 M1,2026-09-30)**:Tensor 存储访问器与静态直构工厂**私有化**(铁律 #11),库外(9 测试文件 + text_train + scan_exprs)全部迁 `engine.create_tensor/from_matrix/to_matrix`;`ComputeEngine::reshape` 落地(D10,元素数不匹配返回 Result 错误);DSL 求值器经 `detail::TensorAccess` 域内通道。验收:ctest 19/19 + CPU 探针逐位一致 + scan 双 hash 不变(详见 17 §5 M1 行与 `docs/history.md`)。 - **声明式初始化(统一总纲 M2,2026-09-30)**:`InitSpec`(Uninitialized/Zero/Constant/Uniform/Normal)+ `engine.create_tensor(rows, cols, P, spec)` 四参重载——**层算分布参数、引擎填数**(host 生成后上传,策略调用方不可见);分布类 seed 必填(U1 裁定,层传 `kInitSeed = 42`),引擎内按创建序号混流防同 seed 撞流(同形状多层不互为镜像)。`Layer::init` 全部迁声明式(Linear/Conv2D/LayerNorm/RMSNorm/token_emb/可学习位置编码/ones_row_),mlp/conv 的 `thread_local rng_` 与 init 侧 `random_device` 清零——**初值跨进程逐字节确定**;`generate()` 采样 RNG 与 `text_train` 数据洗牌 RNG 属运行期随机性,裁定不迁。验收:build 零告警 + ctest 全绿 + `gpu_stability_probe --init-hash` 全部模型跨进程 hash 一致 + scan 双 hash 不变(详见 17 §5 M2 行与 `docs/history.md`)。**ZiPT 移除后为 5 类模型**,当前锚:mnist_mlp `a22e807ee05ec3ac` / cnn `4020958a14160bbd` / mnist_transformer `04a72d865624042a` / gpt `8efa936ac5c8c9b2` / rapt `d1998412d41ef2a0`(rapt 锚由 `b037632f75b7159c` 变化的原因——探针同引擎顺序构建、`InitSpec` seed 按创建序号混流——见 `docs/history.md`「ZiPT 移除」条)。 -- **批量读写(统一总纲 M3,2026-09-30)**:`ComputeEngine` I/O 分组新增 `read/write/get_index/set_index`(与 `from_matrix/to_matrix` 同组,D9 保留原名)——**批量 `read/write` 是本体**:span 元素类型与 `precision()` 精确匹配(U2:float↔F32、f16↔F16,错配运行期错误、类型非法编译期 static_assert),GPU `read` 隐含 flush+同步(走 `to_matrix` 同路)、`write` 覆盖既有存储不替换对象(宿主直写 / GPU 走 `copy_from` drain);**索引级是语法糖**(宿主直读写,GPU 每次一整轮 staging,不承诺热循环性能)。测试 f32 填充/恢复路径迁 `write`(f16 转换填充保留 `copy_from`——其语义就是 f32 Matrix→f16 的转换入口)。顺带修复稳定性探针漏 `backend.initialize()` 的 GPU 崩溃(16 §7-2 根因)并补测:dev0/dev2/dev4 进程内+跨进程 loss/hash 逐字节一致。验收:build 122/122 + ctest 20/20 + `--io-roundtrip` CPU/GPU 全过 + CPU 字节锚不变 + scan 双 hash 不变(详见 17 §5 M3 行与 `docs/history.md`)。 -- **Matrix 降级收口(统一总纲 M4,2026-09-30)**:铁律 #12 新立——**L2+(Layer/Loss/Optimizer/Model)头文件零 `Matrix`、零 `from_matrix/to_matrix/copy_from`**;层自算的辅助数据(RoPE cos/sin、正弦位置编码、ALiBi 斜率/偏置、文档掩码与边界、位置/词元索引、卷积置换与广播索引、patch 提取与散射、labels/mask 打包…共 45 处 I/O + 32 处类型)全部迁 span 宿主桥 `detail::upload_span/download_span/download_vector`(内部 = M3 批量 `write/read`,f16 目标按 RHE 同口径)或 `create_tensor`+`InitSpec`/`zero`。**审计**:`bench/doc_inventory.ps1` 新增第 [4] 节分层审计(注释剥离后匹配,`L2-VIOLATIONS` 验收口径 0,宿主桥用量只披露);U5 落点裁定 = 扩展 doc_inventory。**字节零变化**:CPU `--steps 20` hash=`6f8849f14da23110`、GPU dev2 `8ef51b2927253c50` 均与迁移前逐位一致,scan 双 hash 不变,ctest 20/20(详见 17 §3 D11/§5 M4 行与 `docs/history.md`)。 +- **批量读写(统一总纲 M3,2026-09-30)**:`ComputeEngine` I/O 分组新增 `read/write/get_index/set_index`(与 `from_matrix/to_matrix` 同组,D9 保留原名)——**批量 `read/write` 是本体**:span 元素类型与 `precision()` 精确匹配(U2:float↔F32、f16↔F16,错配运行期错误、类型非法编译期 static_assert),GPU `read` 隐含 flush+同步(走 `to_matrix` 同路)、`write` 覆盖既有存储不替换对象(宿主直写 / GPU 走 `copy_from` drain);**索引级是语法糖**(宿主直读写,GPU 每次一整轮 staging,不承诺热循环性能)。测试 f32 填充/恢复路径迁 `write`(f16 转换填充保留 `copy_from`——其语义就是 f32 Matrix→f16 的转换入口)。顺带修复稳定性探针漏 `backend.initialize()` 的 GPU 崩溃(16 §7-2 根因)并补测:dev0/dev2/dev4 进程内+跨进程 loss/hash 逐字节一致。验收:build 122/122 + ctest 19/19 + `--io-roundtrip` CPU/GPU 全过 + CPU 字节锚不变 + scan 双 hash 不变(详见 17 §5 M3 行与 `docs/history.md`)。 +- **Matrix 降级收口(统一总纲 M4,2026-09-30)**:铁律 #12 新立——**L2+(Layer/Loss/Optimizer/Model)头文件零 `Matrix`、零 `from_matrix/to_matrix/copy_from`**;层自算的辅助数据(RoPE cos/sin、正弦位置编码、ALiBi 斜率/偏置、文档掩码与边界、位置/词元索引、卷积置换与广播索引、patch 提取与散射、labels/mask 打包…共 45 处 I/O + 32 处类型)全部迁 span 宿主桥 `detail::upload_span/download_span/download_vector`(内部 = M3 批量 `write/read`,f16 目标按 RHE 同口径)或 `create_tensor`+`InitSpec`/`zero`。**审计**:`bench/doc_inventory.ps1` 新增第 [4] 节分层审计(注释剥离后匹配,`L2-VIOLATIONS` 验收口径 0,宿主桥用量只披露);U5 落点裁定 = 扩展 doc_inventory。**字节零变化**:CPU `--steps 20` hash=`6f8849f14da23110`、GPU dev2 `8ef51b2927253c50` 均与迁移前逐位一致,scan 双 hash 不变,ctest 19/19(详见 17 §3 D11/§5 M4 行与 `docs/history.md`)。 - **内存契约统一(统一总纲 M5,2026-09-30)**:`pool_stats()` 不再有"CPU 返回空串"的空档——无池引擎走基类默认实现,返回宿主直配账本 **`direct{ blocks=… live_bytes=… peak_bytes=… total_blocks=… total_bytes=… }`**(账本 `nn::host_alloc_ledger()` 在 `algebra_matrix.hpp` 的 `MatrixT` 分配/释放三出口记账,relaxed 原子、只在分配路径);GPU 照旧 `persist{…} transient{…} pending=…`。**两引擎同一动词、都非空**,训练日志一套代码读两个引擎;`release_idle_pool_blocks()` 对 CPU 是**语义成立的 no-op**(直配无整块可归还,已写进头注释与 04 号文档)。验收:CPU/GPU `NN_MEM_STATS=1 text_train` 实测两引擎都非空且随 model-built/optimizer-created 递增 + 四件套(build/ctest/字节锚/scan hash)全过(详见 17 §4.5/§5 M5 行与 `docs/history.md`)。 -- **跨设备拉取 import(统一总纲 M6 段 B + 15 P3,2026-09-30)**:`ComputeEngine::import(src[, P])` 公共 NVI(`bind_check_` + `stamp_`)+ 新虚 `import_impl`(**virtual 方法 48 → 49,`doc_inventory` 口径;`grep -c "\bvirtual\b"` 原始 51 处**)。语义 = 同设备同精度零拷贝别名 / 同设备异精度引擎内 cast / 跨设备经宿主中转(基类默认内含同设备快路);`GpuEngine::import_impl` 保留原 `ensure_gpu` 的宿主直传快路径(省一次宿主拷贝),并修掉它硬取 `cpu_matrix()`(F32 槽)对 f16 源取空指针的隐患。**16 §2 的 43 处 `ensure_gpu` 调用点已全部改名 `import`**(`ensure_gpu` 符号清零,命名即防线),返回新句柄、不改写 `src`(就地重绑定须调用点显式写)。**同批 M6 段 A**:15 D6-1(`load_model` 白做上传与 PCIe 往返收敛为 `copy_from` 一行,save→load→save SHA256 逐字节一致)、D6-3(`matmul_with_bias_impl` 默认宿主兜底改纯虚)、D7(`Model::set_engine` 死码删除)、D8(offload 占位改 `create_tensor`),并把 `NN_BIND_DEBUG=1 ctest` 打通成**可执行门禁**(20/20,含修两处库内中转漏网 + `bind_error_` 同步打 stderr)。验收:ctest 20/20(带与不带 NN_BIND_DEBUG 各一轮)、CPU 锚 `6f8849f14da23110` 与 **GPU dev2 `8ef51b2927253c50`** 逐位不变、scan 双 hash 不变、`--io-roundtrip` CPU/GPU 全过、L2 审计 0(详见 17 §5 M6 行/§8 交接与 `docs/history.md`)。 -- **Layer 删每调用 engine 形参(统一总纲 M6 段 C + 15 P2,2026-09-30)**:`Layer::forward/backward/zero_grad/forward_recompute` **不再收 engine 形参**——engine 在 `init(engine)` 时绑定到层内 `engine_`(`init` 改 NVI:公共入口绑定后转 `init_impl`,17 处层 override 随之改名);方法体用函数首行 `ComputeEngine& engine = engine_ref();` 局部引用(**DSL 入口保留形参**,15 §3.3),54 处定义 + 约 200 处调用点编译器穷尽驱动改写。**新不变量:复合层 `init_impl` 必须 init 全部子层**(含 ReLU/GeLU/SwiGLU/Softmax/`pos_encoding_` 这类无参量子层——本轮补齐 5 处;漏了会 fail-fast 并打印调用点 file:line)。`engine_ref()` 带 `source_location` 诊断;`scan_exprs`/测试里"构造后直接 forward"的层补 `init(engine)`(15 处)。**保留形参**(记录在 17 §8):`Loss::forward/forward_sparse`(Loss 无天然绑定时机、测试里跨 CPU/GPU 复用,改法须先裁定构造签名)、DSL 入口、`PositionEncoder`/`RotaryEmbedding` 等辅助对象与层内私有 helper。验收:ctest 20/20(带与不带 `NN_BIND_DEBUG=1` 各一轮)、CPU 锚 `6f8849f14da23110` 与 GPU dev2 锚 `8ef51b2927253c50` 逐位不变、`--init-hash` 六模型与 M2 锚全同、scan 双 hash 不变、`--io-roundtrip` CPU/GPU 全过、L2 审计 0、layer_bench 正/逆序配对 A/B 无系统性回退(详见 17 §5 M6 行/§8 与 `docs/history.md`)。 +- **跨设备拉取 import(统一总纲 M6 段 B + 15 P3,2026-09-30)**:`ComputeEngine::import(src[, P])` 公共 NVI(`bind_check_` + `stamp_`)+ 新虚 `import_impl`(**virtual 方法 48 → 49,`doc_inventory` 口径;`grep -c "\bvirtual\b"` 原始 51 处**)。语义 = 同设备同精度零拷贝别名 / 同设备异精度引擎内 cast / 跨设备经宿主中转(基类默认内含同设备快路);`GpuEngine::import_impl` 保留原 `ensure_gpu` 的宿主直传快路径(省一次宿主拷贝),并修掉它硬取 `cpu_matrix()`(F32 槽)对 f16 源取空指针的隐患。**16 §2 的 43 处 `ensure_gpu` 调用点已全部改名 `import`**(`ensure_gpu` 符号清零,命名即防线),返回新句柄、不改写 `src`(就地重绑定须调用点显式写)。**同批 M6 段 A**:15 D6-1(`load_model` 白做上传与 PCIe 往返收敛为 `copy_from` 一行,save→load→save SHA256 逐字节一致)、D6-3(`matmul_with_bias_impl` 默认宿主兜底改纯虚)、D7(`Model::set_engine` 死码删除)、D8(offload 占位改 `create_tensor`),并把 `NN_BIND_DEBUG=1 ctest` 打通成**可执行门禁**(19/19,含修两处库内中转漏网 + `bind_error_` 同步打 stderr)。验收:ctest 19/19(带与不带 NN_BIND_DEBUG 各一轮)、CPU 锚 `6f8849f14da23110` 与 **GPU dev2 `8ef51b2927253c50`** 逐位不变、scan 双 hash 不变、`--io-roundtrip` CPU/GPU 全过、L2 审计 0(详见 17 §5 M6 行/§8 交接与 `docs/history.md`)。 +- **Layer 删每调用 engine 形参(统一总纲 M6 段 C + 15 P2,2026-09-30)**:`Layer::forward/backward/zero_grad/forward_recompute` **不再收 engine 形参**——engine 在 `init(engine)` 时绑定到层内 `engine_`(`init` 改 NVI:公共入口绑定后转 `init_impl`,17 处层 override 随之改名);方法体用函数首行 `ComputeEngine& engine = engine_ref();` 局部引用(**DSL 入口保留形参**,15 §3.3),54 处定义 + 约 200 处调用点编译器穷尽驱动改写。**新不变量:复合层 `init_impl` 必须 init 全部子层**(含 ReLU/GeLU/SwiGLU/Softmax/`pos_encoding_` 这类无参量子层——本轮补齐 5 处;漏了会 fail-fast 并打印调用点 file:line)。`engine_ref()` 带 `source_location` 诊断;`scan_exprs`/测试里"构造后直接 forward"的层补 `init(engine)`(15 处)。**保留形参**(记录在 17 §8):`Loss::forward/forward_sparse`(Loss 无天然绑定时机、测试里跨 CPU/GPU 复用,改法须先裁定构造签名)、DSL 入口、`PositionEncoder`/`RotaryEmbedding` 等辅助对象与层内私有 helper。验收:ctest 19/19(带与不带 `NN_BIND_DEBUG=1` 各一轮)、CPU 锚 `6f8849f14da23110` 与 GPU dev2 锚 `8ef51b2927253c50` 逐位不变、`--init-hash` 六模型与 M2 锚全同、scan 双 hash 不变、`--io-roundtrip` CPU/GPU 全过、L2 审计 0、layer_bench 正/逆序配对 A/B 无系统性回退(详见 17 §5 M6 行/§8 与 `docs/history.md`)。 - **训练稳定性**:防 NaN 跳步。 - **训练显存开关(GPT 与 RAPT 同档)**:梯度检查点 `set_checkpoint_every`、activation offload `set_activation_offload`、文档掩码 `set_doc_ids`、batch flush 粒度 `set_flush_interval`;检查点与 offload **可混合**(checkpoint 块重算、其余块 offload)。引擎侧 API 为 `create_offload_buffer`/`offload_save`/`offload_restore`(`set_offload_enabled` 不存在)。 - **Vulkan 多设备选择**:`--gpu` 参数与 `NN_VULKAN_DEVICE` 环境变量(`cli/cli_gpu_option.hpp`、`backend/compute_vk_device.hpp`)。 @@ -317,9 +318,9 @@ optimizer.step(); 1. **运行时值禁进表达式常量池**(进 `expr_spec_key` 会破坏闭合世界):`scale` 折进 Q(forward `scale_inplace` + backward 补乘)、`inv_num_valid` 后置 `scale_inplace`。 2. **BatchCol 视图要求 `(1, BH*seq)`**(doc_ids 按 (b,h) 块重复),`(1, batch*seq)` 会越界。 3. **RowGather 主输入行数≠网格行数**(loss_vec 在 (1,N) 读 (C,N) logits),校验只查 cols。 -4. `gen_fused` `emit_spec` 的 ±inf 常量必须用 `numeric_limits`。 +4. **key 只描述结构**(= 自登记锚点的前提;契约回归测试 `src/expr_fused_key_test.cpp`,并入 `expr_cpu_test`):常量池**值**、`MatmulSpec.transA/transB`、分组归约 **R** 三者**不进** `expr_spec_key`——值走 push constant `c`,转置走 `mm_trans`(shader 内两条加载路径 + uniform 分支),R 走视图参数 `vp`(`gr_r()` 运行期循环)。反之**属于结构、必须进 key** 的:`AttnScoreMask::mask_kind()`、`FoldSpec.tri_skip`。生成阶段 `emit_spec` 的 ±inf 常量必须用 `numeric_limits`。 5. **matmul + 列归约可用**:`generate_glsl_reduce` 列分支按元素分解 batch(`batch = row/m_per`,遍历全部 `rows = batch*m_per` 行);覆盖 = `expr_cpu_test::col_max(matmul)`(batch=2 独立标量参考)+ `expr_gpu_test` col 对拍(广播/向量/batch=2)。 -6. **IR 扩展点**:MatmulSpec.batch(不进 key,dispatch z)、Row/Col/Batch 操作数(6/7/8)、RowGather(9)/BatchMod(10)/BatchCol(11);注意力 forward = 单 fold kernel(`FoldSpec`,掩码 `AttnScoreMask::mask_kind()` × 位置偏置 `PositionEncoder::has_score_bias()` 两个正交入参,构造期定型),bwd = 掩码 → 位置偏置(独立一步)→ softmax 的 R/X 表达式 + 3 个 `batched_matmul`;CE 稠密 `denom = col_sum(exp(logits-cb(col_max)))`,稀疏 grad/loss_vec 用 Row+RowGather。 +6. **IR 扩展点**:MatmulSpec.batch(不进 key,dispatch z)、MatmulSpec.transA/transB(不进 key,PC `mm_trans`)、分组归约 R(不进 key,PC `vp`)、Row/Col/Batch 操作数(6/7/8)、RowGather(9)/BatchMod(10)/BatchCol(11);注意力 forward = 单 fold kernel(`FoldSpec`,掩码 `AttnScoreMask::mask_kind()` × 位置偏置 `PositionEncoder::has_score_bias()` 两个正交入参,构造期定型),bwd = 掩码 → 位置偏置(独立一步)→ softmax 的 R/X 表达式 + 3 个 `batched_matmul`;CE 稠密 `denom = col_sum(exp(logits-cb(col_max)))`,稀疏 grad/loss_vec 用 Row+RowGather。 > ⚠ **`row()` 的批内语义依赖 `MatmulSpec.batch`**:`dsl::row()` 在 IR 里是"批内行号",其分解来自**同一 ExprSpec 里的 matmul 段**。把原本融在 matmul 表达式里的项(如 ALiBi 的 `col − row`)拆成**独立一步**后,新 spec 没有 matmul → batch 退化为 1 → `row()` 变**全局行号** → 静默错值(alibi 实测 |偏置| 达 27,理论界 ≈7)。 > 独立成步的表达式若需要"批内位置",请用 **`(rows,1)` 行表 + `dsl::row_broadcast`**(按全局行号直读,与网格分解无关)——`AlibiPositionEncoder::apply_score_bias` 即此写法。凡是含占位 `eval()` 的节点(`RowIdxLeaf`/`ColIdxLeaf`/`RowGatherRef`/`BatchModRef`/`BatchColRef`)真实语义只由 IR 解释器提供,`dsl::compute_into` 的 `cpu_preparable_v` 白名单不会放它们进模板路径。 diff --git a/docs/development/13-refactor-backlog.md b/docs/development/13-refactor-backlog.md index 762a795c..9e502b50 100644 --- a/docs/development/13-refactor-backlog.md +++ b/docs/development/13-refactor-backlog.md @@ -1,322 +1,16 @@ # 重构与性能机会清单(2026-09-25 审查) -> 来源:v1.4.2 全量代码审查(issue 跟进轮)。**本文档只记录方案与前提,不实施**。 -> 重新立项时先读本文件 + 对应源码现状。 -> 被核对为误报/已修复的审查项、已执行的代码缩减轮等历史记录见 -> `docs/history.md`(含原 §6 对照表与原 §9 执行表)。 - -## 1. 线程池归约策略统一 - -**现状**:`parallel_transform_reduce` 两个重载是确定性分段(边界只由 `total` 决定, -见 `core_threadpool.hpp::reduce_chunk_count`),1-worker / 8-worker / 串行参考逐字节 -一致;性能 6.6×(9M 元素 1.32ms vs 8.7ms)。 - -**遗留重构机会**: -- `parallel_transform`(elementwise 版)与 `parallel_for_*` 仍用 `chunk_count()` - (依赖 `workers_.size()`)。逐元素操作结果与分块无关,**无确定性问题**,但 - 分块策略两套并存,概念上有统一空间。 -- `row_reduce`/`col_reduce`(algebra_matrix)各有独立的行块并行实现 + 与 - `std::execution` fallback 并存。若要"全库唯一归约语义",可复用 - `reduce_chunk_count` 的边界公式做行块划分(行内顺序不动,天然逐字节一致)。 -- **注意**:PyTorch ATen::parallel_reduce 反例(部分和按 `results[tid]` 存放、 - 边界依赖 `get_num_threads()`)在头注释有记录,重设计时别走回去。 - -## 2. 超大 header 拆分(编译时间) - -现状行数(2026-09-25):`compute_vk_backend.hpp` ≈4700 行、`compute_cpu_engine.hpp` -≈2543 行、`expr_glsl_gen.hpp` ≈2310 行。单文件多职责(backend 混 fence/staging/ -dispatch/算子)。项目几乎全 header-only,增量编译是主要开发成本。 - -**方向**:backend 拆 fence 管理 / staging ring / dispatch / 算子为独立 TU -(`compute_staging_ring.hpp` 已先行拆出,证明可行)。**前提**:先补一组编译期/ -链接期冒烟测试,拆分期间保证 ODR 与 include 次序不敏感。 - -## 3. text_train.cpp 模块化(1860 行) - -CLI 解析、训练循环、device-lost 恢复、checkpoint 逻辑混杂。 -**方向**:抽 `train_loop` / `recovery` 模块;退出码语义修正 -(checkpoint 保存失败应 `exit(1)` 而非成功退出)。与 TDR 恢复路径纠缠的 -`exit(0)` 类问题(审查项 P1-33,核对结论见 `docs/history.md`)就是这种纠缠的产物。 - -## 4. gui.py 五 Tab 声明式字段表(1636 行) - -五个 Tab 的 `collect_args` 全是 `_int/_float/_str(entry, key)` 手工罗列 -(:1471-1500)。**方向**:dataclass + 字段元数据 → 自动生成 UI 行 + 收集函数, -一处改动全局生效,消除漏参风险。 - -## 5. 性能机会(未实施) - -### 5.1 F16C 半精度转换快路径 -`precision.hpp::float_to_half_bits` 是纯软件位操作;已有 x86 pause 原语先例 -(`NN_CPU_PAUSE`)。**方向**:`_cvtss_sh`(F16C)快路径 + 批量转换, -`from_matrix/to_matrix/cast` 的全量标量循环可提到 8-wide。 -**前提**:F16C 需 `target = f16c` 或运行时 CPUID 探测——项目现用 `-march=native` -(NN_ENABLE_NATIVE=ON 默认),需处理 ON/OFF 两态与 MSVC `/arch:` 差异; -改前必须跑 `f16_cpu_probe` 全量对拍(`float_to_half_bits` 的次正规移位 UB 教训, -见 docs/development/05 §12.12)。 - -### 5.2 CPU 引擎 f32→f16→f32 冗余双趟 -`compute_cpu_engine.hpp` to_matrix 的 "tensor=f32, 请求 P=F16" 分支做 -f32→f16→f32 双趟 cast 再返回 f32(量化模拟语义)。热路径上多一次全量转换。 -**方向**:引擎层缓存该组合,或 API 层显式区分"量化模拟"与"直接返回"。 -**注意**:这是量化模拟语义,直接删除会改变行为——需先确认无调用方依赖。 - -### 5.3 GUI 绘制节流 -- `gui.py` 画布 `_on_draw_move`(:1127)每次 motion 全 buffer 降采样 + 预览重绘, - 快速拖动卡顿。**方向**:`after(16)` 按帧节流(图表已有 `_RENDER_MS=250` 节流 - 先例,:213/:383)。 -- `ChartWidget._render_data` 定时全量重绘不查数据更新。**方向**:dirty 标志跳过空帧。 - -## 6. 已核对为误报/已修复的审查项(勿重复处理) - -**本节对照表已移入 `docs/history.md`**(8 项:BPE vocab_size 下溢、 -反序列化长度上限、registry bin 丢 batch、GPT generate 未包 begin_batch、 -forward_sparse 越界 label、restart_on_device_lost exit(0)、checkpoint 缺 Adam m/v、 -ZiPT stored_tokens 门控——含各自核对结论与代码位置)。**重复立项前先读该表。** - -## 7. Checkpoint 优化器状态(P1-34 功能缺口,重新立项前提) - -`.bin` 现只存权重 + extra_state,不存 Adam m/v(momentum)。TDR 重启后动量归零。 -**设计要点**:MODEL_VERSION 升版;可选段(老文件缺段 = 冷启动语义); -`Optimizer` 基类加 `save_state/load_state`;text_train 恢复路径接线。 -**依赖**:先完成 §3(text_train 模块化),否则恢复逻辑继续膨胀。 - -## 8. CI 冒烟(issue #13 审查建议,未实施) - -- MSVC Debug 冒烟 + 权重 MD5 变化断言(防"loss 不动"类静默失败)。 - 现有 `f16_writeback_probe` 已覆盖 f16 写回子集,可扩展为通用训练 N 步 - 参数变化校验。 -- MSVC Debug 大 TU(text_train.cpp)可能需 `/bigobj`(C1128),未复现前不加。 - -## 9. 已执行:代码缩减轮(2026-09-26) - -> 执行记录(全仓"悬空设计"审计 A/B/C 三档逐项、ctest 20 → 19、算子收敛 58 → 49、 -> AST 移除、offload A/B 组处置明细)已移入 `docs/history.md`。 -> 本节保留仍然生效的工程教训: - -1. `dsl::compute` **没有 2 参重载**——签名是 `(engine, expr, rows, cols[, P])`;文档或注释 - 里"`dsl::compute(engine, expr)` 最常用"的说法是错的(AGENTS §7 已按此写)。 -2. **eager → DSL 迁移前必须确认目标表达式结构已被 `scan_exprs` 覆盖**:否则 GPU 闭合世界 - 运行期硬报错。低成本做法:把新表达式写成与已扫描表达式**同构**(如 RAPT 除法的 ε 位置 - 对齐 `forward`),即可零成本复用键。 -3. 改测试公共头后,**聚合器的 `#undef CHECK` 必须同步删除**(pragma once 之下 `#undef` - 会让后续子文件失去宏定义)。 -4. **测试公共头未完全收敛**:`max_abs_diff` 仍有 9 份副本(其中 conv2d 版多形状守卫语义) - ——统一前需逐点确认语义,留待单独立项。 -5. **`gpt_test` 偶发失败待观察**:62 次直跑 1 次失败、日志被覆盖未定位;疑似资源/竞争。 - -## 9b. 已执行:Layer 层简化轮(2026-10-01) - -> 执行记录(注意力变体拆分 + 位置编码多态统一、参数收集 / Pre-Norm 残差反向 / 解码采样 / -> 错误传播四处收敛)的完整叙述与验收数据已移入 `docs/history.md`。本节只保留结论与 -> **仍未做的部分**: - -**已做**(验收 = build 零告警、ctest 20/20 含 `NN_BIND_DEBUG=1`、scan 双 hash 与 -CPU/GPU 字节锚逐位不变、`--init-hash` 六模型全同、`--io-roundtrip` 全过、L2 审计 0): - -- 位置编码:`PositionEncoder` 基类 + `Learned/Sinusoidal/RoPE/ALiBi/None` 子类 - (新文件 `compute_position_encoding.hpp`);`PosEncodingType` 只有两个分发点, - 按注入点分开:`make_embedding_position_encoder`(模型侧持有)与 - `make_attention_position_encoder`(注意力层**自持**)。层间无位置编码对象传递 - (无 `set_position_encoder`),"谁拥有 = 谁负责"。 - 代价:RoPE 的 cos/sin 表随层构建(默认配置 ≈64KB/层、全模型 256KB);层数极多 - 时若要回到共享,可考虑 `shared_ptr` 或模型侧持有 + 构造期传引用。 -- 注意力掩码:`AttnScoreMask` 族**只做掩码**(`PlainScoreMask` / `CausalScoreMask` / - `CausalDocScoreMask`,三个类),配置期由 `AttentionBase::make_score_mask_()` 定型; - `recompute_W_` 的变体判断链与三处 `if (use_rope_)` 全部消失。 -- 掩码 × 位置偏置**正交**:`expr::FoldAttnMask`(5 值融合枚举)拆成 - `expr::AttnMaskKind{Plain,Causal,CausalDoc}` + `bool score_bias`;ALiBi 的偏置从 - 掩码表达式里摘出,由 `PositionEncoder::apply_score_bias()` 在 backward 的 - "掩码之后、softmax 之前"独立叠加。掩码工厂不再查询位置编码。 - **代价:backward 多一个逐元素 kernel;且 ALiBi 的行位置改用 `(rows,1)` 行表 + - `row_broadcast`(不能再用 `row()`——它在没有 matmul 段的新 spec 里会退化成全局行号, - 见 `AGENTS.md` §7 的告警)。`expr_specs.bin`/`fused_registry.hpp` 的 hash 因此变化, - 但 CPU/GPU 字节锚逐位不变。** -- `Layer::collect_refs` / `collect_block_refs_` / `prenorm_residual_forward_` / - `prenorm_residual_backward_` / `sample_next_token_`(`compute_layer_base.hpp`)。 -- `NN_TRY` / `NN_TRY_CHECK`(`core_errors.hpp`):L2 层 494 处错误传播收敛; - 批量改写脚本 `tools/apply_nn_try.ps1 -DryRun` 可复现计数。 - -**仍未做**(重新立项前先读本节): - -- **H4 精度/形状三元组收敛**:`dsl::compute(engine, expr, rows, cols, p_.compute)` 的 - 尾部参数仍在每个调用点重复。可考虑 `Layer` 上的 `eval_like_(engine, expr, shape_ref)` - 便捷入口,但需逐点确认每个调用点用的是 `p_.compute` 还是刻意的其他精度(RAPT 的残差 - 相加就是反例),故不适合机械改写。 -- **H5 掩码/索引构造**:注意力侧已随策略类收敛(`CausalDocScoreMask::prepare` 把原先 - 两份数值相同的宿主缓冲并为一份);其余层(conv 置换、transformer patch 提取等)的 - "CPU 循环 + `upload_span`" 仍未声明式化。 -- **H3 解码循环骨架**:仅采样段合并;三个 `generate()` 的循环体因运行态不同而保留 - (理由见 `docs/history.md` 对应条目)。 -- **未评估**:`bench/doc_inventory.ps1` 的 `host_bridge_uses` 计数口径未随本轮新增文件 - 复核(当前 41,仅披露不判违规)。 - - -## 10. Tensor 绑定引擎 + 存储多态(提案,2026-09-28 设计讨论) - -> 状态:**方案未实施**。选型已定(见 §10.8),重新立项时先读本节 + 源码现状。 -> **详细设计、代码片段与未决点裁定见 `docs/development/15-computeengine-refresh.md`**: -> 其中 D1(PrecisionEngine 下沉删除,**P-1 已实施 2026-09-29**)与 D9(字节基线分层)已裁定,§10.3 第 1 条 -> 按 15 §4.2 补"库内不变量 + 库外测试豁免"脚注。 -> **2026-09-30:15 中未实施的 P2-P6 已由 `docs/development/17-unified-tensor-engine.md`(统一底层架构总纲) -> 吸收改期 M1-M7——后续立项先读 17(本文 §10.8 末条有增补注脚)。** -> 范围(2026-09-28 复审扩充):① 绑定与传参解耦(P1-P4)② 跨设备互传协议 `import`(§10.6) -> ③ 存储多态与访问器(P5-P6,兑现"简化后期开发新后端")。 -> **不采用**:`Tensor` 抽象基类 + `CPUTensor/GPUTensor` 句柄指针化(理由 §10.8); -> `Tensor::host` / `.to(engine)` 两步创建(唯一入口收敛,§10.3 第 1 条); -> `to_matrix` / `from_matrix` / `copy_from` 作为日常 API(降级为宿主中转特例,§10.6 表)。 - -### 10.1 现状与动机(坐标可复现) - -仓库已有"绑定后免传 engine"的先例,本条目是把该模式下沉到 `Tensor`/`Layer`,消除内部不一致: - -| 对象 | 现状 | 坐标 | -|---|---|---| -| `Model` | 持 `observer_ptr engine_`,`forward/backward` **不带** engine,未绑定时 `Result` 报错 | `model_container.hpp:36` / `:185` | -| `Optimizer` | 持 `ComputeEngine& engine_`,`step()` 不带 engine | `compute_optimizer.hpp:46` | -| `Layer` | `init(ComputeEngine&)` 是**现成绑定位**(`Model::add` 调用),但 `forward/backward/zero_grad` 每次现传 | `compute_layer_base.hpp:105` / `:69` / `:80` | -| `Tensor` | 无绑定 → 上层被迫到处传 engine | `compute_tensor.hpp` | - -耦合代价(数字复现方式写在括号内): - -1. **序列化必须从 Model 拿引擎**:`save_model` / `load_model` 需 `model.engine()` 才能下载/写入参数(`model_serialization.hpp:539` / `:639`)——L3 本可只依赖张量自身。 -2. **GPU 原语入口防御性兜底**:`GpuEngine::ensure_gpu()` 有 **43 个调用点**(`compute_gpu_engine.hpp:455-1641`;定义 :1707,注释 :1706 自述"纯 GPU 架构下所有 Tensor 应已是 GPU,此方法为防御性兜底");另有 `copy_from:409-413` 的句柄重绑定 `dst = Tensor::from_matrix(...)`。绑定后"入参不是本引擎的张量"变成构造期不变量。 -3. **每调用现传的规模**:`include/` 头文件中 `ComputeEngine&` **175 处**(含应保留的绑定位与应删除的每调用形参;复现:在 `include/` 下 grep `ComputeEngine&`)。对照:`Result` 全仓头文件 **203 处**——这是"子类化方案"必须指针化的量,本方案**一处不动**。 -4. **存储层耦合(加后端要改容器本体)**:`compute_tensor.hpp` 有 **7 处 `#ifdef NN_HAS_VULKAN`**(行 28/65/99/161/205/245/304),并 include `backend/compute_vk_backend.hpp`(≈4700 行,见本文件 §2)——新后端 = 改 `compute_tensor.hpp` 加 variant 槽 + 加 `#ifdef` + 拖进新后端头。 - -### 10.2 目标用法(选型依据,代码为准) - -> **2026-09-30 勘注(M1 落地后)**:下例两处已与现状不符——① `t.cpu_matrix` 访问器 -> 已私有化(17 §4.1 访问不变量),库外读写一律 `engine.to_matrix/from_matrix`; -> ② `from_host/to_host/upload` 改名方案已由 17 §3 D9 **取消**(保留 `from_matrix/ -> to_matrix` 名称 + I/O 分组)。其余目标用法(创建唯一入口、出生绑定、import、 -> Layer 删形参)仍有效,分别由 17 的 M1/M6 推进。 - -```cpp -// ── 创建:唯一入口 = 引擎方法,engine + precision 出生一次到位,返回值类型 ── -auto t = engine.create_tensor(128, 64, nn::Precision::F16); -static_assert(std::is_same_v); // 仍按值,shared_ptr 存储 -auto u = engine.create_tensor(32, 8); // 无 P 版:默认 F32(现状语义) -auto v = engine.from_host(host_matrix, nn::Precision::F16); // 宿主数据 → 成绑张量(原 from_matrix) -// 未绑定态只有 nn::Tensor{}:空槽,valid()==false,禁止参与计算 -// 不存在 "host-only 张量":宿主侧数据用 Matrix 表示(L1 即宿主表示),要张量时已经在引擎上 - -// ── 查询与宿主动词(不再到处传 engine)── -t.bound(); t.engine(); t.precision(); t.valid(); // valid() = bound() && storage -auto host = t.to_host(nn::Precision::F32); // 下载(原 to_matrix;宿主中转特例,§10.6) -t.upload(file_matrix, file_prec); // 写既有存储(原 copy_from;同为特例) -auto t16 = t.cast(nn::Precision::F16); -t.cpu_matrix(); // 既有访问器不变 - -// ── 跨设备互传:唯一对外入口 = 目标侧 import(协议见 §10.6)── -auto g = gpu.import(t); // 同引擎 = 零拷贝别名 / cast;无 A<->B 快速通道 = 默认经宿主(CPU)中转 -// 旧 ensure_gpu(43 处隐式防御)在 P3 收敛为显式 import - -// ── Layer:init 是绑定位,forward/backward 删 engine 形参 ── -class Linear final : public nn::Layer { - nn::observer_ptr engine_; // init 时绑定 - nn::Tensor w_, b_, grad_w_, grad_b_, input_cache_; // 成员类型不变 -public: - nn::Result init(nn::ComputeEngine& e) override { - engine_ = nn::make_observer(e); - w_ = engine_->from_host(w_cpu, p_.param); // 出生即绑定(原 from_matrix);精度显式(§8.5) - return {}; - } - nn::Result forward(const nn::Tensor& input) override { - if (input.engine() != engine_.get()) // 跨引擎混用 → 硬错误 - return std::unexpected(nn::Error{"forward: tensor bound to another engine"}); - return nn::dsl::compute(*engine_, dsl::matmul(w_, input) + dsl::row_broadcast(b_), - w_.rows(), input.cols(), p_.compute); - } - void clear_cache() override { input_cache_ = nn::Tensor{}; } // 空槽语义不变 -}; - -// ── 序列化:L3 不再依赖 model.engine() ── -for (auto& p_tensor : params) { - auto m = p_tensor.get().to_host(nn::Precision::F32); // 原:engine.to_matrix(p_tensor, F32) - detail::write_matrix_v5(ofs, *m, p_tensor.get().precision()); -} -// load 侧:p_tensor.get().upload(file_matrix, file_prec)(in-place 写既有参数;精度契约由 P4 修复) -``` - -### 10.3 设计要点(八条不变量) - -1. **张量只能由绑定引擎产生(唯一创建入口)**:`engine.create_tensor(rows, cols[, P])`、`engine.from_host(matrix, P)`(原 `from_matrix`)、算子输出;`reshape`/`clone` 保绑定。**不提供 `bind()/unbind()`**,也**不提供 `Tensor::host` / `.to(engine)` 两步创建**——宿主数据用 `Matrix` 表达,需要张量时它已经在某个引擎上。非绑定态仅剩 `nn::Tensor{}`(空槽,`valid()==false`,禁止参与计算)。晚绑定 = "未绑定 → 已绑定 → 换引擎"状态机,正是本条目要消灭的东西。 -2. **值类型不变**:`Tensor` 仍是值句柄(shared_ptr 存储);`Result`(203 处)、`std::span`、`std::vector`、层成员类型**全部不改**。 -3. **生命周期**:用 `observer_ptr`(`core_observer_ptr.hpp`,与 `Model` 同惯例)。**绑定不延长引擎寿命**——契约写明"engine 必须比它创建的 Tensor 活得久";一期不加析构探测,若实际出现悬垂事故再评估 `weak_ptr` 方案(先量化再决策)。 -4. **跨引擎一致性**:双张量操作入口检查 `a.engine() == b.engine()`,不等返回 `Result` 硬错误;P1 阶段先加检查(零行为变化),P3 升级报错语义。设备间数据移动**只走 `import`**(§10.6),不走"把张量喂给别的引擎的算子"。 -5. **精度出生即定、此后不可变**(§8.3 存储精度不变量的显式化):`create_tensor` 无 P 版默认 **F32**(与现状签名一致,不引入 engine 侧 profile 第二真相源);层内显式传 `p_.param`(§8.5)。 -6. **头文件环(P1 唯一结构性前置)**:`compute_engine.hpp` include `compute_tensor.hpp`,故 `compute_tensor.hpp` 只**前向声明** `class ComputeEngine;` 并声明委托动词,`inline` 定义放在 `compute_engine.hpp` 尾部(调用这些动词的 TU 本就 include 它)。 -7. **存储由引擎工厂造(P5 前置,否则循环依赖)**:`TensorStorage` 抽象基类定义在 `compute_tensor.hpp`(对后端 storage 只前向声明);`GpuStorage` 这类**需要 backend 引用**的 storage 只能由对应 `XxxEngine::create_tensor` 内 `make_shared`,`Tensor` 只收 `shared_ptr`——若 storage 自己找 backend,就会 storage → backend → 引擎头 → Tensor 头成环。 -8. **`reshape` 下沉为 virtual**(`reshape_view(new_rows, new_cols)`):GPU 零拷贝共享 buffer、CPU 复制数据(坑 #6 的语义差异)由各 storage 自己实现,差异留在实现内、不外泄到 `Tensor`。 - -### 10.4 与既有精度决策的关系(本条目不改) - -- **§8.5(P 显式)**:`create_tensor(rows, cols, P)` 显式重载保留;**无 P 版默认 = F32**(与现状签名 `P = Precision::F32` 完全一致,不引入 engine 侧 profile 真相源),层内按 `p_.param` 显式传。"profile 收归 engine 作默认"若要做,属下一条的另立项范围。 -- **§9.2 D7(`Layer::p_` 注入链)**:profile 归属**不动**,不引入 engine 作为第二真相源;"profile 收归 engine" 是另一选题,牵动 f16 数值路径(`f16_precision_test` / `f16_writeback_probe` / 已知 `stable=f16` 冻结故障),须单独立项评估。 -- **DSL 入口保留 engine 形参**(`expr_dsl.hpp:1340` `dsl::compute(engine, expr, rows, cols[, P])`):它是输出分配上下文,纯标量表达式无叶子可推导;绑定后 Layer 传 `*engine_` 即可,签名零改动。 - -### 10.5 分期与验收(每期构建全绿;失败即停,不带坏状态继续) - -| 期 | 内容 | 验收 | -|---|---|---| -| **P0** 调研基线(0 改动) | ① 43 处 `ensure_gpu` 按入参来源分类(真防御 vs 承载真实路径)② 175 处 `ComputeEngine&` 打标(绑定位保留 / 每调用可删)③ 同 seed 训练 N 步 loss 序列 + 权重 MD5 基线 ④ `expr_specs.bin` / `fused_registry.hpp` hash 基线 ⑤ `from_matrix`/`to_matrix`/`copy_from` 调用方盘点(P2 改名准备) | 数字入档,供 P1-P6 对照 | -| **P1** 加绑定,不删参数 | `observer_ptr` + `bound()/engine()` + 跨引擎一致性检查(只加不改) | build 全绿 + ctest 全绿(`ctest -N` 实测 20 个用例,含 `cnn_test_gpu`)+ **字节基线逐字节一致** + scan 产物 hash 零 diff(不一致 = 暴露了暗中跨引擎路径,本身就是发现 bug 的手段) | -| **P2** 删形参 + 宿主动词改名 | 按 P0 打标:Layer → Loss → 辅助函数 → CLI;编译器报错驱动,漏不掉;DSL 入口不改。同步改名(§10.6 表):`to_matrix→to_host`、`from_matrix→from_host`、`copy_from→upload`,旧名全仓消失 | 同 P1 + `layer_bench` 无回退(警戒线:`Tensor::cpu_get_ptr` 注释实测 8-70×、`expr_dsl` `CpuViewCache` 1.5-4×) | -| **P3** 收紧语义 | 跨引擎混用升 `Result` 硬错误;43 处 `ensure_gpu` 按 P0 分类改为显式 `import`;序列化改 `p_tensor.get().to_host()`,L3 去 `model.engine()` 依赖 | 同 P1 + GPU 用例结果如实记录(退出码 77 = skip) | -| **P4** 正交轨道,**另案勿混入** | `Matrix` 宿主格式契约修复:`model_serialization.hpp:651-674` 同精度路径丢弃 `from_matrix` 结果(每个参数白做一次全量上传)、跨精度路径 5 次设备操作;`CpuEngine::copy_from`(`compute_cpu_engine.hpp:210`)`dst.cpu_matrix()` 硬编码 F32 槽(f16 dst 在 `NDEBUG` 下空指针;P-1 后由基类 `copy_from` NVI 入口兜底——f16 dst 走 from_matrix(F32)+cast_into,不再触达硬编码槽,原"非全 f32 才包 PrecisionEngine"CLI 兜底已随装饰器删除);`ComputeEngine::matmul_with_bias`(`compute_engine.hpp:327-352`)host 往返兜底为死码(三引擎均 override) | 单独立项、单独验收;绑定解决"谁执行传输",不解决"传输格式契约" | -| **P5** 存储多态(P1-P4 之后独立做) | `compute_tensor.hpp` 引入 `TensorStorage` 抽象 + `CpuStorage`/`GpuStorage`;**storage 由引擎工厂造**(§10.3 第 7 条);`reshape` 下沉 virtual(第 8 条);消 7 处 `#ifdef NN_HAS_VULKAN` 与 backend include | build 全绿 + ctest 全绿 + **字节基线逐字节一致** + scan 产物 hash 零 diff(改的是容器内部,靠字基线验收,不靠编译器) | -| **P6** 访问器(纯加法) | `as_cpu()/as_gpu()` 检查式 downcast 返回子类引用(非虚,句柄仍值类型) | 纯加法:ctest 全绿即可 | - -### 10.6 跨设备互传协议(`import` = 唯一对外入口) - -**事实前提**:现有数据流全程单引擎驻留(AGENTS §4.2:`Matrix → from_matrix → 设备上算 → evaluate 时 to_host`),生产路径**没有**跨设备互传调用方;今天的"互传"只有 `GpuEngine::ensure_gpu()` 的 43 处隐式防御。故 **P1 只定义接口 + 同引擎 fast path**,跨设备实现等 P0 分类结果(先量化,不为不存在的路径写代码)。 - -```cpp -class ComputeEngine { - // 唯一互传入口(目标侧拉取:分配与执行都在目标引擎——只有它知道自己的存储怎么造) - // 基类默认实现 = 经宿主(CPU)中转,即"没有 A<->B 快速互转时的兜底": - // auto m = src.to_host(src.precision()); // 源下载,只搬原始字节 - // auto m2 = cast_on_host(m, P); // 精度转换一律在宿主侧做 - // return this->from_host(m2, P); // 目标上传 - // 同引擎 fast path(在默认实现内):同 P = 零拷贝别名,异 P = cast - // 快速通道 = 引擎 override:同设备直拷、将来 peer copy —— 调用方零改动 - [[nodiscard]] virtual Result import(const Tensor& src, Precision P /* 默认 = src 精度 */); -}; -``` - -**`to_matrix` / `from_matrix` / `copy_from` 退出计算路径**,改名收敛为三个**宿主中转特例**(宿主表示就是 `Matrix`,IO/初始化绕不开)。**红线:`forward`/`backward`/算子实现内部不得直接使用宿主中转**——那正是今天 PCIe 往返散落的根源;只允许出现在 IO、init、`import` 回退三类场景: - -| 新名 | 原名 | 允许出现的场景(计算路径禁用) | -|---|---|---| -| `Tensor::to_host(P)` | `to_matrix` | 序列化下载;评估/推理结果回读;`import` 默认回退的源侧 | -| `engine.from_host(m, P)` | `from_matrix` | 层 init 权重上传;序列化上传(产新张量);`import` 默认回退的目标侧 | -| `dst.upload(m[, P])` | `copy_from` | in-place 写既有张量存储(序列化 load / 参数写回;契约见 `cast_into` 注释:不可替换 dst 对象,否则持有同一句柄的缓存静默失联) | - -- 改名放 **P2**:编译器穷尽;旧名全仓消失后,**grep 旧名即可审计"计算路径是否还在绕过 `import` 直接用宿主中转"**(命名即防线)。 -- `ensure_gpu()` 43 处 → P3 逐处改 `import`;`GpuEngine::copy_from:409-413` 的句柄重绑定防御分支随之删除。 -- **精度规则**:转换一律发生在宿主侧,设备只搬原始字节——现状 `GpuEngine::to_matrix`(`compute_gpu_engine.hpp:245-257`:先下载 f16、再在 host 升 f32)正是这么做,本协议把它写成明文;与 P4 协同可消灭 `load_model` 跨精度 5 次往返。 -- GPU→GPU(异设备):当前单后端,走宿主中转;接口留位,将来 peer copy 只改 `import` 内部,调用方零改动。 - -### 10.7 新后端开发 checklist(本条目的兑现目标) - -| 步骤 | 现状要改哪里 | P1-P6 之后 | -|---|---|---| -| 1. 实现引擎接口 | `ComputeEngine` **49 个 virtual 方法**(`bench/doc_inventory.ps1` 口径;2026-09-30 M6 新增 `import_impl` 后由 48 → 49。`grep -c "\bvirtual\b"` 原始 51 处 = 49 方法 + 析构 1 + 注释里的 "Non-Virtual" 1——历史口径"49/50"把注释也算进去了,现已按脚本口径统一) | 不变;传输面收敛为 `import` + 两个宿主特例(**M6 已落地 `import`**) | -| 2. 提供存储类型 | **改 `compute_tensor.hpp`**:加 variant 槽 + `#ifdef`(现有 7 处再加 1)+ include 新后端头 | **`compute_tensor.hpp` 零改动**:只写 `XxxStorage : TensorStorage`(P5) | -| 3. 创建入口 | 各自实现 `create_tensor` | 同左,返回值**出生即绑定**自己(P1) | -| 4. 条件编译 | `NN_HAS_XXX` 渗入容器头 | 只出现在新后端自己的头 + CMake(P5) | -| 5. AOT 代码生成 | — | `expr_emitter.hpp` 注册 emitter(现仅 `glsl`);CPU 式后端走 DSL 模板路径可豁免;表达式结构必须被 `scan_exprs` 覆盖(铁律 7)——**新后端真正的大头,P1-P6 不解决** | -| 6. 测试 | — | 需 Vulkan 类后端的用例退出码 77 = skip,如实记录 | - -### 10.8 选型记录(供 `docs/history.md` 归档引用) - -2026-09-28 两轮讨论结论: - -- **采用:出生绑定**(P1-P3,原"方案 B")。与仓库先例同构(`Model` / `Optimizer` / `Layer::init`);改动形态是**删形参**——漏改处全部编译报错、可被编译器穷尽,而改返回类型需逐处人工判断值/指针语义;值语义零改动。 -- **采用:存储多态 + 访问器**(P5-P6,即原"A 的两项子诉求")。消 `compute_tensor.hpp` 7 处 `#ifdef` 与 backend include,新后端不改容器头;子类式访问人体工学用 `as_cpu()/as_gpu()` 检查式 downcast(非虚)获得。 -- **不采用:`Tensor` 抽象基类 + `CPUTensor/GPUTensor` 句柄指针化**(原"方案 A")。它要解决的 device 互斥,收益与存储多态重叠,却要 `Result` **203 处**指针化 + `span/vector` 元素类型连锁 + `GpuEngine::copy_from:412` 句柄重绑定重写;且虚函数不能模板化,精度维度 variant 依旧要留(收益只覆盖一半),更关键的是**不解决 engine 传参耦合**(`forward(engine,...)` 一个都跑不掉)。 -- **收敛(复审)**:单一创建入口——删除原稿的 `Tensor::host` / `.to(engine)` 两步创建;宿主数据一律用 `Matrix` 表达。 -- **收敛(复审)**:`to_matrix/from_matrix/copy_from` 退出日常 API,仅作宿主中转特例(§10.6 表);互传统一走 `import`,**没有 A<->B 快速互转时默认经宿主(CPU)中转**。 -- **2026-09-30 增补(`17-unified-tensor-engine.md`)**:上文"不采用句柄指针化"**维持**;"直接读写张量"的诉求改由**访问不变量**承接(一切 Tensor 创建/访问/修改经 ComputeEngine、存储私有,17 §4.1)。上一条"to_matrix/from_matrix 退出日常 API"的改名方案由 17 §3 D9 **改为**"保留名称 + I/O 分组 + grep 审计"(Matrix 裁定保留降级为宿主 I/O 载体)。本节整体由 17 吸收为历史引用。 - +> **状态(2026-10-01):本清单已合并归档,本文件只是重定向 stub,不再维护正文。** +> 未完成项已迁入 `docs/development/18-roadmap.md`(未来方向路线图); +> 已完成项与「已裁定不做」项已归档到 `docs/history.md`。 + +| 读者要找什么 | 去哪里 | +|---|---| +| 未完成项与路线图(原 §2 / §3 / §4 / §5.1 / §5.3 / §7 / §8 / §9.5,以及 §10 被吸收后的 M7) | `docs/development/18-roadmap.md` | +| 已完成 / 已被吸收项(原 §10 → 17 号 M1–M6,2026-09-30 全部实施;§9b 已执行部分) | `docs/history.md` 的「13 号清单合并归档(原 docs/development/13-refactor-backlog.md,2026-10-01)」 | +| 2026-10-01 裁定「不做」的项(原 §1 / §5.2 / §9b H3-H5 / §9.4 / §10 选型 / §10.6 改名方案) | 同上(`docs/history.md` 该节逐条给裁定与理由) | +| 本文件原 §6 误报/已修复对照表、原 §9 代码缩减轮执行表、文首「正确性缺陷已在同轮修复」清单 | `docs/history.md`:条目标题均自称"原位置 docs/development/13-refactor-backlog.md",按原章节号检索 | +| 原 §10 提案的详细设计与未决点裁定 | `docs/development/17-unified-tensor-engine.md`(M1–M6 已实施)与 `docs/development/15-computeengine-refresh.md` | + +> 重复立项前先读 `docs/development/18-roadmap.md`(未完成项)与 `docs/history.md` 上述归档节 +> (已做/已否决的结论与理由),避免重复处理。 diff --git a/docs/development/18-roadmap.md b/docs/development/18-roadmap.md new file mode 100644 index 00000000..7ab4c179 --- /dev/null +++ b/docs/development/18-roadmap.md @@ -0,0 +1,296 @@ +# 开发路线图与未完成项裁定(2026-10-01) + +> **本文是"下一步该做什么"的唯一入口**:把 `docs/development/13-refactor-backlog.md` 的未完成条目 +> 与未来 6–12 个月的方向合并为一份可执行的裁定台账。 +> 本轮 13 号文档改为重定向 stub;已完成项、被否决方案与历史取舍统一归档在 `docs/history.md`。 +> 底层契约的已落地部分(统一总纲 M1–M6)见 `docs/development/17-unified-tensor-engine.md`。 +> 写作纪律:只写事实与决定;行号取自 2026-10-01 实测,未能确证的位置只给文件名、不给行号。 + +## 0. 一句话 + +未来一年两条主线:**① 把工程门禁与生态缺口补成真的**(CI 不跑测试、文档自相矛盾、无安装导出、 +推理无采样截断);**② 模型能力按"有真实调用方 / 有对比数据"再立项**,不为可能性写代码。 + +## 1. 定位与用法 + +| 文档 | 角色 | 何时读 | +|---|---|---| +| **本文(18)** | 未来 6–12 个月方向 + 未完成项裁定台账 | 想知道"下一个该做什么"时 | +| `docs/development/13-refactor-backlog.md` | **重定向 stub**:条目原文已合并进本文,历史执行记录在 `docs/history.md` | 只在需要追溯条目来源时 | +| `docs/history.md` | 全仓唯一历史归档:演进记录 / 已修复勘误 / 被否决方案 / 性能 A/B | 查"为什么当初这么改、旧数字是多少" | +| `docs/development/17-unified-tensor-engine.md` | **已落地**的底层总纲(M1–M6 全部实施;仅 M7 未立项) | 改 Tensor/Engine/存储前 | +| `docs/development/15-computeengine-refresh.md`、`16-computeengine-p0-inventory.md` | 17 的前置设计与 P0 盘点,历史引用 | 只做证据追溯 | + +### 1.1 立项三问(三问都答不上来就不立项) + +1. **触发条件是否已满足**(§6 触发式立项清单给了精确判据)? +2. **是否已被否决**(§5 裁定表的 16 条 + §7 非目标)? +3. **验收口径能否复现**(§8 门禁与验收基线)? + +### 1.2 与 17 的关系 + +17 负责"底层已经是什么样"(不变量、契约、已实施分期);本文负责"上层接下来做什么"。 +凡属 17 范围内的未决点(如 M7),本文只做**立项与否的裁定**,实施设计回 17。 + +## 2. 现状画像(2026-10-01 实测) + +| 维度 | 实测 | 复现 | +|---|---|---| +| `include/` | **58 个 `.hpp` / 35637 行**(非空 32594) | `Get-ChildItem include -Recurse -Filter *.hpp` + 逐文件 `Get-Content` 计数 | +| `src/` | **55 个 `.cpp` / 17873 行** | 同上换 `src -Filter *.cpp` | +| `shaders/` | **19 个 `.comp`** | `Get-ChildItem shaders -Recurse -Filter *.comp` | +| `docs/` | **25 篇 `.md`**(本文加入前;含 `history.md` 与 `release-notes/`) | `Get-ChildItem docs -Recurse -Filter *.md` | +| 最大源文件 | `compute_vk_backend.hpp` **5174** 行、`expr_glsl_gen.hpp` **2424**、`compute_cpu_engine.hpp` **2353**、`compute_engine.hpp` **2087**、`src/text_train.cpp` **1746**、`gui.py` **1646**(非空 1456) | 逐文件 `(Get-Content $f).Count` | +| ctest | **19 个测试** = 18 个测试目标(`list(APPEND NN_TEST_TARGETS` 计数)+ `cnn_test_gpu` | `Select-String CMakeLists.txt -Pattern 'list\(APPEND NN_TEST_TARGETS'` | +| 引擎接口 | **49 个 virtual 方法**;`L2-VIOLATIONS: 0`;宿主桥 40 处(仅披露) | `pwsh -File bench/doc_inventory.ps1` | +| 版本 | git tag **v1.5.1**;`CMakeLists.txt` 的 `project(... VERSION 1.0.0)`;`release-notes/` 仅 `v1.5.0.md` | `git describe` + 读 `CMakeLists.txt` | +| CI | 仅 `.github/workflows/cmake-single-platform.yml`:clang++ / Ninja / Release / `NN_ENABLE_NATIVE=OFF`,Linux + Windows;**不开 `NN_ENABLE_TESTS`、不跑 ctest** | 读该 workflow | + +> 口径说明:表中行数均为**总行数**;`gui.py` 另有非空 1456 行。 +> `19` 是当前唯一正确计数——仓库里同时存在"ctest 计数 19"与"ctest 20 比 20"两种表述(§3 工程化第 4 条)。 + +### 2.1 已经稳固的底座(本轮不再动) + +| 底座 | 状态 | 依据 | +|---|---|---| +| 统一 Tensor / ComputeEngine 契约(M1–M6) | 全部实施 2026-09-30 | `docs/development/17-unified-tensor-engine.md` §5 | +| 访问不变量(Tensor 存储私有、一切经引擎) | 铁律 #11,编译期强制 | `AGENTS.md` §5 | +| L2+ 计算路径禁用 `Matrix` | 铁律 #12,审计口径 `L2-VIOLATIONS: 0` | `AGENTS.md` §5;`bench/doc_inventory.ps1` 第 [4] 节 | +| AOT 闭合世界 + 单 fold 注意力 kernel | 现行;掩码 × 位置偏置两正交维度 | `docs/development/02-operator-fusion.md` | +| 位置编码多态统一(按注入点分所有权) | 现行;新增 `compute_position_encoding.hpp` | `docs/history.md`「Layer 层简化轮」条 | +| BPE 保序并行 encode(任意并行度逐字节一致) | 现行 | `AGENTS.md` §12 | +| 声明式初始化 `InitSpec`(初值跨进程确定) | 现行 | `docs/development/17-unified-tensor-engine.md` §4.4 | +| CPU/Vulkan 双后端 + 多设备选择 | 现行 | `AGENTS.md` §2 | + +**含义**:本文的路线图不需要再排"底层重构"——底层已完成;缺口集中在工程门禁与上层生态。 + +## 3. 缺口总表 + +每条 = 一句话现象 + 坐标 + 影响面。**坐标只写在已实测确认的位置。** + +### 3.1 正确性 + +| # | 现象 | 坐标 | 影响面 | +|---|---|---|---| +| C1 | GPU `stable=f16` 训练 loss 回读冻结(权重照常更新) | `docs/development/14-f16-stable-gpu-loss-frozen.md` | GPU f16 训练的 loss 曲线不可观测;无对应 ctest 用例 | +| C2 | `optimizer=f16` 无 loss scaling 即更新爆炸、`stable=f16` 约 200 步 NaN | `docs/development/05-mixed-precision.md` §12.5;`src/text_train.cpp` 解析处只赋值不校验 | 用户可组合出必崩配置,且无任何启动期提示 | +| C3 | `NormType::BatchNorm` **静默回落**到 LayerNorm | `compute_layer_mlp.hpp:782`(`make_norm_layer`);`README.md:248` 声称支持;`gui.py:60` 把 `batchnorm` 列为可选项;`model_spec.hpp:43` 保留枚举 | 模型规格写 batchnorm、实际跑 layernorm,无告警、无日志 | +| C4 | checkpoint(`.bin`)不存 Adam m/v | `model_serialization.hpp`(`MODEL_VERSION = 5`,见该文件版本常量注释);`compute_optimizer.hpp` 无 `save_state/load_state` | TDR 重启 / `--resume` 后动量归零,续训轨迹不连续 | +| C5 | `.bin` 无整文件校验和与尾部完整性标记 | `model_serialization.hpp` 版本常量附近的注释(明示"若引入需升 MODEL_VERSION") | 截断/损坏的模型文件无法与合法文件区分 | +| C6 | `gpt_test` 偶发失败未定位 | 13 原条目 §9.5(62 次直跑 1 次失败、日志被覆盖) | CI 化之后会成为随机红 | +| C7 | RAPT / CNN 未接 f16:`Conv2D` 权重/偏置/梯度硬编码 `Precision::F32` | `compute_layer_conv.hpp:148`(权重)、`:151`(偏置)、`:154`/`:155`(权重/偏置梯度);RAPT 头内 32 处 `dsl::compute*` 调用未接精度实参 | f16 只覆盖 MLP/GPT 路径,CNN/RAPT 用户得不到显存收益 | +| C8 | `host_bridge_uses` 计数与文档不一致(实测 40,13/17 记 41) | `pwsh -File bench/doc_inventory.ps1` 第 [4] 节 | 审计数字失去可比性(仅披露项,不是违规) | + +### 3.2 工程化 + +| # | 现象 | 坐标 | 影响面 | +|---|---|---|---| +| E1 | CI 是唯一自动门禁,却**只编译生产目标、不编译测试、不跑 ctest** | `.github/workflows/cmake-single-platform.yml`(configure 无 `NN_ENABLE_TESTS`,无 ctest 步骤) | 所有测试结论只存在于本地;回归可在无人察觉时合入 | +| E2 | 无 `install()` / export,无 `CMakePresets.json` | `CMakeLists.txt` 实测 `install(` 0 处;根目录无 `CMakePresets.json` | 库无法被下游 `find_package` 消费,构建参数靠口口相传 | +| E3 | `CMakeLists.txt` 版本 1.0.0 与 git tag v1.5.1 不一致,且缺 v1.5.1 release note | `CMakeLists.txt` 的 `project(...)` 行;`docs/release-notes/` | 版本可读性缺失;分发产物无对应变更说明 | +| E4 | 文档内 ctest 计数自相矛盾:同一份 `AGENTS.md` 内既有 19 也有 20 比 20 的表述 | `AGENTS.md` §12 | 读者无法判断哪个是当前事实(实测 19) | +| E5 | `src/text_train.cpp` 1746 行、34 处 `std::exit` | 该文件(CLI 解析 + 训练循环 + TDR 恢复 + checkpoint 混杂) | 任一改动牵动全文件;恢复逻辑是 C4 的前置阻塞 | +| E6 | 单文件多职责:`compute_vk_backend.hpp` 5174 行 | 该文件(fence / staging / dispatch / 算子混装) | header-only 项目里增量编译是主要开发成本 | +| E7 | `README.md` 与实际不符:死链 `docs/development/07-zipt-algorithm.md`、mojibake、声称链式 `add()`、文档索引缺 13–17 | `README.md:21`、`:35`、`:63`、`:269`、索引表(`:23` 起) | 新用户第一入口即误导;`add(args...)` 是返回 `Result` 的模板方法,**无链式 API** | +| E8 | `bench/doc_align_audit.ps1` 的 [A] 只检查**反引号里的文件名**,markdown 链接目标不查 | 该脚本 [A] 段正则 | README 的 `07-zipt-algorithm.md` 死链因此漏检(需扩审计或在文档侧修) | + +### 3.3 生态与可用性 + +| # | 现象 | 坐标 | 影响面 | +|---|---|---|---| +| X1 | 推理无 top-k / top-p 采样截断 | `src/text_infer.cpp` 只有 `--temperature` 与 `--interactive`;`compute_layer_gpt.hpp:622` 的采样入口只吃 temperature | 长文本生成质量不可控;无法做"多轮对话闭环"演示 | +| X2 | 无 Dataset / DataLoader 抽象,文本训练数据加载不成体系 | `src/text_train.cpp` 内联数据路径;`train_pkg.py` / `cli_controllers.py` 各自处理 | 新数据集接入成本高,流式大语料不可行 | +| X3 | 无 Python / C 绑定 | 仓库仅有 `gui.py` 这一 Python 入口(subprocess 调 CLI) | 生态集成只能走命令行 + 文件 | +| X4 | `gui.py` 六个 Tab、7 处手写 `collect_args` | `gui.py`(`def collect_args` 实测 7 处) | 加 CLI 选项要手改多处,漏参风险(13 §4 的"五个 Tab / 1471-1500"坐标已过期) | +| X5 | GUI 画布拖动全量降采样重绘 | `gui.py:1135`(`_on_draw_move`);已有节流先例 `gui.py:216`(`_RENDER_MS = 250`) | 快速拖动卡顿 | + +### 3.4 模型能力 + +| # | 现象 | 坐标 | 影响面 | +|---|---|---|---| +| M1 | 优化器共 **5 个**:SGD / SGDWithMomentum / Adam / **AdamW** / Muon;其中**直接派生自 `Optimizer` 的是 4 个**(SGD/SGDWithMomentum/Adam/Muon),`AdamW` 派生自 `Adam` | `compute_optimizer.hpp:200`(SGD)、`:236`(SGDWithMomentum)、`:294`(Adam)、`:434`(`AdamW : public Adam`)、`:628`(Muon);工厂注册名 `:723`、创建分支 `:745` | 缺无 m/v 状态的低显存优化器,模型规模受优化器状态限制(P2-2) | +| M2 | LRLA 仅存在于文档对比表,未实现 | `docs/development/06-rapt-algorithm.md` §LRLA | 线性注意力家族停在本项目基线 RLA-2/RAPT | +| M3 | IR-C 跨表达式图融合不采用、S6 自动窗口不接线 | `docs/development/03-ir-optimization.md:134`;`docs/development/02-operator-fusion.md` §跨 kernel 自动融合 | 每个表达式仍各 dispatch 一次;能融的必须写进单个 `dsl::compute` | +| M4 | M7 存储多态未立项 | `docs/development/17-unified-tensor-engine.md` §5 M7 行 | 新增第三后端仍需改 `compute_tensor.hpp` | +| M5 | ZiPT / AttnZip 已整体移除,代码保留在 `legacy/zipt` 分支 | `docs/history.md`「ZiPT 移除」条 | 记忆压缩方向暂缺实现 | + +## 4. 路线图分期 + +优先级判据:**P0 = 低成本高价值且不修就有实际风险**;P1 = 1–2 月内的能力与健壮性; +P2 = 3–6 月的结构优化;P3 = 择机(trigger 未到就不排期)。 +每项固定四栏:目标 / 验收口径 / 成本量级 / 依赖。 + +### 4.0 排期总览 + +| 期 | 项数 | 项目 | 共同前置 | +|---|---|---|---| +| **P0** | 5 | CI 真跑测试、文档一致性回正、BatchNorm 禁止静默回落、CMake install/export + presets、推理采样 top-k/top-p | 无(P0-1 是所有人的前置) | +| **P1** | 5 | `text_train` 模块化、checkpoint 优化器状态、危险精度组合 fail-fast、`gpt_test` flaky、CI 冒烟扩 MSVC Debug | P0-1 | +| **P2** | 8 | `.bin` 校验和、Adafactor、RAPT/CNN f16、Dataset/DataLoader、Python/C 绑定、GUI 两项、header 拆分、F16C | P0-1;P2-7 另需 P1-5;P2-6 另需 P0-5 | +| **P3** | 8 | 由 §6 触发条件启动(含 CUDA、分布式) | 各自触发条件 | + +**串行约束(不可并行推进的组合)**: + +1. **P1-1 → P1-2**:checkpoint 优化器状态依赖 `text_train` 先模块化,否则恢复路径继续膨胀。 +2. **P0-1 → P0-2**:文档改动需要 CI 才能被自动验证。 +3. **P0-1 + P1-5 → P2-7**:header 拆分前必须先有编译/链接期冒烟,否则无回归网。 +4. **P0-5 → P2-6**:GUI 字段表在采样开关定型后一次改完。 +5. **P1-2 → P2-1**:`.bin` 校验和与优化器状态段同属版本段改动,合并做省一次 `MODEL_VERSION` 升版。 + +### 4.1 P0(立刻做) + +| 项目 | 目标 | 验收口径 | 成本 | 依赖 | +|---|---|---|---|---| +| **P0-1 CI 真跑构建 + 测试**(最高优先) | workflow 加 `-DNN_ENABLE_TESTS=ON`,并在 Linux 与 Windows 各跑一轮 `ctest`(失败时输出完整失败日志) | GitHub Actions 绿且日志出现 19 个用例;需 Vulkan 的用例按退出码 77 记 skip 而非失败 | 小(改一个 `.yml`,约 10 行) | 无。**先于其它一切**——没有真门禁,后面所有验收都无法自动防回退 | +| **P0-2 文档一致性回正** | ① ctest 计数统一为实测 19;② `CMakeLists.txt` 版本与 tag 对齐(或明确声明"以 tag 为准");③ 补 `docs/release-notes/v1.5.1.md`;④ 修 `README.md` 死链/mojibake/链式 API 声明/索引缺 13–17 | `bench/doc_align_audit.ps1` 的 [A][D][E][F] 可行动项为 0;开发文档中 ctest 计数的旧写法(ASCII 斜杠数字)零命中(历史归档 `docs/history.md` 除外) | 小–中(纯文档 + 一处 `project()` 版本号) | P0-1(否则改动无法被自动验证) | +| **P0-3 BatchNorm 禁止静默回落** | `make_norm_layer` 对 `NormType::BatchNorm` **显式报错**(`Result`/fail-fast)或真正实现 BatchNorm;二选一,不允许静默降级 | 构造 batchnorm 规格必须可观测地失败或给出正确层;`bench/doc_inventory.ps1` 无关项仍 0;新增一条断言测试 | 小(显式报错)–中大(真实现) | 无。**修复本身在本文范围外(本任务只登记路线图)** | +| **P0-4 CMake install/export + CMakePresets** | 头文件库可被 `find_package(neuralnet.cpp)` 消费;`CMakePresets.json` 固化 Release/Debug/无 Vulkan 等常用配置 | 干净目录里执行安装步骤后,最小下游工程能编译并链接出一个 Layer 示例 | 小–中 | P0-1(preset 需进 CI 才不腐化) | +| **P0-5 推理采样 top-k / top-p + 多轮对话闭环** | 采样器支持 top-k / top-p 截断与重复惩罚;`text_infer` 暴露对应开关;GUI GPT 推理 Tab 支持多轮上下文 | 同 prompt 在 top-k / top-p 下输出可复现(同 seed);多轮对话示例可连续交互 N 轮不丢上下文 | 中 | 无。命名冲突提醒:`--topk` 属 `mnist_infer`(`src/mnist_infer.cpp:44`),`--top` 属 `tokenizer_infer`(`src/tokenizer_infer.cpp:46`);`text_infer` 两者都没有,新开关命名不得与它们冲突 | + +> **口径说明(与本文 §4 前置清单的差异)**:任务书前置清单同时把"危险精度组合 fail-fast" +> 列在 P0;按裁定台账(§5 与 §6)该项归 **P1**,理由是它需要先定"报错还是禁用"的语义。 +> 本文以裁定台账为准,不重复排期。 + +### 4.2 P1(1–2 月:能力与健壮性) + +| 项目 | 目标 | 验收口径 | 成本 | 依赖 | +|---|---|---|---|---| +| **P1-1 `text_train` 模块化** | 抽 `train_loop` / `recovery` / `checkpoint` 模块;退出码语义修正(保存失败不得成功退出) | 现有关键路径回归逐字节一致;新增恢复路径单测;`std::exit` 只在 CLI 参数解析层出现 | 中(1746 行文件拆解,34 处 `std::exit`) | 无,但它是 P1-2 的前置 | +| **P1-2 checkpoint 优化器状态** | `.bin` 增加可选优化器状态段(Adam m/v,`MODEL_VERSION` 5 → 6);`Optimizer` 基类加 `save_state/load_state`;恢复路径接线 | 老文件缺段 = 冷启动语义;save→load→save SHA256 逐字节一致;TDR 模拟重启后动量非零且 loss 轨迹连续 | 中 | **P1-1**(否则恢复逻辑继续膨胀) | +| **P1-3 危险精度组合 fail-fast** | 启动期拒绝 `optimizer=f16` / 四字段全 f16 的组合,或明确要求显式确认开关(拟新增,名字待定) | 危险组合直接退出并打印原因;`--f16` 与合法 `--precision-*` 组合不受影响;新增 CLI 断言测试 | 小 | 无。依据 `docs/development/05-mixed-precision.md` §12.5 的实测结论 | +| **P1-4 `gpt_test` flaky 定位** | 复现并消除偶发失败(或定位为已知资源边界并加显式 skip/重试语义) | 连续大样本直跑零失败;定位结论写入 `docs/history.md` | 小–中(复现成本不确定) | P0-1(需要 CI 反复跑才有样本) | +| **P1-5 CI 冒烟扩到 MSVC Debug + 权重变化断言** | Debug 构建冒烟;训练 N 步后断言参数**确实变化**(防"loss 不动"类静默失败) | CI 中 MSVC Debug 目标编译通过;权重变化断言可检出冻结;大 TU 若触发 C1128 再出 `/bigobj` | 中 | P0-1。可复用 `f16_writeback_probe` 的"8 步 Adam 参数必须移动"思路 | + +### 4.3 P2(3–6 月:结构优化) + +| 项目 | 目标 | 验收口径 | 成本 | 依赖 | +|---|---|---|---|---| +| **P2-1 `.bin` 整文件校验和 + 尾部完整性标记** | 版本段之后加校验和;按注释约定升 `MODEL_VERSION` | 截断文件明确报错;合法文件 round-trip SHA256 一致 | 小–中 | P1-2(同一版本段改动,合并做省一次升版) | +| **P2-2 Adafactor 类无 m/v 优化器** | 新增不物化二阶矩的优化器,训练更大模型 | 同任务对比 Adam 的显存下降与收敛曲线;`create_optimizer` 可创建 | 中 | 无 | +| **P2-3 RAPT / CNN f16 接线** | RAPT 的 `dsl::compute*` 调用接精度实参;`Conv2D` 权重/梯度去 `Precision::F32` 硬编码 | f16 下 CNN/RAPT 训练健康收敛;CPU/GPU 字节锚按计划内变化流程重立 | 中 | 无(C2 的 fail-fast 先落地,防用户踩坑) | +| **P2-4 Dataset / DataLoader 抽象 + 流式文本** | 数据集抽象 + 流式加载,不再把语料整段读进内存 | 大文件训练内存曲线平稳;现有 `.nnpkg` 路径回归 | 中–大 | 无 | +| **P2-5 Python / C 绑定** | 提供稳定 C ABI 或 pybind 绑定,供生态集成 | 绑定层跑通训练/推理最小示例;ABI 报错信息可读 | 中–大 | **条件触发**(§6) | +| **P2-6 GUI 声明式字段表 + 绘制节流** | dataclass 元数据自动生成 UI 行与 `collect_args`;画布 motion 按帧节流、dirty 标志跳过空帧 | 现有 Tab 参数零遗漏(`bench/gui_cli_audit.py` 退出码 0);拖动帧率回升 | 小–中(两项同批) | P0-5(采样新开关先定型,避免二次改字段表) | +| **P2-7 超大 header 拆分** | backend 拆 fence / staging / dispatch / 算子为独立 TU;`compute_vk_backend.hpp` 瘦身 | 拆分前后 build 全绿 + ctest 全绿 + CPU/GPU 字节锚逐位一致 | 中–大 | **前置 = 先补编译/链接期冒烟**(P0-1 + P1-5),否则拆分无回归网 | +| **P2-8 F16C 半精度转换快路径** | `_cvtss_sh` 快路径 + 批量转换,替代纯软件位操作 | CPUID 探测 + `NN_ENABLE_NATIVE` ON/OFF 两态;`f16_cpu_probe` 全量对拍逐位一致 | 中 | **条件触发**(§6) | + +### 4.4 P3(择机) + +P3 不排期,只由 §6 的触发条件启动:IR-C 图融合、S6 自动窗口、M7 存储多态、IR-D 第二 emitter、 +ZiPT 恢复、LRLA、CUDA、分布式。 + +## 5. 13 号清单逐条裁定表 + +条目编号沿用 13 号原文的章节号(13 号本身已改为 16 行重定向 stub)。 +其原正文的归档位置:**已执行/已否决记录**在 `docs/history.md`「13 号清单合并归档(2026-10-01)」节; +**仍未决的条目**由下表承接。本节逐条覆盖其**全部 16 条**未决项(含 17 §5 的 M7),一条不漏。 + +| 条目 | 裁定 | 理由与触发条件 | +|---|---|---| +| 13 §1 线程池归约分块统一 | **不做** | 归约已是确定性分段且 1/8-worker/串行逐字节一致;剩余差异(`parallel_transform`/`parallel_for_*` 用 `chunk_count()`,`row_reduce`/`col_reduce` 各自行块)**无正确性问题,只是概念一致性**。历史上 `Tensor::cpu_get_ptr` 的 8-70× 教训说明动热路径风险远大于收益。归档见 `docs/history.md` | +| 13 §2 超大 header 拆分 | **做(P2-7)** | 唯一被确认的日常成本是增量编译。**前置**:先补编译/链接期冒烟(P0-1 + P1-5),否则拆分期间 ODR/include 次序问题无网可抓 | +| 13 §3 `text_train` 模块化 | **做(P1-1)** | `src/text_train.cpp` 1746 行、34 处 `std::exit`,CLI/训练/恢复/checkpoint 混杂;退出码语义有真实缺陷。**它同时是 §7 的前置** | +| 13 §4 gui.py 声明式字段表 | **做(P2-6)** | 实测 6 个 Tab、7 处 `collect_args` 手工罗列,漏参风险真实。与 §5.3 绘制节流同批做(同一文件、同一批次验收) | +| 13 §5.1 F16C 快路径 | **条件触发(P2-8)** | 纯软件位操作在 f16 热点链上是真实开销,但收益未量化。**触发条件(三条同时)**:① CPUID 探测可行;② `NN_ENABLE_NATIVE` ON/OFF 两态都能构建;③ `f16_cpu_probe` 全量对拍逐位一致。历史次正规移位 UB 教训(`docs/development/05-mixed-precision.md` §12.12)要求对拍先行 | +| 13 §5.2 CPU f32→f16→f32 双趟 | **不做** | 该双趟是**量化模拟语义**,删除会改变行为;收益是热路径一次全量转换,属低量级。若将来有真实调用方依赖"直接返回"语义,另立 API 区分而非删除 | +| 13 §5.3 GUI 绘制节流 | **做(P2-6)** | `_on_draw_move` 每次 motion 全量降采样重绘;已有 `_RENDER_MS` 节流先例可复用。低成本、用户可感知 | +| 13 §7 checkpoint 优化器状态 | **做(P1-2)** | TDR 重启动量归零是**真实功能缺陷**(C4)。设计已明确:可选段 + 老文件缺段 = 冷启动 + `MODEL_VERSION` 升版。**依赖 §3 先拆** | +| 13 §8 CI 冒烟 | **做(P1-5)** | MSVC Debug + 权重变化断言可防"loss 不动"类静默失败,与 C1/C3 同族。`/bigobj` 未复现前不加 | +| 13 §9.4 max_abs_diff 统一 | **不做(择机)** | **实测 7 份定义**(`attn_consistency_test.cpp`、`ce_fusion_test.cpp`、`conv2d_gradcheck.cpp`、`fused_gpu_test.cpp`、`gpu_test.cpp`、`maxpool_gradcheck.cpp`、`tensor_expr_test.cpp`),且语义有分叉(conv2d 版多形状守卫返回 `1e9`)。**计数三处不一致**:13 号原文写 9 份、`src/test_common.hpp:19` 注释仍写 9 份、中途交接口径写 8 份,实测为 7 份——P0-2 文档回正时需一并订正。纯测试卫生、无产品风险,机械改写有反例 | +| 13 §9.5 `gpt_test` 偶发失败 | **做(P1-4)** | 偶发红在 CI 化(P0-1)后必然变成噪声;先复现再定性。62 次直跑 1 次失败、日志被覆盖,需先补日志留存 | +| 13 §9b H3 解码循环骨架 | **不做** | 仅采样段已合并,三个 `generate()` 循环体因运行态不同而保留;统一会引入条件分支,收益低。理由已归档 | +| 13 §9b H4 精度/形状三元组收敛 | **不做** | 尾部 `p_.compute` 实参在各点**可能是刻意不同**(RAPT 残差相加即反例),机械改写有语义风险;收益只是少写一个参数 | +| 13 §9b H5 掩码/索引构造 | **不做** | 注意力侧已随策略类收敛;剩余(conv 置换、patch 提取)的"CPU 循环 + upload_span"声明式化无正确性问题,且新原语成本高 | +| 13 §10 句柄指针化 / 存储多态提案 | **不做(已被 17 吸收)** | 13 §10.8 的"不采用句柄指针化"由 17 §3 D2 **维持否决**;其"直接读写"诉求由访问不变量(铁律 #11)承接。存储多态部分改期 M7 | +| 17 §5 **M7** 存储多态 + `as_cpu/as_gpu` | **条件触发(P3)** | 触发条件:**第三后端出现**。在此之前收益只剩"新增后端不改 `compute_tensor.hpp`",无调用方 | + +## 6. 触发式立项清单 + +原则:**触发器不到,不写代码、不排期**。触发条件必须可观测、可复现。 + +| 项 | 精确触发条件 | 立项时先读 | +|---|---|---| +| IR-C 跨表达式图融合 | 出现真实调用方满足**三条同时**:① 同形状长逐元素链;② 中间量不需留 backward;③ 单个 `dsl::compute` 写不下。**且不得复活录制 API**(记录 API 的历史否证见 `docs/history.md`) | `docs/development/03-ir-optimization.md` §5.3 | +| S6 自动窗口(跨 kernel 自动融合) | 同 IR-C 的调用方判据;当前"能融的直接写成单个 `dsl::compute`"已覆盖生产路径 | `docs/development/02-operator-fusion.md` | +| M7 存储多态 | **第三后端出现**(CPU/Vulkan 之外的引擎要落地) | `docs/development/17-unified-tensor-engine.md` §4.2 + §5 M7 | +| IR-D 第二 emitter | 需要 glsl 之外的代码生成后端,且该后端**必须走 AOT 闭合世界**(表达式结构须被 `scan_exprs` 覆盖) | `docs/development/03-ir-optimization.md` 阶段 D;`expr_emitter.hpp` | +| ZiPT 恢复 | ① 压缩向量保证**因果**(压缩器只能看当前位置之前);② 实现质量对齐 RAPT/GPT(`forward_recompute` 梯度检查点 + activation offload + CLI/序列化/测试齐备);③ 有同参对比数据 | `docs/history.md`「ZiPT 移除」条;`legacy/zipt` 分支 | +| LRLA | 先有与 RLA-2 / RAPT 的**同参对比数据**(质量、显存、吞吐),再决定是否实现 | `docs/development/06-rapt-algorithm.md` | +| Adafactor 类优化器 | 已排 **P2-2**;条件 = 需要训练超出 Adam m/v 显存预算的模型 | 本文 §4.3;`compute_optimizer.hpp` | +| CUDA 后端 | **P3**。当前明确不支持(见 §7);触发 = 目标平台出现真实需求 + 有维护者承诺长期维护 + 愿意补 IR-D 第二 emitter 的 AOT 全链 | `AGENTS.md` §2;本文 §7 | +| 分布式训练 | **P3**。触发 = 单机单卡能力被真实任务触顶,且有明确的多机部署场景 | 本文 §7 | + +## 7. 非目标(明确不做) + +| 非目标 | 理由 | +|---|---| +| CUDA 全链 | 项目契约是 CPU / Vulkan 双后端,CLI 无 CUDA 参数;引入 CUDA 等于重走一遍 `import` / 存储多态 / IR-D 全链。仓库中无 CUDA 代码(grep 仅有 Muon 的 URL 注释) | +| 分布式训练 | 数据并行/张量并行需要先有通信层与容错语义;当前连 CI 都不跑测试(E1),先补门禁 | +| HTTP 服务化 | 仓库无任何 HTTP/gRPC 代码;推理入口是 CLI + GUI,服务化的部署/鉴权/并发语义无真实需求方 | +| ONNX 导入导出 | 无 ONNX 依赖与实现;导出需先把动态形状/精度语义固定下来,而 M7 尚未立项 | + +> 列入本表的前提是"当前不做"有证据支持(上表逐条给了证据)。触发条件成熟时经 §6 重新立项。 + +## 8. 门禁与验收口径 + +### 8.1 现有基线(沿用 17 §6) + +| 基线 | 口径 | 当前值 / 复现 | +|---|---|---| +| build | 全绿、零告警(`-Werror`) | `cmake -B build -G Ninja -DNN_ENABLE_TESTS=ON && cmake --build build` | +| ctest | **实测 19**(18 目标 + `cnn_test_gpu`);需 Vulkan 用例退出码 77 = skip | `ctest --test-dir build` | +| L2 分层审计 | `L2-VIOLATIONS: 0`(宿主桥用量只披露) | `pwsh -File bench/doc_inventory.ps1` 第 [4] 节(实测:virtual 49 / host_bridge 40) | +| CPU 字节锚 | `--steps 20` hash = `6f8849f14da23110` | 逐位一致 | +| GPU 字节锚 | dev2 hash = `8ef51b2927253c50` | 逐位一致(GPU 档位按 17 §6 容差规则) | +| scan 双 hash | `expr_specs.bin` = `bdc3a442…a58360`;`fused_registry.hpp` = `7a10412c…` | `tools/scan_exprs.cpp` + `tools/gen_fused.cpp` 产物 | +| 初值锚 | `gpu_stability_probe --init-hash` 各模型跨进程一致 | 见 `AGENTS.md` §12 当前锚 | +| 文档对齐 | `pwsh -File bench/doc_align_audit.ps1`:[A][B][D][E][F] 可行动项为 0 | 2026-10-01 实测:可行动 0 / 历史白名单跳过 11 | +| 行为回归 | `layer_bench` 同窗交错 + 配对 A/B,无系统性回退(噪声 ±6%) | `build/layer_bench` | + +### 8.2 本轮新增门禁(P0 后生效) + +1. **CI 必须执行 ctest**:日志需出现 19 个用例;77 = skip 不算失败(P0-1)。 +2. **开发文档不得出现 ctest 计数的旧写法**:统一写实测 19(P0-2)。 +3. **BatchNorm 行为门禁**:构造 batchnorm 规格必须有可观测结果(报错或正确层),不允许静默降级(P0-3)。 +4. **危险精度组合门禁**:危险组合必须启动期失败(P1-3)。 +5. **审计覆盖补强**:`bench/doc_align_audit.ps1` 的 [A] 需扩展到 markdown 链接目标(E8),或由 P0-2 直接修掉已知死链。 + +### 8.3 验收纪律 + +- **计划内字节变化**:按 17 §6 例外流程**先立新锚**,新锚两次独立运行之间仍须逐字节,并在该期验收中显式记录。 +- **既有故障不混入验收**:C1(GPU `stable=f16` loss 冻结)与 C6(`gpt_test` flaky)在修复前按"既有状态如实记录"处理,不得当成新回归。 +- **裁定不可静默偏离**:任何与本文 §5/§7 不一致的做法,先在本文改裁定并说明理由,再实施。 + +### 8.4 本文档的维护约定 + +1. **状态只写现在式**:做完一项就把该项从 §4 移到 `docs/history.md`,并在 §4 留一行"已完成"指针。 +2. **数字必须带复现命令**:任何计数(行数、测试数、锚值)都要能在命令里复现;不可复现的只写"约"或删。 +3. **行号会腐烂**:行号只作入口线索;引用时优先给文件名 + 符号名,行号在改动后由 `bench/doc_align_audit.ps1` 兜底 + 不覆盖的,由自动化测试兜底(新增断言测试优于文档数字)。 +4. **裁定要留反例**:凡"不做"必须写清触发条件或反例(如 H4 的 RAPT 残差相加),否则后人无法判断能否重新立项。 +5. **与 13 的关系已终结**:13 号是 stub,新增条目只进本文,不再往 13 追加。 + +## 9. 仍然生效的工程教训(原 13 §9 承接) + +13 号清单的"仍然生效的工程教训"中,以下 2 条在 13 改为 stub 后无处落地,按原义承接。 +(其余 3 条已由别处覆盖:`dsl::compute` 无 2 参重载见 `AGENTS.md` §7;`max_abs_diff` 计数已入本文 §5 裁定表; +`gpt_test` 偶发失败已列为本文 P1-4。) + +### 9.1 eager → DSL 迁移前必须确认目标表达式结构已被 `scan_exprs` 覆盖(原 13 §9-2) + +否则 GPU 闭合世界运行期**硬报错**。低成本做法 = 把新表达式写成与已扫描表达式**同构** +(如 RAPT 除法的 ε 位置对齐 `forward`),即可零成本复用键。 +(对应铁律 #7:AOT 闭合世界,未命中硬报错,无 eager、无运行时编译。) + +### 9.2 改测试公共头后,聚合器的 `#undef CHECK` 必须同步删除(原 13 §9-3) + +`pragma once` 之下 `#undef` 会让后续子文件失去宏定义。测试用「`#define main test_xxx` + `#include 子测试.cpp`」 +聚合编译,聚合器里任何 `#undef` 都会影响其后被 include 的所有子测试。 + +## 10. 变更记录 + +| 日期 | 变更 | 依据 | +|---|---|---| +| 2026-10-01 | 本文建立:合并 13 号未完成条目 + 未来方向 + 裁定台账;13 号改重定向 stub | 维护者裁定 | +| 2026-10-01 | 订正实测口径:ctest 19;`max_abs_diff` 7 份(非 9/8);`gui.py` 1646 总行 / 1456 非空 / `collect_args` 7 处;承接原 13 §9 的两条工程教训 | Lead 与归档 owner 双重核验 | + diff --git a/docs/history.md b/docs/history.md index 3825c2eb..6e0f66e2 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1991,3 +1991,90 @@ GPU 后端 / GPU 引擎头文件「历史状态类注释」摘录。整改原则 collect 函数)**仍未做**——本轮只做一致性修正,字段仍是手工罗列; 但三个校验脚本已可作为该重构的回归护栏(改完跑一遍即可验证零漂移)。 - GUI 画布节流(13 §5.3)未动。 + +--- + +# docs/development 13(13 号清单合并归档,2026-10-01) + +## 13 号清单合并归档(原 docs/development/13-refactor-backlog.md,2026-10-01) +- 类型:合并归档 / 演进记录 / 被否决方案 +- 内容: + +**合并说明**:`docs/development/13-refactor-backlog.md` 的未完成项与后续验证口径已迁入 +`docs/development/18-roadmap.md`(未来方向路线图);13 号文件现在只是重定向 stub(指向 18 与本节)。 +本节归档它原有的两类内容——已完成 / 已被吸收的项,与 2026-10-01 裁定"不做"的项。 +13 文首「正确性缺陷已在同轮修复」清单、原 §6 误报对照表、原 §9 代码缩减轮执行表**早已归档在本文件** +(三条既有条目见下),故本节只留指针、不复制正文。 + +**已完成 / 已被吸收(只记坐标,正文见本文件既有条目)**: + +- 13 §10(Tensor 绑定引擎 + 存储多态提案)整体被 `docs/development/17-unified-tensor-engine.md` + 吸收为 M1–M6,并于 2026-09-30 **全部实施**(访问收口 / InitSpec 声明式初始化 / 批量读写 / + Matrix 降级 / 内存池契约统一 / `import` + Layer 删 engine 形参);**M7 未立项**。 + 逐期记录见本文件「统一总纲 M1…M6」诸条。 +- 13 §9(代码缩减轮)执行表见本文件既有条目「13 §9 已执行:代码缩减轮执行表 + (原位置 docs/development/13-refactor-backlog.md §9)」。 +- 13 §9b 已执行部分——位置编码多态统一(`compute_position_encoding.hpp`)、`AttnScoreMask` 族拆分 / + 掩码×位置偏置正交、`NN_TRY`/`NN_TRY_CHECK` 收敛——见本文件既有条目 + 「注意力变体拆分 + 位置编码多态统一(2026-10-01…)」与「Layer 层四处收敛…(2026-10-01…)」。 +- 13 文首「正确性缺陷已在同轮修复」清单、原 §6 误报对照表、原 §9 执行表分别对应本文件既有条目 + 「13 文首…」「13 §6…」「13 §9…」(标题均自称"原位置 docs/development/13-refactor-backlog.md", + 故 13 的 stub 必须继续声明这些归档条目的来源)。 + +**2026-10-01 裁定"不做"的项**: + +1. **13 §1 线程池归约分块统一** —— 裁定:**不做**。`core_threadpool.hpp` 里 `reduce_chunk_count` + (确定性分段:边界只由 total 决定)与 `chunk_count`(依赖 `workers_.size()`)两套并存; + 逐元素操作**结果与分块无关**,不存在确定性问题(铁律 #8 的逐字节一致性只约束归约)。 + 纯概念一致性收益 < 改动并行内核的风险。若将来仍要做:**禁止**走 PyTorch `ATen::parallel_reduce` + 式"部分和按 `results[tid]` 存放、分块边界依赖线程数"的老路——那正是本项目 + `parallel_transform_reduce` 明确修正过的反例。 + +2. **13 §5.2 CPU 引擎 f32→f16→f32 冗余双趟** —— 裁定:**不做**。 + `compute_cpu_engine.hpp` 的 `to_matrix` 分支(tensor=f32、请求 P=F16)做 f32→f16→f32 两趟 cast + 再返回 f32,这是**量化模拟语义**(模拟落 f16 后的回读),直接删除会改变行为;收益 + (热路径省一次全量转换)低于语义风险,且当前没有热路径调用方依赖它被消除。 + 触发条件:出现真实热路径调用方,且愿意承担语义变更(届时按 API 层显式区分"量化模拟 / 直接返回")。 + +3. **13 §9b H3/H4/H5** —— 裁定:**不做**(三条均低成本低收益)。 + H3 解码循环骨架:仅剩三个 `generate()` 的循环体(采样段已合并),因运行态不同而保留; + H4 精度/形状三元组收敛:`dsl::compute(engine, expr, rows, cols, p_.compute)` 尾部精度参数不宜机械 + 改写——RAPT 残差相加用的是**刻意不同**的精度,是反例;H5 掩码/索引构造:注意力侧已随 + `CausalDocScoreMask::prepare` 收敛(两份数值相同的宿主缓冲并为一份),conv 置换 / transformer + patch 提取仍是"CPU 循环 + `upload_span`"、未声明式化。H5 若将来做,随 header 拆分批次一起做。 + +4. **13 §9.4 测试公共头 `max_abs_diff` 多份副本统一** —— 裁定:**择机,不单独立项**。 + 实测当前有 **7 份定义**(`attn_consistency_test` / `ce_fusion_test` / `conv2d_gradcheck` / + `fused_gpu_test` / `gpu_test` / `maxpool_gradcheck` / `tensor_expr_test`),其中 conv2d 版带多形状 + 守卫(越界返回 1e9),其余无守卫。统一前必须逐点确认每个调用点是否依赖该守卫语义,故 + "不机械统一"是唯一安全前提;留待下次动测试公共头时顺带处理。 + +5. **13 §10「Tensor 抽象基类 + `CPUTensor`/`GPUTensor` 句柄指针化」选型** —— 裁定:**维持否决**。 + 由 `docs/development/17-unified-tensor-engine.md` §4.2 与 + `docs/development/15-computeengine-refresh.md` §4 裁定:它要解决的 device 互斥收益与存储多态 + 重叠,却要 `Result` 203 处指针化 + `span/vector` 元素类型连锁,且**不解决 engine 传参耦合**。 + +6. **13 §10.6 宿主中转动词改名(`to_matrix`/`from_matrix`/`copy_from` → `to_host`/`from_host`/`upload`)** + —— 裁定:**方案作废,不再执行**。由 17 §3 D9 改为"**保留名称** + I/O 分组 + grep 审计": + `Matrix` 被裁定降级为宿主 I/O 载体(L2+ 禁用即铁律 #12),动词名不再承担"命名即防线"的职责, + 改由 `bench/doc_inventory.ps1` 第 [4] 节分层审计(L2 违规计数)兑现。 + +**仍未做的项(已迁出本节)**:13 §2(超大 header 拆分)、§3(text_train.cpp 模块化)、§4(gui.py +声明式字段表)、§5.1(F16C 半精度转换快路径)、§5.3(GUI 绘制节流)、§7(checkpoint 优化器状态)、 +§8(CI 冒烟)、§9 第 5 条(18 号文档记作 §9.5;`gpt_test` 偶发失败待观察:62 次直跑 1 次失败、 +日志被覆盖未定位,已列为 18 号文档 P1-4)、§9b 末尾未编号 bullet(`bench/doc_inventory.ps1` 的 +`host_bridge_uses` 计数口径未复核,13 与 17 记 41、18 号文档实测 40),以及 §10 吸收后的 **M7**, +其行动项与触发条件见 `docs/development/18-roadmap.md`。13 原文件中"仍然生效的工程教训"类正文 +(§9 前四条约束、§9b 的 `dsl::compute` 无 2 参重载等)属当前状态说明,不在本节归档(避免与 +AGENTS §7、`docs/development/01-compute-engine-development.md` 等现行文档重复)。 + +**「文档坐标已过期」事实记录(2026-10-01 实测,供后续纠偏)**:13 正文中的行数/份数是审查当时快照, +现已过期——`include/neuralnet.cpp/backend/compute_vk_backend.hpp` **5174** 行(13 写 ≈4700)、 +`include/neuralnet.cpp/expr_glsl_gen.hpp` **2424** 行(写 2310)、 +`include/neuralnet.cpp/compute_cpu_engine.hpp` **2353** 行(写 2543)、 +`src/text_train.cpp` **1746** 行(写 1860)、`gui.py` **1646** 行(非空 1456;13 写 1636, +且所谓"五 Tab"实为 **7** 个 `collect_args` 定义);`max_abs_diff` 定义实测 **7** 份 +(13 与 `src/test_common.hpp:19` 注释均写 9,交接口径记 8——三处不一致,以本实测为准)。 +ctest 实测 **19**(本轮不做构建/ctest 复核,采信交接结论),而 `AGENTS.md` §12 与 +`docs/release-notes/v1.5.0.md`、`docs/development/17-unified-tensor-engine.md` 仍写 20—— +19 vs 20 的矛盾待 18 号文档与 AGENTS 修订统一。