From 3cf591c6cb36f055ebfacedab7763fe6f8c76b03 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 14:21:31 +0800 Subject: [PATCH 01/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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 修订统一。 From aba9ecb2941f9e155ba89ac0fd10790ee586eae3 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 18:11:20 +0800 Subject: [PATCH 18/30] =?UTF-8?q?A1a:=20emitter=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=9C=9F=E7=B2=BE=E5=BA=A6=E5=88=86=E6=B4=BE?= =?UTF-8?q?=EF=BC=88V1=EF=BC=89=E5=8F=91=E5=B0=84=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 EXPR_PREC_SIG_DISPATCH 哨兵(0x00020000,落在输入位/输出位之外, 既有谓词对它一律 false)+ 后缀 #x。 五个生成器(元素/matmul/fold v1/fold v2/reduce)支持分派形态:输入输出各 声明 f32 + float16_t 双视图,加载/存储处按 push constant uint prec 走 uniform 分支。索引数学**逐字不变**(两种视图共用同一逻辑下标),因此分派 分支与既有带类型变体在数值上等价。 非分派路径的发射文本完全保留:重建 build/generated/fused_registry.hpp 与改前 SHA256 逐字节一致(6DF95D20…84D)。 --- include/neuralnet.cpp/expr_glsl_gen.hpp | 485 ++++++++++++++++++------ include/neuralnet.cpp/expr_spec.hpp | 23 ++ 2 files changed, 398 insertions(+), 110 deletions(-) diff --git a/include/neuralnet.cpp/expr_glsl_gen.hpp b/include/neuralnet.cpp/expr_glsl_gen.hpp index aaf6b59d..f8b1ec50 100644 --- a/include/neuralnet.cpp/expr_glsl_gen.hpp +++ b/include/neuralnet.cpp/expr_glsl_gen.hpp @@ -72,31 +72,26 @@ inline const char* glsl_unary_op(ExprOp op) // 包装,整个视图表达式保持 float16_t 类型(GL_EXT_shader_explicit_ // arithmetic_types_float16 允许 f16 算术/取负)。默认 false = f32 算术 // 变体(f16 存储 + 读处显式转 f32)。 -inline void glsl_view_read(std::ostringstream& os, - const ExprView& v, std::uint32_t buf_id, - const std::string& idx_var, - const std::string& row_var, const std::string& col_var, - std::uint32_t vp_slot = 0, - ExprPrecSig sig = 0, - bool f16_native = false) +// 单次构建:给定缓冲区名与该视图是否 f16 视图,产出读取表达式字符串。 +// **非分派路径的输出与历史逐字一致**(RotateHalf 内部转 f32、其余分支裸读, +// 外层转换仍由调用方 float(...) 负责)——这是 V0/V2 文本 byte-identical 的前提。 +inline std::string glsl_view_read_(const ExprView& v, std::uint32_t buf_id, + const std::string& buf, bool f16_buf, + const std::string& idx_var, + const std::string& row_var, const std::string& col_var, + std::uint32_t vp_slot, + ExprPrecSig sig) { - const std::string buf = "b" + std::to_string(buf_id); - // 该输入的存储类型:f16 时**视图内部**的算术/索引读取必须显式转 f32 - // (GL_EXT_shader_16bit_storage 只允许存储,不允许 f16 算术: - // `-b0[i]` / `uint(b0[i])` 都会被 glslc 拒绝)。视图外层的整体转换由 - // 调用方(emit_scalar_chain / vec4 读取)负责。 - // f16_native 时扩展提供了 f16 算术 → 不转(保持 float16_t 类型链)。 - const bool f16_buf = expr_prec_sig_in_f16(sig, buf_id) && !f16_native; const auto rw = [&](const std::string& idx) -> std::string { return f16_buf ? ("float(" + buf + "[" + idx + "])") : (buf + "[" + idx + "]"); }; + const bool disp = expr_prec_sig_is_dispatch(sig); switch (v.kind) { default: case static_cast(ExprViewKind::Linear): - os << buf << "[" << idx_var << "]"; - return; + return buf + "[" + idx_var + "]"; case static_cast(ExprViewKind::RotateHalf): { // block 为运行时视图参数(push constant vpN),形状无关融合 @@ -109,26 +104,18 @@ inline void glsl_view_read(std::ostringstream& os, const std::string lo = "((" + blk + " * vp" + std::to_string(vp_slot) + " + " + rl + " - " + half + ") * cols + " + col_var + ")"; const std::string neg = v.negate_first_half ? "-" : ""; - os << "((" << rl << " < " << half << ") ? " - << neg << rw(hi) << " : " << rw(lo) << ")"; - return; + return "((" + rl + " < " + half + ") ? " + neg + rw(hi) + " : " + rw(lo) + ")"; } case static_cast(ExprViewKind::RowMod): - { // 周期为运行时视图参数(push constant vpN),形状无关融合 - os << buf << "[(" << row_var << " % vp" << std::to_string(vp_slot) << ") * cols + " - << col_var << "]"; - return; - } + return buf + "[(" + row_var + " % vp" + std::to_string(vp_slot) + ") * cols + " + + col_var + "]"; case static_cast(ExprViewKind::RowAccess): - { // 行偏移+取模:data[(offset + r % mod)*cols + c]。 // vp_slot=mod(op),vp_slot+1=offset(param2),双 vp 槽(形状无关)。 - os << buf << "[(" << "vp" << std::to_string(vp_slot + 1) << " + (" - << row_var << " % vp" << std::to_string(vp_slot) << ")) * cols + " - << col_var << "]"; - return; - } + return buf + "[(" + "vp" + std::to_string(vp_slot + 1) + " + (" + + row_var + " % vp" + std::to_string(vp_slot) + ")) * cols + " + + col_var + "]"; case static_cast(ExprViewKind::GroupedReduceSum): case static_cast(ExprViewKind::GroupedReduceMax): // 分组归约:输出行 r → 输入组行 [r*R, (r+1)*R),列不变。 @@ -137,34 +124,69 @@ inline void glsl_view_read(std::ostringstream& os, // (内部为运行期循环;累加顺序与 CPU 模板路径 GroupedReduceRef::eval // 逐元素一致,铁律 8)。辅助函数由 // `glsl_emit_grouped_reduce_helpers()` 在 shader 顶部发射。 - os << "gr_r" << buf_id << "(" << row_var << ", " << col_var << ")"; - return; + return "gr_r" + std::to_string(buf_id) + "(" + row_var + ", " + col_var + ")"; case static_cast(ExprViewKind::RowBroadcast): - os << buf << "[" << row_var << "]"; // 输入 (rows,1):每行一个值 - return; + return buf + "[" + row_var + "]"; // 输入 (rows,1):每行一个值 case static_cast(ExprViewKind::ColBroadcast): - os << buf << "[" << col_var << "]"; // 输入 (1,cols):每列一个值 - return; + return buf + "[" + col_var + "]"; // 输入 (1,cols):每列一个值 case static_cast(ExprViewKind::RowGather): + { // 按标签行收集:data[uint(labels[col]) * cols + col](labels 槽 = v.param) // f16 标签槽(少见)需先转 f32 再取整:uint(float16_t) 不合法。 - os << buf << "[uint(" - << (expr_prec_sig_in_f16(sig, v.param) ? "float(b" : "b") << v.param - << "[" << col_var << "]" - << (expr_prec_sig_in_f16(sig, v.param) ? ")" : "") << ") * cols + " - << col_var << "]"; - return; + // 标签槽可能与被收集的槽**不是同一槽**,故判据用 v.param;运行期 + // 分派形态下标签槽自身也走分派位。 + const std::string lb = "b" + std::to_string(v.param); + std::string lab; + if (disp) + lab = "((pc.prec >> " + std::to_string(v.param) + "u & 1u) != 0u ? uint(float(" + + lb + "h[" + col_var + "])) : uint(" + lb + "[" + col_var + "]))"; + else if (expr_prec_sig_in_f16(sig, v.param)) + lab = "uint(float(" + lb + "[" + col_var + "]))"; + else + lab = "uint(" + lb + "[" + col_var + "])"; + return buf + "[" + lab + " * cols + " + col_var + "]"; + } case static_cast(ExprViewKind::BatchMod): // 按批次取模索引:data[batch % vpN](需要 batch 变量;取模数 // num_heads 为运行时视图参数 → 同结构不同头数共享一个融合 shader) - os << buf << "[batch % vp" << std::to_string(vp_slot) << "]"; - return; + return buf + "[batch % vp" + std::to_string(vp_slot) + "]"; case static_cast(ExprViewKind::BatchCol): // 按 (batch, col) 切片:data[batch*vpN + col](需要 batch 变量; // 每批列数 seq 为运行时视图参数 → 同结构不同 seq 共享一个融合 shader) - os << buf << "[batch * vp" << std::to_string(vp_slot) << " + " << col_var << "]"; + return buf + "[batch * vp" + std::to_string(vp_slot) + " + " + col_var + "]"; + } +} + +inline void glsl_view_read(std::ostringstream& os, + const ExprView& v, std::uint32_t buf_id, + const std::string& idx_var, + const std::string& row_var, const std::string& col_var, + std::uint32_t vp_slot = 0, + ExprPrecSig sig = 0, + bool f16_native = false) +{ + const std::string buf = "b" + std::to_string(buf_id); + // 该输入的存储类型:f16 时**视图内部**的算术/索引读取必须显式转 f32 + // (GL_EXT_shader_16bit_storage 只允许存储,不允许 f16 算术: + // `-b0[i]` / `uint(b0[i])` 都会被 glslc 拒绝)。视图外层的整体转换由 + // 调用方(emit_scalar_chain / vec4 读取)负责。 + // f16_native 时扩展提供了 f16 算术 → 不转(保持 float16_t 类型链)。 + const bool f16_buf = expr_prec_sig_in_f16(sig, buf_id) && !f16_native; + if (!expr_prec_sig_is_dispatch(sig)) + { + os << glsl_view_read_(v, buf_id, buf, f16_buf, idx_var, row_var, col_var, + vp_slot, sig); return; } + // ── 运行期精度分派(V1):同一索引同时作两种视图解引用,uniform 选择 ── + // f16 分支取 float(...)(行内 rw 已转的表达式再套一层 float 是无害恒等)。 + const std::string f32e = glsl_view_read_(v, buf_id, buf, false, idx_var, + row_var, col_var, vp_slot, sig); + const std::string hbuf = buf + "h"; + const std::string f16e = glsl_view_read_(v, buf_id, hbuf, true, idx_var, + row_var, col_var, vp_slot, sig); + os << "((pc.prec >> " << buf_id << "u & 1u) != 0u ? float(" << f16e + << ") : " << f32e << ")"; } // ── 分组归约视图的读取辅助函数发射(R 运行期 → 循环替代编译期展开链)────── @@ -195,10 +217,14 @@ inline void glsl_emit_grouped_reduce_helpers(std::ostringstream& os, vp += expr_view_runtime_param_slots( static_cast(spec.views[j].kind)); const bool is_max = (k == ExprViewKind::GroupedReduceMax); + const bool disp = expr_prec_sig_is_dispatch(sig); 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)"; + if (disp) + return "((pc.prec >> " + std::to_string(i) + "u & 1u) != 0u ? float(" + + buf + "h[" + idx + "]) : " + buf + "[" + idx + "])"; return f16_buf ? ("float(" + buf + "[" + idx + "])") : (buf + "[" + idx + "]"); }; @@ -321,6 +347,9 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) // VFMA 累加 / 尾逐元素链全部保持 f32,符合 §7.2「f32 参考 + 输出舍入」)。 // 形状/分块/双缓冲/barrier 节奏与 f32 版完全一致(只有加载粒度变化)。 const bool sign_f16 = (sig != 0); + // 运行期精度分派(V1):输入/输出各声明 f32 + float16_t 双视图, + // 加载处按 PC `prec` 走 uniform 分支 —— 一个结构一份 shader 覆盖全部签名。 + const bool disp = expr_prec_sig_is_dispatch(sig); const bool a_f16 = sign_f16 && expr_prec_sig_in_f16(sig, a_slot); const bool b_f16 = sign_f16 && expr_prec_sig_in_f16(sig, b_slot); const bool out_f16 = sign_f16 && expr_prec_sig_out_f16(sig); @@ -336,20 +365,36 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) std::ostringstream L; L << "// ── 自动生成(AOT 算子融合 · matmul 分块 4×4 双缓冲),请勿手动编辑 ──\n"; L << "// 表达式: " << name; - if (sign_f16) + if (sign_f16 && !disp) L << " [精度 " << expr_prec_sig_str(sig, n_inputs) << "]"; + else if (disp) + L << " [运行期精度分派]"; L << "\n"; L << "#version 450\n\n"; if (sign_f16) L << "#extension GL_EXT_shader_16bit_storage : require\n\n"; L << "layout(local_size_x = " << T << ", local_size_y = " << T << ") in;\n\n"; for (std::size_t i = 0; i < n_inputs; ++i) + { L << "layout(std430, binding = " << i << ") readonly buffer Buf" << i - << " { " << ((sign_f16 && expr_prec_sig_in_f16(sig, i)) ? "float16_t" : "float") + << " { " << ((disp || (sign_f16 && expr_prec_sig_in_f16(sig, i))) + ? (disp ? "float" : "float16_t") : "float") << " b" << i << "[]; };\n"; - L << "layout(std430, binding = " << n_inputs - << ") writeonly buffer BufOut { " << (out_f16 ? "float16_t" : "float") - << " bout[]; };\n"; + if (disp) + L << "layout(std430, binding = " << (n_inputs + i) + << ") readonly buffer Buf" << i << "h { float16_t b" << i << "h[]; };\n"; + } + if (disp) + { + L << "layout(std430, binding = " << (2 * n_inputs) + << ") writeonly buffer BufOut { float bout[]; };\n"; + L << "layout(std430, binding = " << (2 * n_inputs + 1) + << ") writeonly buffer BufOuth { float16_t bout16[]; };\n"; + } + else + L << "layout(std430, binding = " << n_inputs + << ") writeonly buffer BufOut { " << (out_f16 ? "float16_t" : "float") + << " bout[]; };\n"; // vec4 别名视图(A/B 全局快路径专用;同 binding 双声明 std430,op 级 // matmul_tiled 同款)。a_slot==b_slot 时不发别名、快路径整体关闭。 // f16 槽的别名用 uvec2(4×half = 8B,std430 步长恰好 8)→ 由 @@ -357,18 +402,37 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) const bool vec4_ok = (a_slot != b_slot); if (vec4_ok) { - const auto alias_type = [&](std::uint32_t slot) -> const char* { - return (sign_f16 && expr_prec_sig_in_f16(sig, slot)) ? "uvec2" : "vec4"; - }; - L << "layout(std430, binding = " << a_slot << ") readonly buffer BufAv4 { " - << alias_type(a_slot) << " b" << a_slot << "v4[]; };\n"; - L << "layout(std430, binding = " << b_slot << ") readonly buffer BufBv4 { " - << alias_type(b_slot) << " b" << b_slot << "v4[]; };\n"; + if (disp) + { + L << "layout(std430, binding = " << a_slot << ") readonly buffer BufAv4 { vec4 b" + << a_slot << "v4[]; };\n"; + L << "layout(std430, binding = " << b_slot << ") readonly buffer BufBv4 { vec4 b" + << b_slot << "v4[]; };\n"; + L << "layout(std430, binding = " << (n_inputs + a_slot) + << ") readonly buffer BufAv4h { uvec2 b" << a_slot << "v4h[]; };\n"; + L << "layout(std430, binding = " << (n_inputs + b_slot) + << ") readonly buffer BufBv4h { uvec2 b" << b_slot << "v4h[]; };\n"; + } + else + { + const auto alias_type = [&](std::uint32_t slot) -> const char* { + return (sign_f16 && expr_prec_sig_in_f16(sig, slot)) ? "uvec2" : "vec4"; + }; + L << "layout(std430, binding = " << a_slot << ") readonly buffer BufAv4 { " + << alias_type(a_slot) << " b" << a_slot << "v4[]; };\n"; + L << "layout(std430, binding = " << b_slot << ") readonly buffer BufBv4 { " + << alias_type(b_slot) << " b" << b_slot << "v4[]; };\n"; + } } L << "\n"; // 别名槽的 vec4 读取表达式(f16 → unpackHalf2x16;f32 → 直接别名) const auto vec4_load = [&](std::uint32_t slot, const std::string& idx) -> std::string { const std::string arr = "b" + std::to_string(slot) + "v4[" + idx + "]"; + if (disp) + return "((pc.prec >> " + std::to_string(slot) + "u & 1u) != 0u ? vec4(" + "unpackHalf2x16(b" + std::to_string(slot) + "v4h[" + idx + + "].x), unpackHalf2x16(b" + std::to_string(slot) + "v4h[" + idx + + "].y)) : " + arr + ")"; if (sign_f16 && expr_prec_sig_in_f16(sig, slot)) return "vec4(unpackHalf2x16(" + arr + ".x), unpackHalf2x16(" + arr + ".y))"; return arr; @@ -380,6 +444,8 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) 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"; + if (disp) + L << " uint prec; // 运行期精度分派位(bit i=输入 i 为 f16,bit16=输出)\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"; @@ -410,17 +476,27 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) 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 + "))]"; + const std::string idx = trans + ? "((batch*mm_k + (" + k_e + "))*m_per + (" + row_e + "))" + : "((batch*m_per + (" + row_e + "))*mm_k + (" + k_e + "))"; + const std::string an = "b" + std::to_string(a_slot); + if (disp) + return "((pc.prec >> " + std::to_string(a_slot) + "u & 1u) != 0u ? float(" + + an + "h[" + idx + "]) : " + an + "[" + idx + "])"; + const std::string raw = an + "[" + idx + "]"; return a_f16 ? ("float(" + raw + ")") : raw; }; const auto b_load = [&](const std::string& col_e, const std::string& k_e, bool trans) { - 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 + "))]"; + const std::string idx = trans + ? "((batch*cols + (" + col_e + "))*mm_k + (" + k_e + "))" + : "((batch*mm_k + (" + k_e + "))*cols + (" + col_e + "))"; + const std::string bn = "b" + std::to_string(b_slot); + if (disp) + return "((pc.prec >> " + std::to_string(b_slot) + "u & 1u) != 0u ? float(" + + bn + "h[" + idx + "]) : " + bn + "[" + idx + "])"; + const std::string raw = bn + "[" + idx + "]"; return b_f16 ? ("float(" + raw + ")") : raw; }; @@ -707,9 +783,22 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) L << " const uint rr = block_row + ty * 4u + i;\n"; L << " const uint cc = block_col + tx * 4u + j;\n"; L << " if (rr < m_per && cc < cols)\n"; - L << " bout[(batch * m_per + rr) * cols + cc]\n"; - L << " = " << (out_f16 ? "float16_t(" : "") - << "eval_tail(acc[i][j], rr, cc, batch)" << (out_f16 ? ")" : "") << ";\n"; + if (disp) + { + L << " {\n"; + L << " const uint oi = (batch * m_per + rr) * cols + cc;\n"; + L << " if ((pc.prec >> 16u & 1u) != 0u)\n"; + L << " bout16[oi] = float16_t(eval_tail(acc[i][j], rr, cc, batch));\n"; + L << " else\n"; + L << " bout[oi] = eval_tail(acc[i][j], rr, cc, batch);\n"; + L << " }\n"; + } + else + { + L << " bout[(batch * m_per + rr) * cols + cc]\n"; + L << " = " << (out_f16 ? "float16_t(" : "") + << "eval_tail(acc[i][j], rr, cc, batch)" << (out_f16 ? ")" : "") << ";\n"; + } L << " }\n"; L << "}\n"; return L.str(); @@ -751,6 +840,8 @@ inline std::string generate_glsl_fold(const std::string& name, const ExprSpec& s const std::size_t n_inputs = spec.views.size(); // 带类型变体:f16 输入直读(float() 转 f32 归约——状态/累加全 // float,f16 只在存储边界)+ f16 输出落回((rows,1)/(rows,vector_out))。 + // 运行期精度分派(V1):双视图 + PC `prec`(同 generate_glsl)。 + const bool disp = expr_prec_sig_is_dispatch(sig); const bool out_f16 = sig != 0 && expr_prec_sig_out_f16(sig); const auto in_type = [&](std::size_t i) -> const char* { @@ -770,17 +861,32 @@ inline std::string generate_glsl_fold(const std::string& name, const ExprSpec& s L << "#extension GL_EXT_shader_16bit_storage : require\n\n"; L << "layout(local_size_x = 256) in;\n\n"; for (std::size_t i = 0; i < n_inputs; ++i) + { L << "layout(std430, binding = " << i << ") readonly buffer Buf" << i - << " { " << in_type(i) << " b" << i << "[]; };\n"; - L << "layout(std430, binding = " << n_inputs - << ") writeonly buffer BufOut { " << (out_f16 ? "float16_t" : "float") - << " bout[]; };\n\n"; + << " { " << (disp ? "float" : in_type(i)) << " b" << i << "[]; };\n"; + if (disp) + L << "layout(std430, binding = " << (n_inputs + i) + << ") readonly buffer Buf" << i << "h { float16_t b" << i << "h[]; };\n"; + } + if (disp) + { + L << "layout(std430, binding = " << (2 * n_inputs) + << ") writeonly buffer BufOut { float bout[]; };\n"; + L << "layout(std430, binding = " << (2 * n_inputs + 1) + << ") writeonly buffer BufOuth { float16_t bout16[]; };\n\n"; + } + else + L << "layout(std430, binding = " << n_inputs + << ") writeonly buffer BufOut { " << (out_f16 ? "float16_t" : "float") + << " bout[]; };\n\n"; L << "layout(push_constant) uniform PC {\n"; L << " uint count;\n"; L << " uint cols;\n"; L << " uint rows;\n"; L << " uint vector_out;\n"; L << " uint fold_k;\n"; + if (disp) + L << " uint prec; // 运行期精度分派位(bit i=输入 i 为 f16,bit16=输出)\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"; @@ -943,8 +1049,17 @@ inline std::string generate_glsl_fold(const std::string& name, const ExprSpec& s L << " "; emit_assign(ins, "r" + std::to_string(ins.dst), "0u"); } - L << " bout[row] = " << (out_f16 ? "float16_t(r" : "r") - << static_cast(f.finalize.back().dst) << (out_f16 ? ")" : "") << ";\n"; + if (disp) + { + L << " if ((pc.prec >> 16u & 1u) != 0u)\n"; + L << " bout16[row] = float16_t(r" + << static_cast(f.finalize.back().dst) << ");\n"; + L << " else\n"; + L << " bout[row] = r" << static_cast(f.finalize.back().dst) << ";\n"; + } + else + L << " bout[row] = " << (out_f16 ? "float16_t(r" : "r") + << static_cast(f.finalize.back().dst) << (out_f16 ? ")" : "") << ";\n"; L << "}\n"; return L.str(); } @@ -994,6 +1109,7 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec const bool has_mm = f.matmul.has_value(); // 带类型变体:f16 输入直读(读处 float() 转 f32——mm/vecacc/ // 链算术全 float)+ f16 输出落回。 + const bool disp = expr_prec_sig_is_dispatch(sig); const bool out_f16 = sig != 0 && expr_prec_sig_out_f16(sig); const auto in_type = [&](std::size_t i) -> const char* { @@ -1002,6 +1118,18 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec // 直接下标读(mm 段 A/B、vecacc B——非视图,平铺索引):f16 输入包 float() const auto in_rd = [&](std::size_t i, const std::string& e) -> std::string { + if (disp) + { + // e 是完整表达式(含 f32 缓冲名),分派分支需换成同名 h 视图 + const std::string fn = "b" + std::to_string(i) + "["; + const std::string hn = "b" + std::to_string(i) + "h["; + std::string e16 = e; + const std::size_t p = e16.find(fn); + if (p != std::string::npos) + e16.replace(p, fn.size(), hn); + return "((pc.prec >> " + std::to_string(i) + "u & 1u) != 0u ? float(" + + e16 + ") : " + e + ")"; + } return (sig != 0 && expr_prec_sig_in_f16(sig, i)) ? ("float(" + e + ")") : e; }; // 输出列数 **运行时读 PC 的 vector_out 槽**(形状无关:同 key 服务任意 @@ -1020,16 +1148,31 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec L << "#extension GL_EXT_shader_16bit_storage : require\n\n"; L << "layout(local_size_x = 256) in;\n\n"; for (std::size_t i = 0; i < n_inputs; ++i) + { L << "layout(std430, binding = " << i << ") readonly buffer Buf" << i - << " { " << in_type(i) << " b" << i << "[]; };\n"; - L << "layout(std430, binding = " << n_inputs - << ") writeonly buffer BufOut { " << (out_f16 ? "float16_t" : "float") - << " bout[]; };\n\n"; + << " { " << (disp ? "float" : in_type(i)) << " b" << i << "[]; };\n"; + if (disp) + L << "layout(std430, binding = " << (n_inputs + i) + << ") readonly buffer Buf" << i << "h { float16_t b" << i << "h[]; };\n"; + } + if (disp) + { + L << "layout(std430, binding = " << (2 * n_inputs) + << ") writeonly buffer BufOut { float bout[]; };\n"; + L << "layout(std430, binding = " << (2 * n_inputs + 1) + << ") writeonly buffer BufOuth { float16_t bout16[]; };\n\n"; + } + else + L << "layout(std430, binding = " << n_inputs + << ") writeonly buffer BufOut { " << (out_f16 ? "float16_t" : "float") + << " bout[]; };\n\n"; L << "layout(push_constant) uniform PC {\n"; L << " uint count;\n uint cols;\n uint rows;\n" " uint vector_out;\n uint fold_k;\n"; if (has_mm) L << " uint mm_k;\n uint mm_batch;\n"; + if (disp) + L << " uint prec; // 运行期精度分派位(bit i=输入 i 为 f16,bit16=输出)\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"; @@ -1416,9 +1559,21 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec { L << emit_assign(ins, "r" + std::to_string(ins.dst), 'F', "dd"); } - L << " if (row_ok) bout[row * vector_out + dd] = " - << (out_f16 ? "float16_t(r" : "r") - << static_cast(f.finalize.back().dst) << (out_f16 ? ")" : "") << ";\n"; // 越界 ri 不写回 + if (disp) + { + L << " if (row_ok) {\n"; + L << " if ((pc.prec >> 16u & 1u) != 0u)\n"; + L << " bout16[row * vector_out + dd] = float16_t(r" + << static_cast(f.finalize.back().dst) << ");\n"; + L << " else\n"; + L << " bout[row * vector_out + dd] = r" + << static_cast(f.finalize.back().dst) << ";\n"; + L << " }\n"; + } + else + L << " if (row_ok) bout[row * vector_out + dd] = " + << (out_f16 ? "float16_t(r" : "r") + << static_cast(f.finalize.back().dst) << (out_f16 ? ")" : "") << ";\n"; // 越界 ri 不写回 L << " }\n"; L << " }\n"; // NR 行循环尾 L << "}\n"; @@ -1467,9 +1622,8 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, return generate_glsl_matmul(name, spec, sig); const std::size_t n_inputs = spec.views.size(); + const bool disp = expr_prec_sig_is_dispatch(sig); std::ostringstream L; - - // 逐输入/输出的存储类型 const auto in_type = [&](std::size_t i) -> const char* { return (sig != 0 && expr_prec_sig_in_f16(sig, i)) ? "float16_t" : "float"; @@ -1479,8 +1633,10 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << "// ── 自动生成(AOT 算子融合),请勿手动编辑 ──\n"; L << "// 表达式: " << name; - if (sig != 0) + if (sig != 0 && !disp) L << " [精度 " << expr_prec_sig_str(sig, n_inputs) << "]"; + else if (disp) + L << " [运行期精度分派]"; L << "\n"; L << "#version 450\n\n"; if (any_f16) @@ -1491,17 +1647,36 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << "#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require\n\n"; L << "layout(local_size_x = 256) in;\n\n"; + // 逐输入/输出的存储类型 for (std::size_t i = 0; i < n_inputs; ++i) + { L << "layout(std430, binding = " << i << ") readonly buffer Buf" << i - << " { " << in_type(i) << " b" << i << "[]; };\n"; - L << "layout(std430, binding = " << n_inputs - << ") writeonly buffer BufOut { " << (out_f16 ? "float16_t" : "float") - << " bout[]; };\n\n"; + << " { " << (disp ? "float" : in_type(i)) << " b" << i << "[]; };\n"; + if (disp) + L << "layout(std430, binding = " << (n_inputs + i) + << ") readonly buffer Buf" << i << "h { float16_t b" << i << "h[]; };\n"; + } + if (disp) + { + L << "layout(std430, binding = " << (2 * n_inputs) + << ") writeonly buffer BufOut { float bout[]; };\n"; + L << "layout(std430, binding = " << (2 * n_inputs + 1) + << ") writeonly buffer BufOuth { float16_t bout16[]; };\n\n"; + } + else + { + L << "layout(std430, binding = " << n_inputs + << ") writeonly buffer BufOut { " << (out_f16 ? "float16_t" : "float") + << " bout[]; };\n\n"; + } - // push constants:count + cols(视图行/列)+ 运行时视图参数 vp + 常量 + // push constants:count + cols(视图行/列)+ [prec] + 运行时视图参数 vp + 常量 L << "layout(push_constant) uniform PC {\n"; L << " uint count;\n"; L << " uint cols;\n"; + if (disp) + L << " uint prec; // 运行期精度分派位:bit i = 输入 i 为 f16," + "bit16 = 输出为 f16\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"; @@ -1686,6 +1861,14 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, // native16:寄存器本就是 float16_t → 直写(免冗余 float16_t() 转换) if (native16) L << " bout[i] = r" << last_dst << ";\n"; + else if (disp) + { + L << " if ((pc.prec >> 16u & 1u) != 0u) {\n"; + L << " bout16[i] = float16_t(r" << last_dst << ");\n"; + L << " } else {\n"; + L << " bout[i] = r" << last_dst << ";\n"; + L << " }\n"; + } else L << " bout[i] = " << (out_f16 ? "float16_t(r" : "r") << last_dst @@ -1716,8 +1899,11 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, const auto k = static_cast(spec.views[i].kind); const std::string cvt = (sig != 0 && expr_prec_sig_in_f16(sig, i)) ? "float" : ""; const auto ld = [&](const std::string& idx) { - return cvt.empty() ? ("b" + std::to_string(i) + "[" + idx + "]") - : ("float(b" + std::to_string(i) + "[" + idx + "])"); + const std::string bf = "b" + std::to_string(i) + "[" + idx + "]"; + if (disp) + return "((pc.prec >> " + std::to_string(i) + "u & 1u) != 0u ? float(b" + + std::to_string(i) + "h[" + idx + "]) : " + bf + ")"; + return cvt.empty() ? bf : ("float(" + bf + ")"); }; if (k == ExprViewKind::Linear) L << " const vec4 v" << i << " = vec4(" << ld("base") << ", " @@ -1815,13 +2001,29 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, } } const auto st = [&](const std::string& comp) { - return out_f16 ? ("float16_t(r" + std::to_string(last_dst) + "." + comp + ")") - : ("r" + std::to_string(last_dst) + "." + comp); + const std::string v = "r" + std::to_string(last_dst) + "." + comp; + return (out_f16 && !disp) ? ("float16_t(" + v + ")") : v; }; - L << " bout[base] = " << st("x") << ";\n"; - L << " bout[base+1u] = " << st("y") << ";\n"; - L << " bout[base+2u] = " << st("z") << ";\n"; - L << " bout[base+3u] = " << st("w") << ";\n"; + // 运行期精度分派:输出双视图 + uniform 存储分支(每元素只写一次, + // 两侧视图按各自的元素宽度寻址,无别名风险)。 + const auto emit_store4 = [&](const char* arr, bool c16, const char* ind) { + const std::string o = c16 ? "float16_t(" : ""; + const std::string c = c16 ? ")" : ""; + L << ind << arr << "[base] = " << o << st("x") << c << ";\n"; + L << ind << arr << "[base+1u] = " << o << st("y") << c << ";\n"; + L << ind << arr << "[base+2u] = " << o << st("z") << c << ";\n"; + L << ind << arr << "[base+3u] = " << o << st("w") << c << ";\n"; + }; + if (disp) + { + L << " if ((pc.prec >> 16u & 1u) != 0u) {\n"; + emit_store4("bout16", true, " "); + L << " } else {\n"; + emit_store4("bout", false, " "); + L << " }\n"; + } + else + emit_store4("bout", out_f16, " "); L << " return;\n"; L << " }\n"; @@ -1837,9 +2039,18 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << " const uint batch = 0u;\n"; emit_scalar_chain(L, " ", "e", need_row ? "row" : "", need_col ? "col" : ""); - L << " bout[e] = " - << (out_f16 ? "float16_t(r" : "r") << last_dst - << (out_f16 ? ")" : "") << ";\n"; + const std::string sv = "r" + std::to_string(last_dst); + if (disp) + { + L << " if ((pc.prec >> 16u & 1u) != 0u) {\n"; + L << " bout16[e] = float16_t(" << sv << ");\n"; + L << " } else {\n"; + L << " bout[e] = " << sv << ";\n"; + L << " }\n"; + } + else + L << " bout[e] = " << (out_f16 ? "float16_t(" : "") << sv + << (out_f16 ? ")" : "") << ";\n"; L << " }\n"; L << "}\n"; return L.str(); @@ -1904,9 +2115,13 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, // 缓冲区声明为 float16_t,读取处统一 float(...) 转 f32(累加/合并/尾链 // 全在 f32,符合 §7.2),输出写回按 out 位 float16_t(...)。 const bool sign_f16 = (sig != 0); + const bool disp = expr_prec_sig_is_dispatch(sig); const bool out_f16 = sign_f16 && expr_prec_sig_out_f16(sig); const auto rd = [&](std::size_t k, const std::string& idx) -> std::string { const std::string raw = "b" + std::to_string(k) + "[" + idx + "]"; + if (disp) + return "((pc.prec >> " + std::to_string(k) + "u & 1u) != 0u ? float(b" + + std::to_string(k) + "h[" + idx + "]) : " + raw + ")"; return (sign_f16 && expr_prec_sig_in_f16(sig, k)) ? ("float(" + raw + ")") : raw; }; const auto wr = [&](const std::string& v) -> std::string { @@ -1927,12 +2142,27 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << "\n"; L << "layout(local_size_x = 256) in;\n\n"; for (std::size_t i = 0; i < n_inputs; ++i) + { L << "layout(std430, binding = " << i << ") readonly buffer Buf" << i - << " { " << ((sign_f16 && expr_prec_sig_in_f16(sig, i)) ? "float16_t" : "float") + << " { " + << ((disp || (sign_f16 && expr_prec_sig_in_f16(sig, i))) + ? (disp ? "float" : "float16_t") : "float") << " b" << i << "[]; };\n"; - L << "layout(std430, binding = " << n_inputs - << ") writeonly buffer BufOut { " << (out_f16 ? "float16_t" : "float") - << " bout[]; };\n\n"; + if (disp) + L << "layout(std430, binding = " << (n_inputs + i) + << ") readonly buffer Buf" << i << "h { float16_t b" << i << "h[]; };\n"; + } + if (disp) + { + L << "layout(std430, binding = " << (2 * n_inputs) + << ") writeonly buffer BufOut { float bout[]; };\n"; + L << "layout(std430, binding = " << (2 * n_inputs + 1) + << ") writeonly buffer BufOuth { float16_t bout16[]; };\n\n"; + } + else + L << "layout(std430, binding = " << n_inputs + << ") writeonly buffer BufOut { " << (out_f16 ? "float16_t" : "float") + << " bout[]; };\n\n"; L << "layout(push_constant) uniform PC {\n"; L << " uint count;\n"; L << " uint cols;\n"; @@ -1947,6 +2177,8 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << " uint mm_batch;\n"; L << " uint mm_trans; // bit0=transA, bit1=transB(不进 key)\n"; } + if (disp) + L << " uint prec; // 运行期精度分派位(bit i=输入 i 为 f16,bit16=输出)\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"; @@ -1958,6 +2190,22 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << "};\n\n"; L << "shared float s_red[" << n_slots << "][256];\n\n"; + // 输出写回:非分派 = 单条赋值(wr 包 float16_t);分派 = 输出双视图 + // + uniform 分支(每个元素只写一次)。 + const auto emit_wr = [&](const char* ind, const std::string& lhs, + const std::string& v) + { + if (!disp) + { + L << ind << "bout[" << lhs << "] = " << wr(v) << ";\n"; + return; + } + L << ind << "if ((pc.prec >> 16u & 1u) != 0u)\n"; + L << ind << " bout16[" << lhs << "] = float16_t(" << v << ");\n"; + L << ind << "else\n"; + L << ind << " bout[" << lhs << "] = " << v << ";\n"; + }; + // ── matmul 段(分块 + batch):当前元素 (row,col) 的 matmul 值(内联 // K 循环点积,不物化 (batch*M,N) 中间矩阵;**transA/transB 是运行期 // operand layout(mm_trans,不进 key)→ 两种索引都发射、运行时三元选择**, @@ -2274,25 +2522,29 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << " if (vector_out == 1u) {\n"; L << " if (tid == 0u) {\n"; if (last_is_reduce) - L << " bout[idx] = " << wr("s_red[" + std::to_string(slot_of_instr[spec.instrs.back().dst]) + "][0]") << ";\n"; + emit_wr(" ", "idx", + "s_red[" + std::to_string(slot_of_instr[spec.instrs.back().dst]) + "][0]"); else { L << " const uint row = idx;\n const uint col = 0u;\n"; emit_reg_decl(); emit_instrs(0, spec.instrs.size()); - L << " bout[idx] = " << wr("r" + std::to_string(static_cast(spec.instrs.back().dst))) << ";\n"; + emit_wr(" ", "idx", + "r" + std::to_string(static_cast(spec.instrs.back().dst))); } L << " }\n"; L << " return;\n"; L << " }\n"; L << loop_decl; if (last_is_reduce) - L << " bout[" << out_idx << "] = " << wr("s_red[" + std::to_string(slot_of_instr[spec.instrs.back().dst]) + "][0]") << ";\n"; + emit_wr(" ", out_idx, + "s_red[" + std::to_string(slot_of_instr[spec.instrs.back().dst]) + "][0]"); else { emit_reg_decl(); emit_instrs(0, spec.instrs.size()); - L << " bout[" << out_idx << "] = " << wr("r" + std::to_string(static_cast(spec.instrs.back().dst))) << ";\n"; + emit_wr(" ", out_idx, + "r" + std::to_string(static_cast(spec.instrs.back().dst))); } L << " }\n"; } @@ -2420,26 +2672,39 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << "\n // 输出\n"; L << " if (vector_out == 1u) {\n"; if (last_is_reduce) - L << " if (col < cols) bout[col] = " << wr("s_red[" + std::to_string(slot_of_instr[spec.instrs.back().dst]) + "][tid]") << ";\n"; + { + if (disp) + { + L << " if (col < cols) {\n"; + emit_wr(" ", "col", + "s_red[" + std::to_string(slot_of_instr[spec.instrs.back().dst]) + "][tid]"); + L << " }\n"; + } + else + L << " if (col < cols) bout[col] = " << wr("s_red[" + std::to_string(slot_of_instr[spec.instrs.back().dst]) + "][tid]") << ";\n"; + } else { L << " if (col < cols) {\n"; L << " const uint row = 0u;\n"; emit_reg_decl(); emit_instrs(0, spec.instrs.size()); - L << " bout[col] = " << wr("r" + std::to_string(static_cast(spec.instrs.back().dst))) << ";\n"; + emit_wr(" ", "col", + "r" + std::to_string(static_cast(spec.instrs.back().dst))); L << " }\n"; } L << " return;\n"; L << " }\n"; L << " if (col < cols) for (uint row = wb; row < rows; row += 8u) {\n"; if (last_is_reduce) - L << " bout[row*cols + col] = " << wr("s_red[" + std::to_string(slot_of_instr[spec.instrs.back().dst]) + "][tid]") << ";\n"; + emit_wr(" ", "row*cols + col", + "s_red[" + std::to_string(slot_of_instr[spec.instrs.back().dst]) + "][tid]"); else { emit_reg_decl(); emit_instrs(0, spec.instrs.size()); - L << " bout[row*cols + col] = " << wr("r" + std::to_string(static_cast(spec.instrs.back().dst))) << ";\n"; + emit_wr(" ", "row*cols + col", + "r" + std::to_string(static_cast(spec.instrs.back().dst))); } L << " }\n"; } diff --git a/include/neuralnet.cpp/expr_spec.hpp b/include/neuralnet.cpp/expr_spec.hpp index bb127d6e..845f8e71 100644 --- a/include/neuralnet.cpp/expr_spec.hpp +++ b/include/neuralnet.cpp/expr_spec.hpp @@ -720,6 +720,29 @@ using ExprPrecSig = std::uint32_t; [[nodiscard]] inline constexpr bool expr_prec_sig_is_f32(ExprPrecSig s) noexcept { return s == 0u; } +// ── 运行期精度分派(V1 形态)哨兵 ──────────────────────────────────────── +// 语义:**同一份 shader 处理任意 (输入精度位图, 输出精度)**——每个操作数 +// 声明 f32 / float16_t 双视图(binding i 与 n_inputs+i),加载/存储处按 +// push constant `uint prec`(逐位)走 uniform 分支选择。 +// +// 与"带类型变体"(sig = 真实位图)的区别: +// · 带类型变体:精度是**身份**(key 含 `#xxxx`),必须先知道有哪些签名 +// → 只能靠 dry-run 执行期发现 → 需要回填清单; +// · 运行期分派:精度是**参数**(PC `prec`),一个结构一份 shader 覆盖全部 +// 签名 → 不需要发现 → 构建期只需"结构"这一维(锚点已自动)。 +// +// 哨兵取值落在输入位(bit 0..15)与输出位(bit 16)之外 —— 因此既有谓词 +// expr_prec_sig_in_f16 / expr_prec_sig_out_f16 对它一律返回 false, +// 旧路径不会被误触发。 +inline constexpr ExprPrecSig EXPR_PREC_SIG_DISPATCH = 0x00020000u; + +[[nodiscard]] inline constexpr bool expr_prec_sig_is_dispatch(ExprPrecSig s) noexcept +{ return s == EXPR_PREC_SIG_DISPATCH; } + +// 运行期分派变体的 key 后缀(结构 key + "#x")。#x 不含 [0-9a-f] 之外的 +// 字符,且既有变体后缀是 "#%04x" / "#a",不会碰撞。 +inline constexpr const char* EXPR_PREC_DISPATCH_SUFFIX = "#x"; + // 第 i 个输入是否为 f16 / 输出是否为 f16 [[nodiscard]] inline constexpr bool expr_prec_sig_in_f16(ExprPrecSig s, std::size_t i) noexcept From 2b43846d5dad112da7060022869308559cff6da4 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 18:22:54 +0800 Subject: [PATCH 19/30] =?UTF-8?q?A1b-d:=20=E8=BF=90=E8=A1=8C=E6=9C=9F?= =?UTF-8?q?=E7=B2=BE=E5=BA=A6=E5=88=86=E6=B4=BE=E8=90=BD=E5=9C=B0=E2=80=94?= =?UTF-8?q?=E2=80=94=E7=AD=BE=E5=90=8D=E4=BB=8E=E3=80=8C=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E3=80=8D=E9=99=8D=E7=BA=A7=E4=B8=BA=E3=80=8C=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生成期(tools/fused_generate.hpp): · 每个结构无条件发 V1(键 key#x)= 双视图 + PC prec; · V2(键 key#a,native16)改由**结构谓词**判定生成(代入「全输入 f16 + 输出 f16」这一常量签名),不再依赖执行期发现的签名; · 删除 (结构,签名) 变体的发射与 run-only 回填(回填清单不再参与生成)。 运行期(compute_gpu_engine.hpp / compute_vk_backend.hpp): · find_prec_variant_ 改为:psig==0 → V0;全 f16 且输出 f16 → V2(若有); 否则 → V1。V1 对每个结构无条件存在 ⇒ **任何签名都命中,无 miss**; · run_fused_gpu 增 prec_sig 形参,分派变体写 PC 末尾的 prec 槽; · 分派变体描述符 = 2N 输入视图 + 2 输出视图(同一 VkBuffer 绑两次, shader 只解引用与真实元素类型一致的那一个); · supports_expr_precision_variant 与 eval_expr_impl 选路同源。 验收: · build 零告警;ctest 19/19; · CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变; · GPT f16 训练 NN_PREC_TRACE=1:[prec][miss]=0、边界 cast 归因表为空、 alu-hit 6670 —— f16 路径全程命中预生成 shader,零 cast 回退; · 注册表 84 结构 + 84 分派 + 35 native16 = 203 条(旧:84 + 86 变体)。 --- .../backend/compute_vk_backend.hpp | 62 ++++++++-- include/neuralnet.cpp/compute_gpu_engine.hpp | 90 +++++++++------ include/neuralnet.cpp/expr_glsl_gen.hpp | 32 +++--- tools/fused_generate.hpp | 108 ++++++++---------- 4 files changed, 170 insertions(+), 122 deletions(-) diff --git a/include/neuralnet.cpp/backend/compute_vk_backend.hpp b/include/neuralnet.cpp/backend/compute_vk_backend.hpp index cba9a060..bc393802 100644 --- a/include/neuralnet.cpp/backend/compute_vk_backend.hpp +++ b/include/neuralnet.cpp/backend/compute_vk_backend.hpp @@ -543,6 +543,10 @@ class GpuBackend std::unordered_map fused_rparam_counts_; // 每线程处理元素数(1=标量, 4=vec4;决定 dispatch 宽度缩放) std::unordered_map fused_vec_width_; + // 运行期精度分派变体(键后缀 "#x"):描述符 = 2N 输入视图 + 2 输出视图, + // 且 push constants 多一个 `prec` 槽(位于固定头末尾)。**创建侧与写入侧 + // 必须同源判定**(见 run_fused_gpu 的 pc_base 注释)。 + std::unordered_map fused_is_dispatch_; std::unique_ptr memory_pool_; // 第二阶段:瞬态/持久分池。batch 录制期(batch_mode_=true)创建的 @@ -1705,6 +1709,9 @@ class GpuBackend { if (!fs.spirv || fs.spirv_words == 0) continue; // 空 SPIR-V:跳过(构建配置缺失) + // 运行期精度分派变体(键 "#x"):输入/输出各双视图 + PC 多一个 prec 槽 + const bool is_disp = + std::string_view(fs.key).ends_with(nn::EXPR_PREC_DISPATCH_SUFFIX); // 精度变体(key 含 '#',Phase 2 in-kernel f16)需要 SSBO 16 位存储; // 设备未启用 → 跳过注册(运行时按 (key,sig) 查不到 → 回退边界 cast)。 if (std::string_view(fs.key).find('#') != std::string_view::npos && @@ -1716,8 +1723,12 @@ class GpuBackend if (std::string_view(fs.key).ends_with("#a") && !device_.has_shader_float16()) continue; + const std::uint32_t n_in = + static_cast(fs.spec.views.size()); + // 分派变体:输入 i 的 f32 视图 binding=i、f16 视图 binding=n_in+i; + // 输出 f32/f16 视图 binding=2*n_in / 2*n_in+1。 const std::uint32_t num_bindings = - static_cast(fs.spec.views.size()) + 1; // 输入 + 输出 + is_disp ? (2u * n_in + 2u) : (n_in + 1u); // 输入 + 输出 // 归约 kernel 的 push constants 多 uint rows + uint vector_out; // matmul 融合 kernel 多 uint rows + uint mm_k + uint mm_batch + uint mm_trans; // matmul+归约组合再多 uint mm_k + uint mm_batch + uint mm_trans(7 槽); @@ -1728,13 +1739,15 @@ class GpuBackend // 假 PASS,"全绿"验不出);改动 PC 形态时创建侧(本处)与写入侧 // (run_fused_gpu)必须同改); // 另加 fs.view_param_count 个运行时视图参数槽(RowMod/RotateHalf) - const std::uint32_t pc_base = + const std::uint32_t pc_base_fixed = (fs.spec.fold && fs.spec.fold->matmul) ? 7u // fold+mm 7 槽 : (fs.spec.fold) ? 5u : (fs.reduce_axis >= 0 && fs.has_matmul) ? 7u : (fs.reduce_axis >= 0) ? 4u : (fs.has_matmul) ? 6u : 2u; + // 分派变体在固定头末尾多一个 uint prec(生成器同位置声明) + const std::uint32_t pc_base = pc_base_fixed + (is_disp ? 1u : 0u); 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) + @@ -1759,6 +1772,7 @@ class GpuBackend fused_view_param_counts_.emplace(fs.key, fs.view_param_count); fused_rparam_counts_.emplace(fs.key, fs.rparam_count); fused_vec_width_.emplace(fs.key, fs.vec_width); + fused_is_dispatch_.emplace(fs.key, is_disp); } } #endif @@ -3347,7 +3361,10 @@ class GpuBackend // 输出存储精度(Phase 2 in-kernel f16):true → 分配 f16 字节布局的 // 输出缓冲(返回的 GpuTensor 仅是**占位标签**,调用方按此标志重贴 // GpuTensorF16 —— 见 GpuEngine::eval_expr)。 - bool out_f16 = false) + bool out_f16 = false, + // 运行期精度分派(键 "#x"):真实 (输入精度位图, 输出精度)。仅在 + // 分派变体上写入 PC `prec` 槽;其余变体忽略(其精度已是身份)。 + nn::ExprPrecSig prec_sig = 0) { if (!initialized_) return std::unexpected(Error{"GPU backend not initialized"}); @@ -3356,6 +3373,8 @@ class GpuBackend if (it == fused_pipelines_.end()) return std::unexpected(Error{"fused shader not registered: " + shader_name}); const VulkanPipeline& pipeline = it->second; + const bool is_disp = fused_is_dispatch_.count(shader_name) + ? fused_is_dispatch_.at(shader_name) : false; // 归约轴:-1=逐元素, 0=行归约, 1=列归约 const int raxis = fused_reduce_axis_.count(shader_name) ? fused_reduce_axis_.at(shader_name) : -1; @@ -3453,17 +3472,33 @@ class GpuBackend } GpuTensor output = *output_ptr; - // 2. 分配描述符集(N 输入 + 1 输出) + // 2. 分配描述符集(N 输入 + 1 输出;分派变体 = 2N + 2) auto ds_r = alloc_desc_set(pipeline.descriptor_layout()); if (!ds_r) return std::unexpected(ds_r.error()); VkDescriptorSet desc_set = *ds_r; - const std::size_t n_bindings = inputs.size() + 1; + const std::size_t n_in = inputs.size(); + const std::size_t n_bindings = is_disp ? (2 * n_in + 2) : (n_in + 1); std::vector buf_infos(n_bindings); std::vector writes(n_bindings); - for (std::size_t i = 0; i < inputs.size(); ++i) - buf_infos[i] = {inputs[i]->impl(), 0, VK_WHOLE_SIZE}; - buf_infos[inputs.size()] = {output.buffer().impl(), 0, VK_WHOLE_SIZE}; + if (is_disp) + { + // 同一 VkBuffer 绑两次(f32 视图 / f16 视图);shader 按 PC `prec` + // 只解引用与真实元素类型一致的那一个 → 另一个从不访问(只读无别名风险)。 + for (std::size_t i = 0; i < n_in; ++i) + { + buf_infos[i] = {inputs[i]->impl(), 0, VK_WHOLE_SIZE}; + buf_infos[n_in + i] = {inputs[i]->impl(), 0, VK_WHOLE_SIZE}; + } + buf_infos[2 * n_in] = {output.buffer().impl(), 0, VK_WHOLE_SIZE}; + buf_infos[2 * n_in + 1] = {output.buffer().impl(), 0, VK_WHOLE_SIZE}; + } + else + { + for (std::size_t i = 0; i < n_in; ++i) + buf_infos[i] = {inputs[i]->impl(), 0, VK_WHOLE_SIZE}; + buf_infos[n_in] = {output.buffer().impl(), 0, VK_WHOLE_SIZE}; + } for (std::size_t i = 0; i < n_bindings; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; @@ -3508,14 +3543,16 @@ class GpuBackend // 错一个 uint 即常量池整体后移 → shader 从错位处读常量,GPU 上带 // 常量的归约静默错值而 CPU 正常(实测把 select(cond,1,0) 的常量 // 读成垃圾)。 - const std::uint32_t pc_base = + const std::uint32_t pc_base_fixed = (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) ? 7u // …, mm_k, mm_batch, mm_trans : (raxis >= 0) ? 4u // count, cols, rows, vector_out : (has_mm) ? 6u // count, cols, rows, mm_k, mm_batch, mm_trans : 2u; // count, cols - const std::uint32_t pc_uints = pc_base; + // 分派变体(键 "#x")在固定头**末尾**多一个 uint prec:生成器同位置 + // 声明(各生成器的 PC 块都在固定槽之后、vp 之前发 `uint prec;`)。 + const std::uint32_t pc_uints = pc_base_fixed + (is_disp ? 1u : 0u); std::vector pc( (pc_uints + n_vp) * sizeof(std::uint32_t) + sizeof(Scalar) * consts.size() + sizeof(Scalar) * rparams.size()); @@ -3582,6 +3619,11 @@ class GpuBackend sizeof(std::uint32_t)); } } + // 运行期精度分派:把真实 (输入精度位图, 输出精度) 写进固定头末尾的 + // prec 槽——shader 据此在每个加载/存储点走 uniform 分支。 + if (is_disp) + std::memcpy(pc.data() + pc_base_fixed * sizeof(std::uint32_t), + &prec_sig, sizeof(prec_sig)); // 运行时视图参数(RowMod 周期 / RotateHalf 块大小),置于固定头之后、常量池之前 for (std::uint32_t i = 0; i < n_vp; ++i) std::memcpy(pc.data() + (pc_uints + i) * sizeof(std::uint32_t), diff --git a/include/neuralnet.cpp/compute_gpu_engine.hpp b/include/neuralnet.cpp/compute_gpu_engine.hpp index 6ca205a1..c9107a14 100644 --- a/include/neuralnet.cpp/compute_gpu_engine.hpp +++ b/include/neuralnet.cpp/compute_gpu_engine.hpp @@ -1370,21 +1370,32 @@ class GpuEngine final : public ComputeEngine // 3. psig == 0 → 全 f32 结构键(调用方处理)。 // 两变体并存 → 任何设备都有最优可用路径,无回归。 [[nodiscard]] const nn::fused::FusedShader* find_prec_variant_( - const std::string& key, nn::ExprPrecSig psig) const + const std::string& key, nn::ExprPrecSig psig, + std::span inputs) const { if (psig == 0) - return nn::fused::find_fused(key); - const std::string akey = nn::expr_prec_sig_alu_key(key, psig); - if (const auto* fs = nn::fused::find_fused(akey); - fs && backend_.has_fused_shader(akey)) + return nn::fused::find_fused(key); // V0:全 f32 结构键 + // V2(native16 原生 f16 算术):键 = key#a,**结构谓词判定**(生成期 + // 已知),运行时仅在"全输入 f16 且输出 f16"时才走它。 + const bool all_in_f16 = [&] { + for (std::size_t i = 0; i < inputs.size(); ++i) + if (!nn::expr_prec_sig_in_f16(psig, i)) return false; + return true; + }(); + if (all_in_f16 && nn::expr_prec_sig_out_f16(psig)) { - // 诊断(NN_PREC_TRACE=1):确认 native16 变体被实际命中 - //(而非静默落 f32 算术回退——测试全绿区分不了这两者) - if (nn::dsl::env_flag("NN_PREC_TRACE")) - std::fprintf(stderr, "[prec][alu-hit] %s\n", akey.c_str()); - return fs; + const std::string akey = key + "#a"; + if (const auto* fs = nn::fused::find_fused(akey); + fs && backend_.has_fused_shader(akey)) + { + if (nn::dsl::env_flag("NN_PREC_TRACE")) + std::fprintf(stderr, "[prec][alu-hit] %s\n", akey.c_str()); + return fs; + } } - return nn::fused::find_fused(nn::expr_prec_sig_key(key, psig)); + // V1(运行期精度分派):键 = key#x —— 一个 shader 覆盖任意签名, + // 因此**任何非零签名都必定命中**(不再有 miss / 边界 cast 回退)。 + return nn::fused::find_fused(key + nn::EXPR_PREC_DISPATCH_SUFFIX); } #endif // ══════════════════════════════════════════════════════════════════════ @@ -1405,14 +1416,16 @@ class GpuEngine final : public ComputeEngine // fold 精度变体(键 fkey#sig;native16 谓词排除 fold // → 只有 f32 算术变体,find_prec_variant_ 的 #a 探测自然落空) const nn::ExprPrecSig fpsig = nn::expr_prec_sig_of(inputs, P); - const nn::fused::FusedShader* ffs = find_prec_variant_(fkey, fpsig); + const nn::fused::FusedShader* ffs = find_prec_variant_(fkey, fpsig, inputs); if (ffs && backend_.has_fused_shader(ffs->key)) { auto fi_r = fused_buffers_(inputs); if (!fi_r) return std::unexpected(fi_r.error()); std::vector& gpu_inputs = fi_r->bufs; const auto fvp = nn::expr_spec_runtime_view_params(raw_spec); - const bool fout_f16 = nn::expr_prec_sig_out_f16(ffs->prec_sig); + const bool fdisp = nn::expr_prec_sig_is_dispatch(ffs->prec_sig); + const bool fout_f16 = fdisp ? nn::expr_prec_sig_out_f16(fpsig) + : nn::expr_prec_sig_out_f16(ffs->prec_sig); auto out = backend_.run_fused_gpu( ffs->key, gpu_inputs, raw_spec.consts, rows, cols, /*vector_out=*/false, fvp, raw_spec.rparams, @@ -1422,7 +1435,7 @@ class GpuEngine final : public ComputeEngine 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); + nn::expr_spec_runtime_fold_k(raw_spec), fout_f16, fpsig); if (!out) return std::unexpected(out.error()); // f16 输出:按 2B/元素分配后重贴 GpuTensorF16(同非 fold 路径) if (fout_f16) @@ -1447,7 +1460,7 @@ class GpuEngine final : public ComputeEngine // 半精度读/写,无需边界 cast;未命中则退回全 f32 key —— 此时输入必须 // 已全 f32(由基类 NVI 入口 cast)。 const nn::ExprPrecSig psig = nn::expr_prec_sig_of(inputs, P); - const nn::fused::FusedShader* fs = find_prec_variant_(key, psig); + const nn::fused::FusedShader* fs = find_prec_variant_(key, psig, inputs); if (fs && backend_.has_fused_shader(fs->key)) { // 命中:收集 GPU 输入(同一 buffer 可重复绑定,如 RoPE 的 q×2) @@ -1462,14 +1475,17 @@ class GpuEngine final : public ComputeEngine // 否则 shader 只写前半、返回的却是 f32 标签 → 下游全是垃圾(实测 // batch32 训练 loss=NaN)。缓冲布局由 out_f16 决定,返回的 GpuTensor // 只是占位标签,这里按实际精度重贴 GpuTensorF16。 - const bool out_f16 = nn::expr_prec_sig_out_f16(fs->prec_sig); + // 输出存储精度:分派变体按**运行期真实签名**(bit16),其余按变体身份 + const bool disp = nn::expr_prec_sig_is_dispatch(fs->prec_sig); + const bool out_f16 = disp ? nn::expr_prec_sig_out_f16(psig) + : nn::expr_prec_sig_out_f16(fs->prec_sig); auto out = backend_.run_fused_gpu( fs->key, gpu_inputs, spec.consts, rows, cols, /*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); + /*fold_k=*/std::nullopt, out_f16, psig); if (!out) return std::unexpected(out.error()); if (out_f16) return Tensor::from_gpu(GpuTensorF16(out->shared_buffer(), rows, cols)); @@ -1512,23 +1528,18 @@ class GpuEngine final : public ComputeEngine Precision P = Precision::F32) const override { #ifdef NN_FUSED_REGISTRY_EMBEDDED - if (raw_spec.fold) - { - // Phase D2:fold 带类型变体已由 gen_fused 生成(键 fkey#sig)→ - // 按真实签名查询(native16 谓词排除 fold,#a 探测自然落空)。 - const nn::ExprPrecSig psig = nn::expr_prec_sig_of(inputs, P); - if (psig == 0) - return false; // 全 f32:无带类型变体(适配层 all_f32 直通) - const std::string k = nn::expr_prec_sig_key( - nn::expr_spec_key(raw_spec), psig); - return nn::fused::find_fused(k) != nullptr && backend_.has_fused_shader(k); - } + // psig == 0(全 f32):基类快路径直通,无需变体。 + // psig != 0:选路与 eval_expr_impl 完全同源(V2 native16 优先 → + // V1 运行期分派)——V1 对每个结构无条件生成,故**任何签名都命中**, + // 基类不再有"边界 cast 回退"这条路径。 const nn::ExprPrecSig psig = nn::expr_prec_sig_of(inputs, P); if (psig == 0) return false; - const std::string k = nn::expr_prec_sig_key( - nn::expr_spec_key(nn::canonicalize_expr_spec(raw_spec)), psig); - return nn::fused::find_fused(k) != nullptr && backend_.has_fused_shader(k); + const std::string key = raw_spec.fold + ? nn::expr_spec_key(raw_spec) + : nn::expr_spec_key(nn::canonicalize_expr_spec(raw_spec)); + const auto* fs = find_prec_variant_(key, psig, inputs); + return fs != nullptr && backend_.has_fused_shader(fs->key); #else (void)raw_spec; (void)inputs; (void)P; return false; @@ -1575,22 +1586,24 @@ class GpuEngine final : public ComputeEngine // 精度变体优先(Phase 2 in-kernel f16 + Phase 3 native16):与 eval_expr // 同款 (key,sig) 匹配;native16 ALU 变体优先(find_prec_variant_)—— // 归约形态 native16 谓词恒 false,自动落 f32 算术变体 - const nn::fused::FusedShader* fs = (psig != 0) ? find_prec_variant_(key, psig) - : nn::fused::find_fused(key); + const nn::fused::FusedShader* fs = (psig != 0) + ? find_prec_variant_(key, psig, inputs) : nn::fused::find_fused(key); if (fs && backend_.has_fused_shader(fs->key)) { auto fi_r = fused_buffers_(inputs); if (!fi_r) return std::unexpected(fi_r.error()); std::vector& gpu_inputs = fi_r->bufs; // owners 随 fi_r 存活到本作用域末 const auto vp = nn::expr_spec_runtime_view_params(spec); - const bool out_f16 = nn::expr_prec_sig_out_f16(fs->prec_sig); + const bool rdisp = nn::expr_prec_sig_is_dispatch(fs->prec_sig); + const bool out_f16 = rdisp ? nn::expr_prec_sig_out_f16(psig) + : nn::expr_prec_sig_out_f16(fs->prec_sig); auto out = backend_.run_fused_gpu( 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), nn::expr_spec_runtime_matmul_trans(spec), - std::nullopt, out_f16); + std::nullopt, out_f16, psig); if (!out) return std::unexpected(out.error()); if (out_f16) { @@ -1641,8 +1654,8 @@ class GpuEngine final : public ComputeEngine nn::expr_prec_sig_of(inputs, dst.precision()); #ifdef NN_FUSED_REGISTRY_EMBEDDED // native16 ALU 变体优先,f32 算术变体回退(find_prec_variant_) - const nn::fused::FusedShader* fs = (psig != 0) ? find_prec_variant_(key, psig) - : nn::fused::find_fused(key); + const nn::fused::FusedShader* fs = (psig != 0) + ? find_prec_variant_(key, psig, inputs) : nn::fused::find_fused(key); if (fs && backend_.has_fused_shader(fs->key)) { auto fi_r = fused_buffers_(inputs); @@ -1682,7 +1695,8 @@ class GpuEngine final : public ComputeEngine spec.rparams, dst_override, nn::expr_spec_runtime_matmul_k(spec), nn::expr_spec_runtime_matmul_batch(spec), - nn::expr_spec_runtime_matmul_trans(spec)); + nn::expr_spec_runtime_matmul_trans(spec), + /*fold_k=*/std::nullopt, /*out_f16=*/false, psig); 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 f8b1ec50..3128dc46 100644 --- a/include/neuralnet.cpp/expr_glsl_gen.hpp +++ b/include/neuralnet.cpp/expr_glsl_gen.hpp @@ -138,7 +138,7 @@ inline std::string glsl_view_read_(const ExprView& v, std::uint32_t buf_id, const std::string lb = "b" + std::to_string(v.param); std::string lab; if (disp) - lab = "((pc.prec >> " + std::to_string(v.param) + "u & 1u) != 0u ? uint(float(" + lab = "((prec >> " + std::to_string(v.param) + "u & 1u) != 0u ? uint(float(" + lb + "h[" + col_var + "])) : uint(" + lb + "[" + col_var + "]))"; else if (expr_prec_sig_in_f16(sig, v.param)) lab = "uint(float(" + lb + "[" + col_var + "]))"; @@ -185,7 +185,7 @@ inline void glsl_view_read(std::ostringstream& os, const std::string hbuf = buf + "h"; const std::string f16e = glsl_view_read_(v, buf_id, hbuf, true, idx_var, row_var, col_var, vp_slot, sig); - os << "((pc.prec >> " << buf_id << "u & 1u) != 0u ? float(" << f16e + os << "((prec >> " << buf_id << "u & 1u) != 0u ? float(" << f16e << ") : " << f32e << ")"; } @@ -223,7 +223,7 @@ inline void glsl_emit_grouped_reduce_helpers(std::ostringstream& os, const auto elem = [&](const std::string& tt) -> std::string { const std::string idx = "((row * R + " + tt + ") * cols + col)"; if (disp) - return "((pc.prec >> " + std::to_string(i) + "u & 1u) != 0u ? float(" + return "((prec >> " + std::to_string(i) + "u & 1u) != 0u ? float(" + buf + "h[" + idx + "]) : " + buf + "[" + idx + "])"; return f16_buf ? ("float(" + buf + "[" + idx + "])") : (buf + "[" + idx + "]"); @@ -429,7 +429,7 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) const auto vec4_load = [&](std::uint32_t slot, const std::string& idx) -> std::string { const std::string arr = "b" + std::to_string(slot) + "v4[" + idx + "]"; if (disp) - return "((pc.prec >> " + std::to_string(slot) + "u & 1u) != 0u ? vec4(" + return "((prec >> " + std::to_string(slot) + "u & 1u) != 0u ? vec4(" "unpackHalf2x16(b" + std::to_string(slot) + "v4h[" + idx + "].x), unpackHalf2x16(b" + std::to_string(slot) + "v4h[" + idx + "].y)) : " + arr + ")"; @@ -481,7 +481,7 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) : "((batch*m_per + (" + row_e + "))*mm_k + (" + k_e + "))"; const std::string an = "b" + std::to_string(a_slot); if (disp) - return "((pc.prec >> " + std::to_string(a_slot) + "u & 1u) != 0u ? float(" + return "((prec >> " + std::to_string(a_slot) + "u & 1u) != 0u ? float(" + an + "h[" + idx + "]) : " + an + "[" + idx + "])"; const std::string raw = an + "[" + idx + "]"; return a_f16 ? ("float(" + raw + ")") : raw; @@ -494,7 +494,7 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) : "((batch*mm_k + (" + k_e + "))*cols + (" + col_e + "))"; const std::string bn = "b" + std::to_string(b_slot); if (disp) - return "((pc.prec >> " + std::to_string(b_slot) + "u & 1u) != 0u ? float(" + return "((prec >> " + std::to_string(b_slot) + "u & 1u) != 0u ? float(" + bn + "h[" + idx + "]) : " + bn + "[" + idx + "])"; const std::string raw = bn + "[" + idx + "]"; return b_f16 ? ("float(" + raw + ")") : raw; @@ -787,7 +787,7 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) { L << " {\n"; L << " const uint oi = (batch * m_per + rr) * cols + cc;\n"; - L << " if ((pc.prec >> 16u & 1u) != 0u)\n"; + L << " if ((prec >> 16u & 1u) != 0u)\n"; L << " bout16[oi] = float16_t(eval_tail(acc[i][j], rr, cc, batch));\n"; L << " else\n"; L << " bout[oi] = eval_tail(acc[i][j], rr, cc, batch);\n"; @@ -1051,7 +1051,7 @@ inline std::string generate_glsl_fold(const std::string& name, const ExprSpec& s } if (disp) { - L << " if ((pc.prec >> 16u & 1u) != 0u)\n"; + L << " if ((prec >> 16u & 1u) != 0u)\n"; L << " bout16[row] = float16_t(r" << static_cast(f.finalize.back().dst) << ");\n"; L << " else\n"; @@ -1127,7 +1127,7 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec const std::size_t p = e16.find(fn); if (p != std::string::npos) e16.replace(p, fn.size(), hn); - return "((pc.prec >> " + std::to_string(i) + "u & 1u) != 0u ? float(" + return "((prec >> " + std::to_string(i) + "u & 1u) != 0u ? float(" + e16 + ") : " + e + ")"; } return (sig != 0 && expr_prec_sig_in_f16(sig, i)) ? ("float(" + e + ")") : e; @@ -1562,7 +1562,7 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec if (disp) { L << " if (row_ok) {\n"; - L << " if ((pc.prec >> 16u & 1u) != 0u)\n"; + L << " if ((prec >> 16u & 1u) != 0u)\n"; L << " bout16[row * vector_out + dd] = float16_t(r" << static_cast(f.finalize.back().dst) << ");\n"; L << " else\n"; @@ -1863,7 +1863,7 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << " bout[i] = r" << last_dst << ";\n"; else if (disp) { - L << " if ((pc.prec >> 16u & 1u) != 0u) {\n"; + L << " if ((prec >> 16u & 1u) != 0u) {\n"; L << " bout16[i] = float16_t(r" << last_dst << ");\n"; L << " } else {\n"; L << " bout[i] = r" << last_dst << ";\n"; @@ -1901,7 +1901,7 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, const auto ld = [&](const std::string& idx) { const std::string bf = "b" + std::to_string(i) + "[" + idx + "]"; if (disp) - return "((pc.prec >> " + std::to_string(i) + "u & 1u) != 0u ? float(b" + return "((prec >> " + std::to_string(i) + "u & 1u) != 0u ? float(b" + std::to_string(i) + "h[" + idx + "]) : " + bf + ")"; return cvt.empty() ? bf : ("float(" + bf + ")"); }; @@ -2016,7 +2016,7 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, }; if (disp) { - L << " if ((pc.prec >> 16u & 1u) != 0u) {\n"; + L << " if ((prec >> 16u & 1u) != 0u) {\n"; emit_store4("bout16", true, " "); L << " } else {\n"; emit_store4("bout", false, " "); @@ -2042,7 +2042,7 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, const std::string sv = "r" + std::to_string(last_dst); if (disp) { - L << " if ((pc.prec >> 16u & 1u) != 0u) {\n"; + L << " if ((prec >> 16u & 1u) != 0u) {\n"; L << " bout16[e] = float16_t(" << sv << ");\n"; L << " } else {\n"; L << " bout[e] = " << sv << ";\n"; @@ -2120,7 +2120,7 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, const auto rd = [&](std::size_t k, const std::string& idx) -> std::string { const std::string raw = "b" + std::to_string(k) + "[" + idx + "]"; if (disp) - return "((pc.prec >> " + std::to_string(k) + "u & 1u) != 0u ? float(b" + return "((prec >> " + std::to_string(k) + "u & 1u) != 0u ? float(b" + std::to_string(k) + "h[" + idx + "]) : " + raw + ")"; return (sign_f16 && expr_prec_sig_in_f16(sig, k)) ? ("float(" + raw + ")") : raw; }; @@ -2200,7 +2200,7 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << ind << "bout[" << lhs << "] = " << wr(v) << ";\n"; return; } - L << ind << "if ((pc.prec >> 16u & 1u) != 0u)\n"; + L << ind << "if ((prec >> 16u & 1u) != 0u)\n"; L << ind << " bout16[" << lhs << "] = float16_t(" << v << ");\n"; L << ind << "else\n"; L << ind << " bout[" << lhs << "] = " << v << ";\n"; diff --git a/tools/fused_generate.hpp b/tools/fused_generate.hpp index 88eee4e0..426ff65c 100644 --- a/tools/fused_generate.hpp +++ b/tools/fused_generate.hpp @@ -189,6 +189,20 @@ namespace nn::tool std::error_code ec; std::filesystem::create_directories(out_dir, ec); + // A/B 逃生阀:跳过运行期精度分派变体(键 #x)的发射。 + const bool nn_no_dispatch = [] { +#if defined(_MSC_VER) + char* b = nullptr; std::size_t n = 0; + _dupenv_s(&b, &n, "NN_SCAN_NO_DISPATCH"); + const bool v = (b != nullptr && b[0] != '\0' && b[0] != '0'); + std::free(b); + return v; +#else + const char* v = std::getenv("NN_SCAN_NO_DISPATCH"); + return v != nullptr && v[0] != '\0' && v[0] != '0'; +#endif + }(); + if (reg.specs.empty()) { std::fprintf(stderr, "[FAIL] 表达式集合为空(scan_exprs 未覆盖任何路径)\n"); @@ -325,11 +339,18 @@ namespace nn::tool if (alu && !nn::expr_prec_sig_native16(spec, sig)) return false; // 谓词不通过 → 不发 ALU 变体(f32 算术变体仍覆盖) const std::string key = nn::expr_spec_key(spec); + // 运行期精度分派(V1):键 = key#x(无签名 hex——精度不再是身份) + const bool dispv = nn::expr_prec_sig_is_dispatch(sig); char sigbuf[16]; std::snprintf(sigbuf, sizeof(sigbuf), "%04x", static_cast(sig)); - const std::string vkey = alu ? nn::expr_prec_sig_alu_key(key, sig) - : nn::expr_prec_sig_key(key, sig); - const std::string suffix = key + "_" + sigbuf + (alu ? "_a" : ""); + const std::string vkey = dispv + ? (key + nn::EXPR_PREC_DISPATCH_SUFFIX) + : (alu ? (key + "#a") // native16:结构谓词判定 → 键与签名无关 + : nn::expr_prec_sig_key(key, sig)); + const std::string suffix = dispv + ? (key + "_x") + : (alu ? (key + "_a") + : (key + "_" + sigbuf)); if (!emitted_vkeys.insert(vkey).second) return false; // 已发射(scan 预测 ∩ 回填清单重叠)→ 幂等跳过 const std::string comp_path = out_dir + "/fused_" + suffix + ".comp"; @@ -377,64 +398,35 @@ namespace nn::tool emitted_variants.push_back(VariantEmit{&spec, sig, vkey, suffix, alu}); return true; }; - for (const auto& v : reg.variants) - { - emit_one(v.spec, v.sig, /*alu=*/false); - emit_one(v.spec, v.sig, /*alu=*/true); // 谓词不通过时内部静默跳过 - } - // ── run-only 签名回填──────────────────────────────────────── - // scan 的 dry-run 预测不到的运行时签名(Linear matmul 段混合签名 / Norm - // 归约链 / 全 f32 入 + f16 出等),miss 时适配层走边界 cast 物化输入副本 - // (归因大头 (64,8192)/(256,8192) 即此)。清单 = 实测 [prec][miss] 输出 - // (tools/prec_backfill.txt,NN_PREC_TRACE=1 mem_probe 复现),对命中的 - // (结构, 签名) 走与 scan 变体完全相同的 emit_one 发射(含 ALU 谓词、 - // 重复去重);结构不在 bin 中 / 生成器不支持时告警跳过(不失败)。 - if (!prec_manifest.empty()) + // ── 运行期精度分派变体(V1,键 = key#x)────────────────────────────── + // **每个结构无条件发射一份**:输入/输出各声明 f32 + float16_t 双视图, + // 加载/存储处按 push constant `uint prec` 走 uniform 分支 → 一个 shader + // 覆盖全部 (输入精度位图, 输出精度)。 + // + // 这是"签名不再需要构建期枚举"的载体:运行时按真实张量精度填 prec, + // 任何签名都有 shader → **永不 miss**,因此 dry-run / 模型 pass / + // tools/prec_backfill.txt 全部不再参与生成(A2 起从 scan 移除)。 + // + // ── native16 变体(V2,键 = key#a)───────────────────────────────── + // 生成条件 = **结构谓词**(expr_prec_sig_native16 代入"全输入 f16 + + // 输出 f16"这一结构性常量签名):无 fold / 无 matmul 段 / 无归约 / + // 无 rparams / 无常量微值。运行时仅在真实签名确为全 f16 时才选它 → + // 生成期不需要知道签名,运行期也不需要发现。 + // + // A/B 逃生阀:NN_SCAN_NO_DISPATCH=1 时不发射 V1(退回旧集合); + // 只影响登记集合,不影响语义。 + for (const auto& spec : reg.specs) { - const std::string& manifest = prec_manifest; - std::ifstream mf(manifest); - if (!mf) - { - std::fprintf(stderr, "[gen][backfill] 清单不可读,跳过回填: %s\n", - manifest.c_str()); - } - else - { - std::unordered_map spec_by_key; - for (const auto& s : reg.specs) - spec_by_key.emplace(nn::expr_spec_key(s), &s); - std::string line; - std::size_t n_ok = 0, n_total = 0; - while (std::getline(mf, line)) - { - std::istringstream ln(line); - std::string k; - unsigned sig = 0; - if (!(ln >> k) || k.front() == '#') - continue; // 空行 / 注释 - if (!(ln >> std::hex >> sig)) - { - std::fprintf(stderr, "[gen][backfill] 无法解析行: %s\n", line.c_str()); - continue; - } - ++n_total; - const auto it = spec_by_key.find(k); - if (it == spec_by_key.end()) - { - std::fprintf(stderr, "[gen][backfill] 结构不在 bin 中(scan 未覆盖?): %s\n", - k.c_str()); - continue; - } - const nn::ExprPrecSig psig = static_cast(sig); - if (emit_one(*it->second, psig, /*alu=*/false)) - ++n_ok; - emit_one(*it->second, psig, /*alu=*/true); // 谓词不通过静默跳过 - } - std::printf("[gen][backfill] 回填清单 %s: %zu/%zu 条命中发射\n", - manifest.c_str(), n_ok, n_total); - } + if (!nn_no_dispatch) + emit_one(spec, nn::EXPR_PREC_SIG_DISPATCH, /*alu=*/false); + const std::size_t n_in = spec.views.size(); + const std::uint32_t all_in_bits = + (n_in >= 32) ? 0u : ((1u << n_in) - 1u); + emit_one(spec, nn::expr_prec_sig_make(all_in_bits, /*out_f16=*/true), + /*alu=*/true); // 谓词不通过时内部静默跳过 } + (void)prec_manifest; // 签名不再回填(V1 覆盖任意签名) H << "inline const FusedShader kFusedShaders[] = {\n"; for (const auto& spec : reg.specs) From 6c9e091f5e46fb35266789203948fd573e4e4755 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 18:34:40 +0800 Subject: [PATCH 20/30] =?UTF-8?q?A1e:=20=E5=88=A0=E9=99=A4=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E5=9B=9E=E5=A1=AB=E5=B7=A5=E4=BD=9C=E6=B5=81=E4=B8=8E?= =?UTF-8?q?=20f16=20=E6=89=AB=E4=B8=A4=E9=81=8D=EF=BC=88=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E9=87=8F=E5=87=8F=E5=8D=8A=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit · tools/prec_backfill.txt 删除:生成阶段不再消费回填清单 (CMake 的 COMMAND/DEPENDS 同步去掉); · scan_exprs 用法去掉 [] 形参; · f16 dry-run 遍 + f16 模型 pass 删除——**实测其对结构贡献为 0**: 跳过 f16 遍后签名 66 → 0、结构恒 84(NN_SCAN_F32_ONLY 探针); 该两遍存在的唯一理由是发现精度签名,而签名已不是构建期集合; · scan_exprs 末尾的"没有带类型变体 / 用 NN_PREC_TRACE 回填"提示改为 结构侧诊断(该工作流已不存在)。 验收: · build 零告警;ctest 19/19; · 注册表仍 84 结构 + 84 分派 + 35 native16 = 203 条(与 A1b-d 逐字节同构); · CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变。 --- CMakeLists.txt | 2 -- tools/prec_backfill.txt | 59 -------------------------------- tools/scan_exprs.cpp | 76 ++++++++++++++--------------------------- 3 files changed, 25 insertions(+), 112 deletions(-) delete mode 100644 tools/prec_backfill.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 6aea3c0f..a331851c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -486,9 +486,7 @@ if(Vulkan_FOUND) # 全新构建时 generated/ 尚未存在会写入失败(见 08-pitfalls)。 COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/generated COMMAND scan_exprs ${CMAKE_BINARY_DIR}/generated ${GLSLC} - ${CMAKE_SOURCE_DIR}/tools/prec_backfill.txt DEPENDS scan_exprs ${SCAN_SRC} ${FUSED_GEN_SRC} - ${CMAKE_SOURCE_DIR}/tools/prec_backfill.txt COMMENT "[GPU] Scan + synthesize fused shaders (scan_exprs) → fused_registry.hpp" ) diff --git a/tools/prec_backfill.txt b/tools/prec_backfill.txt deleted file mode 100644 index 03ba74cb..00000000 --- a/tools/prec_backfill.txt +++ /dev/null @@ -1,59 +0,0 @@ -# gen_fused 精度变体回填清单(Phase D3:run-only 签名回填) -# -# scan_exprs 的 dry-run 预测不到的**运行时签名**(如 Linear matmul 段的 -# in=[f16,f32,f32] 混合签名、Norm 归约链的 [f32,f16] 输入)——这类结构只有 -# 真实训练跑起来才暴露,miss 时适配层走边界 cast(to_f32_all 物化输入副本)。 -# -# 行格式: (空白分隔;# 起始为注释) -# 来源: NN_PREC_TRACE=1 mem_probe 的 [prec][miss] 输出 -# 机制: gen_fused 读本清单,对命中的 (结构, 签名) 额外发射变体 -# (键 key#sig,谓词通过时另发 key#sig#a)——与 scan 预测的变体 -# 同一代码路径,重复条目自动去重。 -# -# 维护:miss 复现 → 追加一行 → 重跑构建即可;生成后不再需要的行可留着 -# (幂等去重,不影响产物)。 - -# Linear matmul 段 run-only 混合签名(miss 时 (64,8192)/(256,8192) 物化大头) -ad5a483ea593210a 10001 -ad5a483ea593210a 10005 - -# Norm 归约链(raxis=1/含归约逐元素):输入 f16 统计量、输出 f32 -e21f87a71850cc6d 0002 -c0006fc8ea6a0e6b 0002 -237bfb5805bbf52c 0002 - -# 逐元素:全 f32 输入、f16 输出(scan 预测不到的 runtime-only 签名) -79f355d6f8d1d247 10000 - -# 注意力 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 - -# ── 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 - -# ── 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 diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index 0f3eba15..35e069a0 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -80,15 +80,15 @@ int main(int argc, char* argv[]) if (argc < 3) { std::fprintf(stderr, - "用法: scan_exprs [] " - "[--list-backends]\n" - " 收集 Layer 内联表达式(锚点自登记 + dry-run + 模型 pass),\n" - " 原地合成 /fused_registry.hpp(不经 .bin 中间文件)。\n"); + "用法: scan_exprs [--list-backends]\n" + " 收集 Layer 内联表达式的**结构**(锚点自登记 + dry-run + 模型 pass),\n" + " 原地合成 /fused_registry.hpp(不经 .bin 中间文件)。\n" + " 精度签名不再是构建期集合:生成阶段对每个结构发一份运行期\n" + " 精度分派 shader(键 key#x),运行时按真实精度填 PC `prec`。\n"); return 2; } 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; @@ -383,8 +383,11 @@ 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:收集 (结构, 精度签名); + // ⚠ 只跑 f32 一遍:**实测 f16 遍对结构贡献为 0**(NN_SCAN_F32_ONLY 探针: + // 跳过 f16 遍后签名 66 → 0,结构恒 84)。f16 遍存在的唯一理由是发现精度 + // 签名,而签名已不再是构建期集合(生成阶段对每个结构发运行期分派 shader, + // 键 key#x,运行期按真实精度填 PC `prec`)→ 该遍删除,扫描工作量减半。 + dry_run(raw_engine, nn::profile_f32()); // 结构(与精度无关) #endif // 分项台账快照(末尾合并时打印;见"结构来源"行) const std::size_t n_after_dry = nn::fused::global_registry().specs.size(); @@ -524,8 +527,8 @@ int main(int argc, char* argv[]) [&] { return nn::build_rapt_model(engine, cfg); }, ids); } }; - model_pass(raw_engine, nn::profile_f32()); // 结构 + 全 f32 签名 - model_pass(raw_engine, nn::profile_f16()); // 整模型路径的 f16 签名 + // 模型 pass:**结构**来源(运行期配置相关的路径),与精度无关 → 只跑 f32。 + model_pass(raw_engine, nn::profile_f32()); 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(标量域)分块状态归约(表达式集合登记)────────────────────────── @@ -611,51 +614,22 @@ int main(int argc, char* argv[]) "或所有调用点都已被 dry-run 覆盖\n"); } std::printf("[scan] 收集到 %zu 条融合表达式\n", reg.specs.size()); - // ── 签名覆盖报告:哪些结构**没有任何带类型变体** ────────────────────── - // 自登记只能给结构(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 段)。 + // ── 签名覆盖:**不再需要背书** ──────────────────────────────────────── + // 生成阶段对每个结构无条件发一份运行期精度分派 shader(键 key#x)—— + // 输入/输出双视图 + PC `prec`,一份覆盖任意 (输入精度位图, 输出精度)。 + // 因此: + // · 结构 = 唯一需要"收集"的东西(锚点 + dry-run/模型 pass); + // · 签名**不再是构建期集合**,无需 dry-run 的 f16 遍、无需回填清单, + // 运行期也不存在 miss / 边界 cast 回退。 + // 下面只保留结构侧的诊断(dry-run 的 profile 遍仍会走 Layer,用于暴露 + // 运行期配置相关的结构,与签名无关)。 if (!reg.variants.empty()) - { - std::size_t plain = 0, with_mm = 0, with_fold = 0, out_f16 = 0; - for (const auto& v : reg.variants) - { - if (v.spec.fold) ++with_fold; - else if (v.spec.matmul) ++with_mm; - else ++plain; - if (nn::expr_prec_sig_out_f16(v.sig)) ++out_f16; - } - std::printf("[scan] 精度变体 %zu 条(纯逐元素 %zu / 含 matmul 段 %zu / fold %zu;" - "输出 f16 者 %zu)\n", - reg.variants.size(), plain, with_mm, with_fold, out_f16); - } - else - { - std::printf("[scan] 精度变体 0 条(f16 pass 未产生非零签名)\n"); - } + std::printf("[scan] 精度变体 %zu 条(历史数据;生成阶段已不再消费——" + "分派 shader 覆盖全部签名)\n", + reg.variants.size()); // ── 生成阶段(原 gen_fused):同一进程内直接消费注册表 → fused_registry.hpp // 构建期因此只有一步;不再经 expr_specs.bin 中间序列化。 - if (!nn::tool::generate_fused_registry(out_dir, glslc, reg, manifest)) + if (!nn::tool::generate_fused_registry(out_dir, glslc, reg)) return 1; return 0; } From 94d5c91251088e04d9ce90ee7090cf7aeae53539 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 18:42:59 +0800 Subject: [PATCH 21/30] =?UTF-8?q?A3:=20=E6=96=87=E6=A1=A3=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E3=80=8C=E7=AD=BE=E5=90=8D=E4=BB=8E=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E9=99=8D=E7=BA=A7=E4=B8=BA=E5=8F=82=E6=95=B0=E3=80=8D+=20?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E5=BD=92=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit · AGENTS.md §7:构建期单步改为「只收集结构;每个结构发 V0/V1/V2 三份」, 新增两节——「精度签名不再是构建期集合」与「新增调用点无需改动其他文件」 (含端到端实测数字);§12 约束 4 补「精度签名同样不进 key」。 · docs/development/02:新增 §精度签名已不是构建期集合(A1),改写收集步骤、 自登记锚点(只登记结构 / 模型 pass 只跑 f32)、常驻台账、回填条目改为"已删除"。 · docs/development/05 §12.10 ⑤:签名回填流程 → 运行期精度分派;§12.12 待办改写。 · docs/introduction/01:「AOT 收集原则」段同步。 · docs/usage/03:NN_PREC_TRACE 行与闭合世界排错行同步。 · docs/history.md:新增归档条目(旧机制、删除清单、A vs B 性能 A/B、 实测验收、以及"结构侧只能靠 dry-run/模型 pass 提供 71/84"这一关键约束)。 验收:doc_align_audit [A]/[D]/[E]/[F] 可行动 0;doc_inventory L2-VIOLATIONS 0、 virtual methods 49;build 零告警(本次仅文档变更);注册表仍 203 条 / 84 结构。 --- AGENTS.md | 8 +++-- docs/development/02-operator-fusion.md | 45 ++++++++++++++++++-------- docs/development/05-mixed-precision.md | 11 +++++-- docs/history.md | 10 ++++++ docs/introduction/01-architecture.md | 2 +- docs/usage/03-compute-engine-usage.md | 4 +-- 6 files changed, 58 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b7810a45..c6ba9826 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -178,8 +178,10 @@ GPT 序列展平: 列序 i = b*seq + t(batch-major,全局唯一约定) - 主要入口:`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 内联表达式后重跑构建即可)——`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` 已删除)。 + 1. **收集**:只收集**结构**(= 唯一需要构建期枚举的东西)。`FusedAnchor` 在静态初始化期按表达式**类型**自登记(编译期可达,dry-run 跑不到的分支也覆盖),再由 per-layer dry-run + 模型 pass 执行补齐"运行期配置相关"的结构;fold spec 与 matmul trans 族由 `scan_exprs` 显式登记。**只跑 f32 一遍**——实测 f16 遍对结构贡献为 0(跳过它签名 66 → 0、结构恒 84),该两遍已删除。 + 2. **生成**:`tools/fused_generate.hpp`(原独立工具 `gen_fused`)对**每个结构发三份**:V0 全 f32(键 = `key`)/ **V1 运行期精度分派**(键 = `key#x`)/ V2 native16(键 = `key#a`,按 `expr_prec_sig_native16` 的**结构谓词**判定)→ 经 `emitter_registry` 选后端(默认 `"glsl"` = `GlslEmitter`)生成 GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。**不经 `.bin` 中间序列化**(`write_registry/read_registry` 已删除)。 +- **精度签名不再是构建期集合**("签名从身份降级为参数"):V1 的 GLSL 对每个输入/输出各声明 f32 + `float16_t` 双视图,加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支,**一份 shader 覆盖任意签名**。因此运行期**不存在 miss、不存在边界 cast 回退、不存在回填清单**(`tools/prec_backfill.txt` 已删除,`NN_PREC_TRACE` 的"补变体"流程随之失效)。V1 对每个结构无条件生成 ⇒ 任何签名都命中。 +- **新增调用点无需改动其他文件**(实测):在 Layer 头里新写一个 `dsl::compute`(其结构不在任何 dry-run / 模型 pass 覆盖内)→ 锚点独有 13 → 14、结构 84 → 85、注册表新增 `key` / `key#x` / `key#a` 三条,**其他文件零改动**。 - **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`)。 @@ -318,7 +320,7 @@ 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. **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`。 +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**:`key#x` 是"运行期精度分派"结构变体(双视图 + PC `prec`,覆盖任意签名),`key#a` 是 native16 结构变体,两者都不带签名 hex(旧的 `key#[#a]` 形式已随签名降级为参数而消失)。反之**属于结构、必须进 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)、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。 diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index 9b51555b..bfc0bb56 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -158,7 +158,7 @@ struct MatmulSpec { ## 表达式录制与融合边界 -本文的"表达式录制"指**构建期收集**:`scan_exprs` 收集各 Layer 折叠出的 `ExprSpec`——**结构**由 `FusedAnchor` 在静态初始化期按表达式类型自登记(编译期可达),**精度签名**由 dry-run / 模型 pass 执行产生(见 §构建工具链与闭合世界与 §自登记锚点)——这是库里唯一的表达式收集机制。 +本文的"表达式录制"指**构建期收集**:`scan_exprs` 收集各 Layer 折叠出的 `ExprSpec` **结构**——由 `FusedAnchor` 在静态初始化期按表达式类型自登记(编译期可达),再由 dry-run / 模型 pass 补齐运行期配置相关的结构(见 §构建工具链与闭合世界与 §自登记锚点)。**精度签名不是构建期集合**:生成阶段对每个结构发一份运行期精度分派 shader(键 `key#x`),运行时按真实精度填 PC `prec`。这是库里唯一的表达式收集机制。 **运行时没有跨表达式录制**:`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`)。 @@ -233,10 +233,33 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba 构建期**一步**(CMake 自动编排,改 Layer 内联表达式后重跑构建即可): 1. **`tools/scan_exprs.cpp`**(收集 + 生成同一进程): - · **收集**:`FusedAnchor` 在静态初始化期按表达式**类型**登记结构(编译期可达),再由 dry-run per-layer 块 + 模型级配置矩阵补精度签名;每个 `dsl::compute*` 在记录模式下把折叠出的 `ExprSpec` 登记进注册表(按 key 去重)。 - · **生成**:`tools/fused_generate.hpp`(原独立工具 `gen_fused`,现已并入本工具)对每条 spec 出 `glsl_gen` GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。 + · **收集**:只收集**结构**。`FusedAnchor` 在静态初始化期按表达式**类型**登记结构(编译期可达),再由 dry-run per-layer + 模型 pass(**只跑 f32**)补齐"运行期配置相关"的结构;fold spec 与 matmul trans 族由显式登记块给出。 + · **生成**:`tools/fused_generate.hpp`(原独立工具 `gen_fused`,现已并入本工具)对**每个结构发三份**:V0 全 f32(键 = `key`)/ **V1 运行期精度分派**(键 = `key#x`)/ V2 native16(键 = `key#a`,按 `expr_prec_sig_native16` 的**结构谓词**判定)→ GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。 · **不经 `.bin` 中间序列化**(`expr_registry.hpp` 的 `write_registry/read_registry/kExprBinVersion` 已删除)。 +### 精度签名已不是构建期集合(2026-10-01,A1) + +**问题**:签名(哪些输入/输出是 f16)由运行期张量精度决定、类型层面不可见,因此历史上"每个 (结构, 签名) 一份 shader"必须靠**执行**发现(dry-run 跑 f32/f16 两遍 + 模型 pass + `NN_PREC_TRACE` 回填清单)。这是扫描里唯一无法用自登记取代的部分。 + +**解法 = 把签名从「身份」降级为「参数」**:V1 变体的 GLSL 对每个输入/输出各声明 **f32 + `float16_t` 双视图**(同一 `VkBuffer` 绑两次,只解引用与真实元素类型一致的那一个),加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 **uniform 分支**。索引数学**逐字不变**(两种视图共用同一逻辑下标)→ 与旧带类型变体数值等价。 + +于是: +- V1 对**每个结构无条件生成** ⇒ **任何签名都命中**,运行期**不存在 miss**、**不存在边界 cast 回退**、**不存在回填清单**(`tools/prec_backfill.txt` 已删除); +- V2(native16,原生 f16 算术)改由**结构谓词**判定生成(代入"全输入 f16 + 输出 f16"这一常量签名),运行时按真实签名确认 —— 生成期同样不需要发现; +- **实测 f16 扫描遍对结构贡献为 0**(`NN_SCAN_F32_ONLY` 探针:跳过它签名 66 → 0、结构恒 84)→ f16 dry-run 遍与 f16 模型 pass 已删除,扫描工作量减半。 + +**端到端实测**("写 `dsl::compute` 即自动融合"):在 Layer 头里新写一个结构不在任何 dry-run/模型 pass 覆盖内的表达式 → + +``` +[scan] 结构来源:dry-run 38(含模型 pass +25、显式登记 +8) + 锚点 60 → 合并 85(锚点独有 14) +[scan] 收集到 85 条融合表达式 +新增键:7d5efd96f337a4a9 / 7d5efd96f337a4a9#x / 7d5efd96f337a4a9#a +``` + +**其他文件零改动**——无 dry-run 条目、无回填清单、无模型 pass 改动。 + +**f16 路径实测**(`NN_PREC_TRACE=1 text_train … --gpu --f16`):`[prec][miss]=0`、边界 cast 归因表**为空**、`[prec][alu-hit]` 6670 → 全程命中预生成 shader,零 cast 回退。 + `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) @@ -244,24 +267,20 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba 手写 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 槽的宽表达式、其余逐元素)。 +- **覆盖语义**:调用点在函数体里,**函数被编译即实例化**——与运行期是否走到该分支无关,因此是"编译期可达"而非"运行期可达"。A/B 实测(`-DNN_SCAN_NO_ANCHOR`):结构数 **71(仅 dry-run/模型 pass)→ 84(+锚点)**,多出的 13 条都是编译进来的真实调用点(含 matmul、归约、16 个 vp 槽的宽表达式等)。⇒ 锚点与 dry-run **互补**:dry-run/模型 pass 是结构的主要来源(71/84),锚点覆盖它跑不到的调用点(13/84)。 - **常驻台账**:锚点登记进独立注册表 `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 值)。该 pass 失败只打 `[scan][warn]` 不中断构建(它是增量,主路径已由 dry-run + 锚点覆盖)。 +- **只登记结构**:⚠ 自登记只能给**结构**(`sig=0`)。**精度签名由生成期的运行期分派变体覆盖**(见上节"精度签名已不是构建期集合")——自登记 + V1 合起来才构成完整的"零注解"链路:结构靠编译期可达,签名靠运行期参数。 +- **模型级 pass(结构侧)**:per-layer dry-run 块只覆盖"层被单独造出来"的路径;有些调用点只在**完整模型**里才执行(GPT/RAPT 的 LM head、PatchEmbedding、模型级位置编码等)。`scan_exprs` 因此跑一遍**配置矩阵**(只跑 f32):`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 38(含模型 pass +25、显式登记 +8) + 锚点 59 → 合并 84(锚点独有 13) - [scan] 签名来源:dry-run 32 + 模型 pass 34 + 显式登记 0 = 66 ``` - A/B 开关:`-DNN_SCAN_NO_ANCHOR`(关锚点)/ `-DNN_SCAN_NO_DRYRUN`(关手写块),均只影响登记集合、不改语义。 -- **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**。 + A/B 开关:`-DNN_SCAN_NO_ANCHOR`(关锚点)/ `-DNN_SCAN_NO_DRYRUN`(关 dry-run/模型 pass),均只影响登记集合、不改语义。 +- **run-only 签名回填已删除**(A1e):`--f16` 下曾有一批只有真实训练才暴露的签名(注意力 fold 的文档掩码 5 输入形态、MNIST/CNN f16 训练的 8 条"f32 入 + f16 出"混合签名),靠 `NN_PREC_TRACE=1` 的 `[prec][miss]` 收集后写进 `tools/prec_backfill.txt`。**该清单与其工作流已随 A1 整体删除**——V1 分派 shader 覆盖任意签名,f16 训练实测 `miss=0` 且边界 cast 归因表为空。历史清单条目与复现命令归档在 `docs/history.md`。 - **契约回归**:上述三条 key 语义 + "符号实例 key ≡ 真实实例 key" 由 `src/expr_fused_key_test.cpp` 锁定(并入 `expr_cpu_test` 聚合目标,ctest 目标数不变)。 任一条被违反(有人重新引入"运行期值决定结构")→ 运行时 key 与登记 key 不一致 diff --git a/docs/development/05-mixed-precision.md b/docs/development/05-mixed-precision.md index 58a02f79..94682919 100644 --- a/docs/development/05-mixed-precision.md +++ b/docs/development/05-mixed-precision.md @@ -631,9 +631,14 @@ loss = ce.forward_sparse(engine, logits, labels, mask, vocab, matmul 段 / 归约 / 目标传递三类变体落地后的同窗交错实测(f32 与 f16 峰值、耗时、backward transient 分桶变化)见 `docs/history.md`;**当前(含 op-level f16 GEMM)的最终数字见 §12.11**。 -#### ⑤ 运行时签名回填(当前) +#### ⑤ 运行期精度分派(当前,2026-10-01 A1) -`[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)。 +**签名已从「身份」降级为「参数」**:融合 shader 的变体集合不再按签名枚举,而是对每个结构发一份**运行期精度分派** shader(键 `key#x`)——它声明每个输入/输出各 f32 + `float16_t` **双视图**(同一 `VkBuffer` 绑两次,只解引用与真实元素类型一致的那一个),加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支。索引数学与旧带类型变体**逐字相同** ⇒ 数值等价。 + +- **无 miss**:V1 对每个结构无条件生成 ⇒ 任何签名都命中;`[prec][miss]` 与边界 cast 回退**不再是 f16 融合路径的一部分**。 +- **native16** 改由**结构谓词**判定生成(键 `key#a`,代入"全输入 f16 + 输出 f16"这一常量签名),运行时按真实签名确认。 +- **回填清单已删除**:`tools/prec_backfill.txt` 连同 `NN_PREC_TRACE` 的"补变体"工作流一并移除;f16 扫描遍也因"对结构贡献为 0"而删除(实测:跳过它签名 66 → 0、结构恒 84)。 +- **实测**(GPT f16 训练,`NN_PREC_TRACE=1`):`[prec][miss]=0`、边界 cast 归因表为空、`[prec][alu-hit]` 6670。 --- @@ -686,7 +691,7 @@ matmul 段 / 归约 / 目标传递三类变体落地后的同窗交错实测(f #### ④ 当前剩余(收益递减,按需再做) - **仍走边界 cast 的 op-level 原语**:用 `NN_PREC_TRACE=1` + `dump_temp_stats()` 复测形状表,对剩余大项按 §12.11 ① 的 `-DNN_SHADER_F16` 手法补变体(同法已覆盖大部分 op 级 shader,见 ① 的 CMake 清单)。 -- **扫描预测不到的运行时签名**:按 §12.10 ⑤ 的回填清单流程处理(miss → 追加 `tools/prec_backfill.txt` → 重跑构建)。 +- **签名覆盖**:不需要任何操作——生成阶段对每个结构发运行期精度分派 shader(§12.10 ⑤),任何签名都命中;新的调用点只要被构建期收集到结构即自动获得 f16 路径(实测:Layer 头里新写一个 `dsl::compute` → 注册表自动多出 `key`/`key#x`/`key#a` 三条,其他文件零改动)。 - **native16(f16 ALU 算术)变体**按设备能力命中/回退(§11.1),无需额外操作。 --- diff --git a/docs/history.md b/docs/history.md index 6e0f66e2..bbd02907 100644 --- a/docs/history.md +++ b/docs/history.md @@ -40,6 +40,16 @@ expr_opt / expr_registry / expr_spec)「历史状态类注释」摘录。整 原理、契约与理由;本文件保存被移出的历史叙事(bug 根因、被否决方案、性能 A/B 过程、 删除清单),供追溯用。 +## 精度签名从构建期枚举 → 运行期参数(2026-10-01,分支 `feat/fused-runtime-prec-dispatch`) + +- 类型:演进记录 / 删除清单 / 性能 A/B +- **背景(被取代的机制)**:精度签名(哪些输入/输出是 f16,`ExprPrecSig`)由运行期张量精度决定、类型层面不可见,因此"每个 (结构, 签名) 一份 shader"必须靠**执行**发现。旧链路 = `FusedAnchor` 只登记结构 + dry-run `profile_f32`/`profile_f16` **两遍** + 模型级配置矩阵两个 profile + `NN_PREC_TRACE=1` 的 `[prec][miss]` → 手工写 `tools/prec_backfill.txt` → 重新构建。清单曾有两批:注意力 fold 文档掩码 5 输入形态(`74a6eaacc0e5d766 10007`)、MNIST/CNN f16 训练 8 条"f32 入 + f16 出"混合签名(`59e079001367d11b 10000`、`fe93c0d99c16113f 10000/10002`、`cb3d830f25b84895 10001`、`b7e6d363af1963ac 10000/10002`、`8a29d02213a0e196 10003`、`27c7edd7a0a4f808 10007`)。旧注册表 84 结构 + 86 签名变体 = 170 条 / 3.43 MB。 +- **新机制**:V1「运行期精度分派」shader(键 `key#x`)对每个输入/输出各声明 f32 + `float16_t` **双视图**(同一 `VkBuffer` 绑两次,只解引用与真实元素类型一致的那一个),加载/存储按 push constant `uint prec` 走 uniform 分支;**索引数学逐字不变** ⇒ 与旧带类型变体数值等价。native16 改按**结构谓词**生成(键 `key#a`,代入"全输入 f16 + 输出 f16"常量签名)。新注册表 84 结构 + 84 分派 + 35 native16 = 203 条 / 4.36 MB。 +- **删除清单**:`tools/prec_backfill.txt`(整文件)、`scan_exprs` 的 f16 dry-run 遍与 f16 模型 pass、"没有带类型变体 / 用 NN_PREC_TRACE 回填"提示、`supports_expr_precision_variant` 里的 (key,sig) 探测、旧 `key#` 变体发射与 run-only 回填分支。 +- **性能 A/B(决定 A 而非 B 的依据)**:曾实测「同质化 + 边界 cast」(方案 B,数值与混合变体等价)在 GPT f16 1 epoch / `tinystories_smoke` / 40HX 上为 4.1s vs 变体路径 4.0/3.9s(**≈+4%**,38 个签名落回 cast);但 B 会给含 f16 输入的表达式造 f32 临时量、推高 f16 峰值显存,故最终选 A(运行期分派,显存不涨)。 +- **实测验收**:build 零告警;ctest 19/19;CPU 锚 `6f8849f14da23110` / GPU dev2 锚 `8ef51b2927253c50` 逐位不变;GPT f16 训练 `NN_PREC_TRACE=1` → `[prec][miss]=0`、边界 cast 归因表为空、`[prec][alu-hit]` 6670;**f16 扫描遍对结构贡献为 0**(`NN_SCAN_F32_ONLY` 探针:跳过它签名 66 → 0、结构恒 84);**端到端**:Layer 头里新写一个 dry-run 覆盖不到的结构 → 锚点 59→60、结构 84→85、注册表 +`key`/`key#x`/`key#a`,**其他文件零改动**。 +- **未做(另案)**:把"收集 + 生成"做成库自带 CMake 能力 `nn_enable_gpu_fusion(target)`(下游自定义层)。**结构侧的关键约束已测明**:84 个结构里只有 13 个来自锚点,71 个来自 dry-run/模型 pass ⇒ 该收集器**必须保留模型 pass**,做不到"30 行纯锚点收集器"。 + ## 融合 matmul BK 取值 A/B 流水(原位置 include/neuralnet.cpp/expr_glsl_gen.hpp:262-269、303-310) - 类型:性能 A/B / 否决方案 - 内容:40HX 变体 trade-off 记录(% = vs 单缓冲耗时,负=更快):BK=32 单缓冲(16KB) 为深网格基线、 diff --git a/docs/introduction/01-architecture.md b/docs/introduction/01-architecture.md index 5c1669e5..b35da40c 100644 --- a/docs/introduction/01-architecture.md +++ b/docs/introduction/01-architecture.md @@ -290,7 +290,7 @@ class Tensor { | `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` 收集折叠后的 `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`,不含注意力语义。 +> **AOT 收集原则**:表达式**文本只出现在 Layer**(内联写进 `forward/backward`)。构建期 `scan_exprs` 只收集折叠后的 `ExprSpec` **结构**(派生物,非手写定义):由 `FusedAnchor` 按表达式类型自登记(编译期可达,dry-run 跑不到的分支也覆盖),再由 dry-run / 模型 pass 补齐运行期配置相关的结构;随后同进程为每个结构发三份 shader(全 f32 / **运行期精度分派** `key#x` / native16 `key#a`)并内联进 `fused_registry.hpp`。**精度签名不是构建期集合**——分派 shader 声明输入/输出双视图、按 PC `prec` 走 uniform 分支,覆盖任意签名,故运行时无 miss、无边界 cast 回退、无回填清单。运行时按 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/usage/03-compute-engine-usage.md b/docs/usage/03-compute-engine-usage.md index d0e2c0d2..33cedf39 100644 --- a/docs/usage/03-compute-engine-usage.md +++ b/docs/usage/03-compute-engine-usage.md @@ -1570,7 +1570,7 @@ public: | 环境变量 | 作用 | |---|---| | `NN_BIND_DEBUG=1` | 未绑定输入进引擎也报错(抓库内 stamp 漏网);错误同步打 stderr,带 `file:line` 与形状。**建议作为门禁运行 `NN_BIND_DEBUG=1 ctest`** | -| `NN_PREC_TRACE=1` | 打印每个 `(结构 key, 精度签名)`;`[prec][miss]` = "请求了非零精度签名却没命中带类型变体"(静默回退边界 cast 的唯一可见信号);`[into] branch=…` 打印 `eval_expr_into` 的分支 | +| `NN_PREC_TRACE=1` | 打印每个 `(结构 key, 精度签名)`;`[prec][alu-hit]` = 命中 native16 变体;`[into] branch=…` 打印 `eval_expr_into` 的分支。**`[prec][miss]` 与"回填清单"工作流已随"运行期精度分派"删除**——分派 shader(键 `key#x`)覆盖任意签名,f16 路径实测 `miss=0` | | `NN_F16_DEBUG=1` | f16 中间量数值扫描(`nn_dbg_scan`)、`accumulate` 巨值打印、`compute_into` 预绑定失败原因 | | `NN_MEM_STATS=1` | 训练中内存采样(配合 `pool_stats()`) | | `NN_VULKAN_DEVICE` | 强制指定 Vulkan 计算设备(索引 `"2"` 或名称子串 `"NVIDIA"`)。优先级:显式 API > 本变量 > 自动打分 | @@ -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` → 生成阶段 自动跑) | +| GPU 报闭合世界未命中 | 表达式**结构**没有预生成 shader | 检查是否把运行时值写成了 `ConstLeaf`(应改 `rparam`);改 Layer 表达式后需重新构建(`scan_exprs` → 生成阶段 自动跑)。**精度签名不会再导致 miss**(运行期分派 shader 覆盖任意签名) | | `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 重试 | From 476e05783a5a1a4a3f8c09955a13c381cdf24937 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 18:52:25 +0800 Subject: [PATCH 22/30] =?UTF-8?q?A2:=20=E5=BA=93=E8=87=AA=E5=B8=A6?= =?UTF-8?q?=E8=9E=8D=E5=90=88=E8=83=BD=E5=8A=9B=20nn=5Fenable=5Fgpu=5Ffusi?= =?UTF-8?q?on=20+=20=E4=BF=AE=E5=A4=8D=E5=A4=9A=20TU=20=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 缺口:本库 header-only,但 fused_registry.hpp 的生成绑死在本仓库 (CMakeLists 硬编码 tools/scan_exprs.cpp,nn_core 无导出)——库外使用者 写自己的 dsl::compute 后没有任何合法途径产出自己的注册表。 落地:CMake 函数 nn_enable_gpu_fusion( MAIN <含 main 的源> [OUT_DIR <目录>])。收集器 = 库内收集逻辑(tools/scan_exprs.cpp:锚点 + dry-run + 模型 pass + 显式登记)+ 本目标的全部 TU(-DNN_EXPR_SCAN 编译, 令使用者调用点自登记),产物目录作该目标的私有 include 目录。 · scan_exprs.cpp 的 `#define NN_EXPR_SCAN` 加防重定义护栏(收集器侧用 -DNN_EXPR_SCAN 编使用者 TU 与此冲突); · **顺带修 header-only 缺陷**:compute_vk_backend.hpp 的 `GpuBuffer::~GpuBuffer()` 类外定义缺 `inline` → ≥2 个 TU 的程序链接期 duplicate symbol(此前未暴露,因为库自身目标都是单 TU); · 新增样例 examples/fusion_custom_layer{,.hpp,_main.cpp}(库外使用者形态, 含自研融合层 + 一行 CMake),并加进默认构建。 实测:样例目标注册表 = 库内 203 条 + 自研层 `94a2cc052598fe09` / `#x` (该表达式含 rparams → native16 谓词不通过,无 `#a`,符合预期);样例 可执行文件输出与期望值 0.6712035 逐位一致;库内文件零改动。 验收:build 零告警;ctest 19/19;CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变;库内注册表仍 203 条 / 84 结构。 --- AGENTS.md | 4 +- CMakeLists.txt | 63 +++++++++++++++++++ docs/history.md | 9 +++ examples/fusion_custom_layer.cpp | 22 +++++++ examples/fusion_custom_layer.hpp | 35 +++++++++++ examples/fusion_custom_layer_main.cpp | 53 ++++++++++++++++ .../backend/compute_vk_backend.hpp | 7 ++- tools/scan_exprs.cpp | 4 ++ 8 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 examples/fusion_custom_layer.cpp create mode 100644 examples/fusion_custom_layer.hpp create mode 100644 examples/fusion_custom_layer_main.cpp diff --git a/AGENTS.md b/AGENTS.md index c6ba9826..eb7805b3 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/fused_generate.hpp`(生成阶段,原独立工具 `gen_fused`;另有 `tools/decode_fused.py` 调试用) | +| 构建期工具(AOT 融合) | `tools/scan_exprs.cpp`(**单步** = 收集 + 生成)/ `tools/fused_generate.hpp`(生成阶段,原独立工具 `gen_fused`;另有 `tools/decode_fused.py` 调试用)。**库外使用者**:CMake 函数 `nn_enable_gpu_fusion( MAIN <含 main 的源>)`(定义在 `CMakeLists.txt`,样例 `examples/fusion_custom_layer*`) | | 批量改写 / 一致性审计(改多处时用,均带 `-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`) | @@ -182,6 +182,8 @@ GPT 序列展平: 列序 i = b*seq + t(batch-major,全局唯一约定) 2. **生成**:`tools/fused_generate.hpp`(原独立工具 `gen_fused`)对**每个结构发三份**:V0 全 f32(键 = `key`)/ **V1 运行期精度分派**(键 = `key#x`)/ V2 native16(键 = `key#a`,按 `expr_prec_sig_native16` 的**结构谓词**判定)→ 经 `emitter_registry` 选后端(默认 `"glsl"` = `GlslEmitter`)生成 GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。**不经 `.bin` 中间序列化**(`write_registry/read_registry` 已删除)。 - **精度签名不再是构建期集合**("签名从身份降级为参数"):V1 的 GLSL 对每个输入/输出各声明 f32 + `float16_t` 双视图,加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支,**一份 shader 覆盖任意签名**。因此运行期**不存在 miss、不存在边界 cast 回退、不存在回填清单**(`tools/prec_backfill.txt` 已删除,`NN_PREC_TRACE` 的"补变体"流程随之失效)。V1 对每个结构无条件生成 ⇒ 任何签名都命中。 - **新增调用点无需改动其他文件**(实测):在 Layer 头里新写一个 `dsl::compute`(其结构不在任何 dry-run / 模型 pass 覆盖内)→ 锚点独有 13 → 14、结构 84 → 85、注册表新增 `key` / `key#x` / `key#a` 三条,**其他文件零改动**。 +- **库外使用者:`nn_enable_gpu_fusion( MAIN <含 main 的源>)`**(`CMakeLists.txt`):使用者在自己的层头里写 `dsl::compute` + 在 CMake 加这一行 → 自动生成**该目标专属**的 `fused_registry.hpp` 并加进它的 include path。机制 = 收集器由**库内收集逻辑**(`tools/scan_exprs.cpp`:锚点 + dry-run + 模型 pass + 显式登记)**加上本目标的全部 TU**(`-DNN_EXPR_SCAN` 编译,让使用者的调用点自登记)组成。**⚠ 必须保留库内收集逻辑**:84 个结构里只有 13 个来自编译期锚点、71 个来自 dry-run/模型 pass,"纯锚点收集器"会漏库自身的结构。样例见 `examples/fusion_custom_layer*`(自研层 → 应用注册表多出 `94a2cc052598fe09` + `#x`,库内文件零改动)。 +- **库是 header-only,多 TU 必须可链接**:头里的类外定义一律要 `inline`(曾漏 `GpuBuffer::~GpuBuffer` → ≥2 个 TU 的程序链接期 duplicate symbol,`examples/fusion_custom_layer*` 抓到)。 - **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`)。 diff --git a/CMakeLists.txt b/CMakeLists.txt index a331851c..ca0ce5b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -492,6 +492,55 @@ if(Vulkan_FOUND) list(APPEND GPU_SHADER_HPPS ${NN_FUSED_REGISTRY_HPP}) + # ── 库自带的融合能力:为**任意目标**收集 + 生成它自己的 fused_registry.hpp ── + # 用法:nn_enable_gpu_fusion( MAIN <含 main 的源文件> [OUT_DIR <目录>]) + # + # 语义(对应"写 dsl::compute 之后只跑构建即可"): + # 收集器 = 库内收集逻辑(`tools/scan_exprs.cpp`:锚点自登记 + dry-run + + # 模型 pass + 显式登记)+ **本目标的全部 TU**(用 -DNN_EXPR_SCAN 编, + # 让使用者的 `dsl::compute` 调用点在静态初始化期自登记)。 + # 产物目录只作为该目标的私有 include 目录——"`fused_registry.hpp` 在 + # include path 上即生效"这一既有机制不变。 + # + # ⚠ 收集器**必须保留库内收集逻辑**:实测 84 个结构里只有 13 个来自 + # 编译期锚点,71 个来自 dry-run/模型 pass(见 docs/history.md 与 + # docs/development/02)。纯锚点收集器会漏掉库自身的结构。 + function(nn_enable_gpu_fusion target) + cmake_parse_arguments(NNF "" "MAIN;OUT_DIR" "" ${ARGN}) + if(NOT NNF_OUT_DIR) + set(NNF_OUT_DIR "${CMAKE_BINARY_DIR}/nn_fusion/${target}") + endif() + if(NNF_MAIN) + get_filename_component(NNF_MAIN "${NNF_MAIN}" ABSOLUTE) + endif() + get_target_property(_nnf_srcs ${target} SOURCES) + set(_nnf_inputs "") + foreach(_s IN LISTS _nnf_srcs) + if(_s MATCHES "^\\$<") # 生成器表达式源(跳过,罕见) + continue() + endif() + get_filename_component(_abs "${_s}" ABSOLUTE) + if(NOT NNF_MAIN OR NOT _abs STREQUAL NNF_MAIN) + list(APPEND _nnf_inputs "${_abs}") + endif() + endforeach() + set(_nnf_reg "${NNF_OUT_DIR}/fused_registry.hpp") + add_executable(${target}_nnfusion_collect ${SCAN_SRC} ${_nnf_inputs}) + target_include_directories(${target}_nnfusion_collect + PRIVATE ${CMAKE_SOURCE_DIR}/include/neuralnet.cpp) + target_compile_definitions(${target}_nnfusion_collect PRIVATE NN_EXPR_SCAN) + target_link_libraries(${target}_nnfusion_collect PRIVATE nn_core Vulkan::Vulkan) + add_custom_command( + OUTPUT ${_nnf_reg} + COMMAND ${CMAKE_COMMAND} -E make_directory ${NNF_OUT_DIR} + COMMAND ${target}_nnfusion_collect ${NNF_OUT_DIR} ${GLSLC} + DEPENDS ${target}_nnfusion_collect ${SCAN_SRC} ${FUSED_GEN_SRC} ${_nnf_inputs} + COMMENT "[nn] 为 ${target} 收集 + 合成融合 shader → ${_nnf_reg}") + add_custom_target(${target}_nnfusion_registry DEPENDS ${_nnf_reg}) + target_include_directories(${target} PRIVATE ${NNF_OUT_DIR}) + add_dependencies(${target} ${target}_nnfusion_registry) + endfunction() + if(NOT GLSLC) message(WARNING "No SPIR-V compiler found (glslc). " "GPU acceleration disabled. Install Vulkan SDK.") @@ -514,6 +563,20 @@ if(Vulkan_FOUND) add_dependencies(${TARGET} compile_gpu_shaders) endforeach() + # ── A2 验收样例:一个「库外使用者」形态的目标 ────────────────────── + # 它有自己的自研融合层(examples/fusion_custom_layer.hpp),**不使用** + # 库内 build/generated 的注册表,而是靠 nn_enable_gpu_fusion 生成自己 + # 的那一份 —— 证明"写 dsl::compute + 一行 CMake 即可"。 + add_executable(fusion_custom_layer_example + examples/fusion_custom_layer_main.cpp + examples/fusion_custom_layer.cpp) + target_include_directories(fusion_custom_layer_example PRIVATE + ${CMAKE_SOURCE_DIR}/include/neuralnet.cpp) + target_compile_definitions(fusion_custom_layer_example PRIVATE NN_HAS_VULKAN) + target_link_libraries(fusion_custom_layer_example PRIVATE nn_core Vulkan::Vulkan) + nn_enable_gpu_fusion(fusion_custom_layer_example + MAIN ${CMAKE_SOURCE_DIR}/examples/fusion_custom_layer_main.cpp) + message(STATUS "GPU acceleration enabled (Vulkan compute)") endif() else() diff --git a/docs/history.md b/docs/history.md index bbd02907..b5e6eff3 100644 --- a/docs/history.md +++ b/docs/history.md @@ -50,6 +50,15 @@ expr_opt / expr_registry / expr_spec)「历史状态类注释」摘录。整 - **实测验收**:build 零告警;ctest 19/19;CPU 锚 `6f8849f14da23110` / GPU dev2 锚 `8ef51b2927253c50` 逐位不变;GPT f16 训练 `NN_PREC_TRACE=1` → `[prec][miss]=0`、边界 cast 归因表为空、`[prec][alu-hit]` 6670;**f16 扫描遍对结构贡献为 0**(`NN_SCAN_F32_ONLY` 探针:跳过它签名 66 → 0、结构恒 84);**端到端**:Layer 头里新写一个 dry-run 覆盖不到的结构 → 锚点 59→60、结构 84→85、注册表 +`key`/`key#x`/`key#a`,**其他文件零改动**。 - **未做(另案)**:把"收集 + 生成"做成库自带 CMake 能力 `nn_enable_gpu_fusion(target)`(下游自定义层)。**结构侧的关键约束已测明**:84 个结构里只有 13 个来自锚点,71 个来自 dry-run/模型 pass ⇒ 该收集器**必须保留模型 pass**,做不到"30 行纯锚点收集器"。 +## 库自带的融合能力 `nn_enable_gpu_fusion`(2026-10-01,A2) + +- 类型:演进记录 / 新增能力 +- **背景(缺口)**:本库 header-only,但 `fused_registry.hpp` 的生成**绑死在本仓库**(`CMakeLists.txt` 硬编码 `tools/scan_exprs.cpp` + `tools/prec_backfill.txt`,且 `nn_core` 无 install/导出)——库外使用者写自己的 `dsl::compute` 后**没有任何合法途径**产出自己的注册表,只能 fork。这是"导入头文件即可用"真正卡住的地方。 +- **落地**:CMake 函数 `nn_enable_gpu_fusion( MAIN <含 main 的源> [OUT_DIR <目录>])`。收集器 = **库内收集逻辑**(`tools/scan_exprs.cpp`)+ **本目标的全部 TU**(`-DNN_EXPR_SCAN` 编译,令使用者调用点经 `FusedAnchor` 自登记)链接成一个可执行文件,产物目录只作该目标的私有 include 目录。样例 `examples/fusion_custom_layer{,.hpp,_main.cpp}`("库外使用者"形态:自定义层 + 一行 CMake)。 +- **实测**:样例目标的注册表 = 库内 203 条 + 自研层 `94a2cc052598fe09` / `94a2cc052598fe09#x`(该表达式含 rparams → native16 谓词不通过,无 `#a`,符合预期);样例可执行文件运行结果与期望值 `0.6712035` 逐位一致。库内文件零改动。 +- **顺带修的 header-only 缺陷**:`backend/compute_vk_backend.hpp` 的 `GpuBuffer::~GpuBuffer()` 类外定义**缺 `inline`** → 任何 ≥2 个 TU 的程序链接期 `duplicate symbol`(样例两 TU 复现)。已加 `inline`。这条此前从未暴露,因为库自身的可执行目标都是单 TU。 +- **未做**:把样例注册进 ctest(本次保持 ctest 19/19 口径不变;样例由默认构建覆盖编译,运行时断言需手工跑 `build/fusion_custom_layer_example.exe`)。 + ## 融合 matmul BK 取值 A/B 流水(原位置 include/neuralnet.cpp/expr_glsl_gen.hpp:262-269、303-310) - 类型:性能 A/B / 否决方案 - 内容:40HX 变体 trade-off 记录(% = vs 单缓冲耗时,负=更快):BK=32 单缓冲(16KB) 为深网格基线、 diff --git a/examples/fusion_custom_layer.cpp b/examples/fusion_custom_layer.cpp new file mode 100644 index 00000000..c6df6a3e --- /dev/null +++ b/examples/fusion_custom_layer.cpp @@ -0,0 +1,22 @@ +// ═══════════════════════════════════════════════════════════════════════════ +// examples/fusion_custom_layer.cpp — 自研层的翻译单元(A2 验收用) +// +// 存在的唯一目的:让 `nn_enable_gpu_fusion` 编收集器时**编译到本 TU**, +// 从而让 fusion_custom_layer.hpp 里的 `dsl::compute` 调用点在静态初始化期 +// 经 `FusedAnchor` 自登记——这正是"收集器必须看到使用者的调用点"的 +// 落地方式(收集器 = `tools/scan_exprs.cpp` 的库内收集 + 本应用的全部 TU)。 +// ═══════════════════════════════════════════════════════════════════════════ + +#include "fusion_custom_layer.hpp" + +namespace nn_example +{ + +// 非 inline:odr-use 该函数 → 其函数体被发射 → 内部的 `dsl::compute` 实例化 +// → `FusedAnchor` 静态初始化登记结构。不调用它、也不改任何库内文件。 +void fusion_anchor_touch() +{ + (void)&fused_custom_op; +} + +} // namespace nn_example diff --git a/examples/fusion_custom_layer.hpp b/examples/fusion_custom_layer.hpp new file mode 100644 index 00000000..7a274c64 --- /dev/null +++ b/examples/fusion_custom_layer.hpp @@ -0,0 +1,35 @@ +#pragma once + +// ═══════════════════════════════════════════════════════════════════════════ +// examples/fusion_custom_layer.hpp — 「自研层」示例(A2 验收) +// +// 模拟一个**库外**使用者的自定义层:表达式结构在库内不存在,只在本头里写。 +// 配合 `nn_enable_gpu_fusion()`,使用者**只做两件事**: +// ① 写 `dsl::compute`(本文件); +// ② 在 CMake 里加一行 `nn_enable_gpu_fusion()`。 +// 不需要写 dry-run 条目、不需要精度回填清单、不需要碰库内任何文件。 +// +// 注意:库内规则「表达式文本只出现在 Layer」对**库外使用者**的自然对应物 +// 就是"只出现在自己的层/算子头里"。 +// ═══════════════════════════════════════════════════════════════════════════ + +#include + +namespace nn_example +{ + +// 一个刻意与库内任何结构都不同的融合表达式: +// out = exp(|x| * alpha) / (sqrt(|y|) + alpha) +// 两个运行时标量参数(alpha)→ 值不进 key,同结构共享 shader。 +[[nodiscard]] inline nn::Result fused_custom_op( + nn::ComputeEngine& engine, nn::Tensor& x, nn::Tensor& y, float alpha, + nn::Precision P = nn::Precision::F32) +{ + return nn::dsl::compute(engine, + nn::dsl::exp(nn::dsl::abs(nn::dsl::leaf(x)) * nn::dsl::rparam(alpha)) + / (nn::dsl::sqrt(nn::dsl::abs(nn::dsl::leaf(y))) + + nn::dsl::rparam(alpha)), + x.rows(), x.cols(), P); +} + +} // namespace nn_example diff --git a/examples/fusion_custom_layer_main.cpp b/examples/fusion_custom_layer_main.cpp new file mode 100644 index 00000000..cf7fa8c3 --- /dev/null +++ b/examples/fusion_custom_layer_main.cpp @@ -0,0 +1,53 @@ +// ═══════════════════════════════════════════════════════════════════════════ +// examples/fusion_custom_layer_main.cpp — A2 验收样例的可执行入口 +// +// 验收断言(构建期即完成,见 CMake 的 nn_enable_gpu_fusion): +// 本目标**只**依赖自己的 `fused_registry.hpp`(由 nn_enable_gpu_fusion +// 用本应用的 TU + 库内收集逻辑生成),该注册表里必须出现自研表达式 +// 的结构键(以及 `#x` 运行期精度分派变体)——**库内文件零改动**。 +// +// 运行时这里只验证 CPU 路径可用(模板求值,与注册表无关);GPU 路径的 +// 结构登记与分派命中由 A1 的端到端实测覆盖(见 docs/history.md)。 +// ═══════════════════════════════════════════════════════════════════════════ + +#include "fusion_custom_layer.hpp" + +#include +#include +#include + +int main() +{ + nn::CpuEngine cpu; + nn::Tensor x = cpu.create_tensor(4, 3, nn::Precision::F32, + nn::InitSpec::constant(0.5f)); + nn::Tensor y = cpu.create_tensor(4, 3, nn::Precision::F32, + nn::InitSpec::constant(2.0f)); + auto out = nn_example::fused_custom_op(cpu, x, y, 0.7f); + if (!out) + { + std::fprintf(stderr, "[example] fused_custom_op 失败: %s\n", + out.error().message.c_str()); + return 1; + } + std::vector buf(12, 0.0f); + if (auto r = cpu.read(*out, std::span(buf)); !r) + { + std::fprintf(stderr, "[example] read 失败\n"); + return 1; + } + // exp(0.5*0.7) / (sqrt(2.0) + 0.7) + const float expect = 1.419067549f / (1.414213562f + 0.7f); + const float got = buf[0]; + const float diff = got > expect ? got - expect : expect - got; + std::printf("[example] 自研融合层 CPU 结果 %.7f(期望 %.7f,差 %.3e)\n", + static_cast(got), static_cast(expect), + static_cast(diff)); + if (diff > 1e-5f) + { + std::fprintf(stderr, "[example] 数值不符\n"); + return 1; + } + std::puts("[example] OK"); + return 0; +} diff --git a/include/neuralnet.cpp/backend/compute_vk_backend.hpp b/include/neuralnet.cpp/backend/compute_vk_backend.hpp index bc393802..ebeb3409 100644 --- a/include/neuralnet.cpp/backend/compute_vk_backend.hpp +++ b/include/neuralnet.cpp/backend/compute_vk_backend.hpp @@ -5188,8 +5188,13 @@ class GpuBackend // ══════════════════════════════════════════════════════════════════════ // GpuBuffer 析构(类外定义):batch 录制期间延迟销毁,避免已录制的 // descriptor 引用已销毁的 buffer(VUID-vkDestroyBuffer-buffer-00922)。 +// +// **必须 inline**:本库是 header-only,任何包含本头的翻译单元都会发射本定义 +// ——缺失 inline 会让"≥2 个 TU 的程序"链接期 duplicate symbol(实测 +// `examples/fusion_custom_layer_*` 两 TU 即撞)。库外使用者的常规工程 +// (多 TU)因此不可用。 // ══════════════════════════════════════════════════════════════════════ -GpuBuffer::~GpuBuffer() +inline GpuBuffer::~GpuBuffer() { if (buffer_ == VK_NULL_HANDLE) return; diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index 35e069a0..f3035305 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -16,7 +16,11 @@ // 时,把对应的 dry-run 调用加到下面。 // ─────────────────────────────────────────────────────────────────────────── +// NN_EXPR_SCAN 由本文件自行开启;`nn_enable_gpu_fusion` 还会用同名宏编译 +// 使用者的 TU(让它们的 `dsl::compute` 调用点自登记)——此处必须防重定义。 +#ifndef NN_EXPR_SCAN #define NN_EXPR_SCAN +#endif #include #include From f334cd2e153714b8cb05957842d7d3f14775948c Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 18:59:53 +0800 Subject: [PATCH 23/30] =?UTF-8?q?A1f:=20=E5=88=A0=E9=99=A4=E7=B2=BE?= =?UTF-8?q?=E5=BA=A6=E5=8F=98=E4=BD=93=E6=AE=8B=E7=95=99=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=EF=BC=88=E6=B3=A8=E5=86=8C=E8=A1=A8=20variants=20/=20=E6=AD=BB?= =?UTF-8?q?=E5=88=86=E6=94=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 签名不再是构建期集合之后,这些代码全部成了死重: · expr_registry.hpp:删除 `ExprVariant`、`variants`、`variant_keys`、 `contains_variant`;`add()` 收敛为只收结构; · expr_dsl.hpp:scan 模式的 compute/compute_into/compute_reduce 与 FusedAnchor 一律只 `add(spec)`(原先每个调用点还要算一次 `expr_prec_sig_of`); · tools/scan_exprs.cpp:删除 variants 计数器与"签名来源"台账行、 以及末尾的"精度变体 N 条"打印; · tools/fused_generate.hpp:`emit_one` 的 key/suffix 收敛为两种形态 (`key#x` 分派 / `key#a` native16),删掉永不可达的 `key#` 分支; 分派变体发射失败改为**可见告警**(它是该结构唯一的签名无关 shader), native16 失败仍静默跳过; · expr_spec.hpp:删除已无调用点的 `expr_prec_sig_alu_key` (保留 `expr_prec_sig_key` 供 `NN_PREC_TRACE` 诊断用)。 验收: · build 零告警;ctest 19/19; · **build/generated/fused_registry.hpp 与清理前 SHA256 逐字节一致** (3D47775F77D34888069482F3BFDA410B837D55F9A8A68FE5B5A15A5BA1B7F64D) —— 证明删掉的全是不可达路径; · CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变; · doc_align [A]/[B]/[D]/[E]/[F] 可行动 0。 --- include/neuralnet.cpp/expr_dsl.hpp | 20 +++++------ include/neuralnet.cpp/expr_registry.hpp | 44 ++++++------------------- include/neuralnet.cpp/expr_spec.hpp | 14 +------- tools/fused_generate.hpp | 25 +++++++------- tools/scan_exprs.cpp | 22 +++---------- 5 files changed, 35 insertions(+), 90 deletions(-) diff --git a/include/neuralnet.cpp/expr_dsl.hpp b/include/neuralnet.cpp/expr_dsl.hpp index a81ca7af..a7c080af 100644 --- a/include/neuralnet.cpp/expr_dsl.hpp +++ b/include/neuralnet.cpp/expr_dsl.hpp @@ -1390,7 +1390,7 @@ struct FusedAnchor (void)inputs; // 符号实例:inputs 全是占位张量,注册只用结构 if (auto v = validate_expr_spec(spec, spec.views.size()); !v) scan_reject(v.error()); - anchor_registry().add(spec, 0u); + anchor_registry().add(spec); return 0; }(); }; @@ -1425,11 +1425,9 @@ template auto [spec, inputs] = to_expr_spec(e); if (auto v = validate_expr_spec(spec, inputs.size()); !v) scan_reject(v.error()); - // 精度签名(结构 key 不含精度):同一结构可登记多个带类型变体 - // (in-kernel f16 = 半精度直读直写)。占位张量按目标精度返回 —— 否则 - // dry-run 下游层看到的是 f32,f16 变体永远发现不到。 - const ExprPrecSig sig = expr_prec_sig_of(inputs, P); - fused::global_registry().add(spec, sig); + // 构建期只登记**结构**:精度签名已不是构建期集合(生成阶段对每个结构 + // 发运行期精度分派 shader,运行期按真实精度填 PC `prec`)。 + fused::global_registry().add(spec); return eng.adopt(P == Precision::F16 ? TensorAccess::cpu(rows, cols) : TensorAccess::cpu(rows, cols)); #else @@ -1533,10 +1531,9 @@ template auto [spec, inputs] = to_expr_spec(e); if (auto v = validate_expr_spec(spec, inputs.size()); !v) scan_reject(v.error()); - // 原地语义:输出精度 = dst 的存储精度(§8.3)→ 签名输出位取 dst - const ExprPrecSig sig = expr_prec_sig_of(inputs, dst.precision()); + // 只登记结构(精度签名不是构建期集合,见 expr_dsl.hpp 头注释) (void)dst; - fused::global_registry().add(spec, sig); + fused::global_registry().add(spec); return {}; #else if (eng.device() == Device::CPU) @@ -1612,9 +1609,8 @@ template if (auto v = validate_expr_spec(spec, inputs.size()); !v) scan_reject(v.error()); const int raxis = expr_spec_reduce_axis(spec); - // 精度签名(同 compute()):占位张量按目标精度返回,供下游 dry-run 继续 - const ExprPrecSig sig = expr_prec_sig_of(inputs, P); - fused::global_registry().add(spec, sig); + // 只登记结构;占位张量仍按目标精度返回,供下游 dry-run 继续(形状相关) + fused::global_registry().add(spec); if (P == Precision::F16) return eng.adopt((raxis == 0) ? TensorAccess::cpu(rows, 1) : (raxis == 1) ? TensorAccess::cpu(1, cols) diff --git a/include/neuralnet.cpp/expr_registry.hpp b/include/neuralnet.cpp/expr_registry.hpp index f49bb5d3..730b90b9 100644 --- a/include/neuralnet.cpp/expr_registry.hpp +++ b/include/neuralnet.cpp/expr_registry.hpp @@ -29,52 +29,28 @@ namespace nn::fused { -// ── 精度变体(Phase 2 in-kernel f16):同一结构 + 精度签名 ──────────────── -struct ExprVariant -{ - ExprSpec spec; - ExprPrecSig sig = 0; -}; - -// ── 注册表:收集折叠出的 ExprSpec 结构,按规范 key 去重 ───────────────── +// ── 注册表:收集折叠出的 ExprSpec **结构**,按规范 key 去重 ─────────────── +// +// 只装**结构**:精度签名已不是构建期集合(生成阶段对每个结构发运行期精度 +// 分派 shader,键 `key#x`;见 expr_spec.hpp 的 EXPR_PREC_SIG_DISPATCH)。 +// 历史上有过 `variants`((结构, 签名) 表)与 `contains_variant`——随签名 +// 降级为运行期参数一并删除(docs/history.md)。 struct ExprRegistry { - // sig == 0(全 f32):按结构 key 去重进基础表 specs —— bin / 生成头的 - // 基础段只承载这些条目(key 不加后缀)。 std::vector specs; std::unordered_set keys; - // sig != 0(带类型变体):按 (结构 key, 精度签名) 去重。bin 只序列化 - // {sig, 基础结构下标}:变体与基础结构**同结构同 key**,故只需下标引用。 - // 同结构同 key,spec 体复用基础表条目,不重复落盘。 - std::vector variants; - std::unordered_set variant_keys; - void add(const ExprSpec& s, ExprPrecSig sig = 0) + void add(const ExprSpec& s) { // 登记 canonical IR:canonicalize 为引擎内部优化(IR-A/IR-B), - // bin 与 key 建立在 canonical 形态上(scan 与 runtime 两端一致)。 + // key 建立在 canonical 形态上(scan 与 runtime 两端一致)。 const ExprSpec canon = canonicalize_expr_spec(s); const std::string k = expr_spec_key(canon); - if (sig == 0u) - { - if (keys.insert(k).second) - specs.push_back(canon); - return; - } - const std::string vk = expr_prec_sig_key(k, sig); - if (variant_keys.insert(vk).second) - variants.push_back(ExprVariant{canon, sig}); + if (keys.insert(k).second) + specs.push_back(canon); } [[nodiscard]] bool contains(const ExprSpec& s) const { return keys.count(expr_spec_key(canonicalize_expr_spec(s))) != 0; } - // 该 (结构, 签名) 是否已登记(sig==0 走结构表) - [[nodiscard]] bool contains_variant(const ExprSpec& s, ExprPrecSig sig) const - { - const std::string k = expr_spec_key(canonicalize_expr_spec(s)); - if (sig == 0u) - return keys.count(k) != 0; - return variant_keys.count(expr_prec_sig_key(k, sig)) != 0; - } }; // 全局注册表(scan_exprs 记录模式写入;普通构建不含 NN_EXPR_SCAN,零开销) diff --git a/include/neuralnet.cpp/expr_spec.hpp b/include/neuralnet.cpp/expr_spec.hpp index 845f8e71..87a44289 100644 --- a/include/neuralnet.cpp/expr_spec.hpp +++ b/include/neuralnet.cpp/expr_spec.hpp @@ -761,19 +761,7 @@ inline constexpr const char* EXPR_PREC_DISPATCH_SUFFIX = "#x"; return spec_key + buf; } -// native16(原生 f16 算术)变体键 = 签名键再加 "#a" 后缀(如 -// "644a...#10001#a")。与 f32 算术变体("644a...#10001")**并存**: -// 后端按设备 shaderFloat16 能力决定创建哪个 pipeline,运行时优先命中 -// native16 键、回退 f32 算术键(设备无 ALU 能力时走 f32 算术键)。结构 key 与 sig 均为 -// hex 字符串,"#a" 后缀不可能与任何普通键碰撞(普通键只含 [0-9a-f#] 且以 -// sig hex 结尾)。 -[[nodiscard]] inline std::string expr_prec_sig_alu_key(const std::string& spec_key, - ExprPrecSig sig) -{ - return expr_prec_sig_key(spec_key, sig) + "#a"; -} - -// 诊断用:把签名渲染成 "in=[f16,f32,...] out=f16" +// ── 诊断用:把签名渲染成 "in=[f16,f32,...] out=f16" [[nodiscard]] inline std::string expr_prec_sig_str(ExprPrecSig sig, std::size_t num_inputs) { diff --git a/tools/fused_generate.hpp b/tools/fused_generate.hpp index 426ff65c..fe2e4265 100644 --- a/tools/fused_generate.hpp +++ b/tools/fused_generate.hpp @@ -339,18 +339,13 @@ namespace nn::tool if (alu && !nn::expr_prec_sig_native16(spec, sig)) return false; // 谓词不通过 → 不发 ALU 变体(f32 算术变体仍覆盖) const std::string key = nn::expr_spec_key(spec); - // 运行期精度分派(V1):键 = key#x(无签名 hex——精度不再是身份) + // 只有两种结构变体(精度不再是身份): + // V1 运行期精度分派 → 键 key#x + // V2 native16(结构谓词判定)→ 键 key#a const bool dispv = nn::expr_prec_sig_is_dispatch(sig); - char sigbuf[16]; - std::snprintf(sigbuf, sizeof(sigbuf), "%04x", static_cast(sig)); - const std::string vkey = dispv - ? (key + nn::EXPR_PREC_DISPATCH_SUFFIX) - : (alu ? (key + "#a") // native16:结构谓词判定 → 键与签名无关 - : nn::expr_prec_sig_key(key, sig)); - const std::string suffix = dispv - ? (key + "_x") - : (alu ? (key + "_a") - : (key + "_" + sigbuf)); + const std::string vkey = dispv ? (key + nn::EXPR_PREC_DISPATCH_SUFFIX) + : (key + "#a"); + const std::string suffix = dispv ? (key + "_x") : (key + "_a"); if (!emitted_vkeys.insert(vkey).second) return false; // 已发射(scan 预测 ∩ 回填清单重叠)→ 幂等跳过 const std::string comp_path = out_dir + "/fused_" + suffix + ".comp"; @@ -367,9 +362,11 @@ namespace nn::tool if (glsl.empty()) { if (alu) - return false; // ALU 谓词/形态不支持 → 静默跳过(f32 变体已覆盖) - std::fprintf(stderr, "[skip] 精度变体 %s(sig=%s)暂不支持带类型生成\n", - key.c_str(), sigbuf); + return false; // 结构谓词通过但发射器不支持该形态 → 静默跳过 + //(V0/V1 仍覆盖,只是拿不到原生 f16 算术) + // 分派变体(key#x)是唯一的签名无关 shader:发不出来 = 该结构 + // 没有 f16 路径 → 必须可见。 + std::fprintf(stderr, "[skip] 分派变体 %s 的形态暂不支持生成\n", vkey.c_str()); return false; } { diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index f3035305..3a6e88ae 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -395,7 +395,6 @@ int main(int argc, char* argv[]) #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(补"整模型路径"的签名)── @@ -534,7 +533,6 @@ int main(int argc, char* argv[]) // 模型 pass:**结构**来源(运行期配置相关的路径),与精度无关 → 只跑 f32。 model_pass(raw_engine, nn::profile_f32()); 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 双 @@ -590,7 +588,6 @@ 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 未执行到"的 @@ -598,7 +595,7 @@ int main(int argc, char* argv[]) { auto& anchor = nn::dsl::anchor_registry(); for (const auto& s : anchor.specs) - reg.add(s, 0u); + reg.add(s); std::printf("[scan] 结构来源:dry-run %zu(含模型 pass +%zu、显式登记 +%zu)" " + 锚点 %zu → 合并 %zu(锚点独有 %zu)\n", n_after_dry, @@ -606,31 +603,22 @@ int main(int argc, char* argv[]) 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 - // 带类型变体 —— 下面单独报告。 + // 没纳入 dry-run)。 if (anchor.specs.empty() || n_before_anchor == reg.specs.size()) std::fprintf(stderr, "[scan][warn] 锚点未贡献任何结构:可能被 -DNN_SCAN_NO_ANCHOR 关闭," "或所有调用点都已被 dry-run 覆盖\n"); } std::printf("[scan] 收集到 %zu 条融合表达式\n", reg.specs.size()); - // ── 签名覆盖:**不再需要背书** ──────────────────────────────────────── + // ── 签名覆盖:**构建期不再需要任何背书** ────────────────────────────── // 生成阶段对每个结构无条件发一份运行期精度分派 shader(键 key#x)—— // 输入/输出双视图 + PC `prec`,一份覆盖任意 (输入精度位图, 输出精度)。 // 因此: // · 结构 = 唯一需要"收集"的东西(锚点 + dry-run/模型 pass); // · 签名**不再是构建期集合**,无需 dry-run 的 f16 遍、无需回填清单, - // 运行期也不存在 miss / 边界 cast 回退。 - // 下面只保留结构侧的诊断(dry-run 的 profile 遍仍会走 Layer,用于暴露 - // 运行期配置相关的结构,与签名无关)。 - if (!reg.variants.empty()) - std::printf("[scan] 精度变体 %zu 条(历史数据;生成阶段已不再消费——" - "分派 shader 覆盖全部签名)\n", - reg.variants.size()); + // 运行期也不存在 miss / 边界 cast 回退; + // · 注册表里没有"精度变体"这一层(`ExprRegistry::variants` 已删除)。 // ── 生成阶段(原 gen_fused):同一进程内直接消费注册表 → fused_registry.hpp // 构建期因此只有一步;不再经 expr_specs.bin 中间序列化。 if (!nn::tool::generate_fused_registry(out_dir, glslc, reg)) From a2c90fbee092f699a78d962ad5054a25f9bbae73 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 19:05:03 +0800 Subject: [PATCH 24/30] =?UTF-8?q?A2b:=20=E6=A0=B7=E4=BE=8B=E8=A1=A5=20GPU?= =?UTF-8?q?=20=E7=AB=AF=E5=88=B0=E7=AB=AF=E9=AA=8C=E6=94=B6=EF=BC=88?= =?UTF-8?q?=E5=90=AB=20f16=20=E5=88=86=E6=B4=BE=E8=B7=AF=E5=BE=84=EF=BC=89?= =?UTF-8?q?+=20=E4=BF=AE=20CMake=20include=20=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原先的样例只验 CPU——而 CPU 走编译期模板求值、与注册表无关,**等于没验到 本 PR 的核心机制**。补上 GPU 路径: CPU f32 = 0.6712035(差 0) GPU f32 (V0) = 0.6712036(差 5.96e-08) GPU f16 (V1 分派) = 0.6708984(差 3.05e-04,f16 舍入内) 该表达式含 rparams ⇒ expr_prec_sig_native16 谓词不通过 ⇒ f16 那一跑命中的 必然是 `key#x` 运行期精度分派 shader(双视图 + PC `prec`)。注册表若漏了它 就是闭合世界硬报错 —— "跑通"本身即断言。 顺带修 nn_enable_gpu_fusion 的两处: · include 顺序:只加 ${NNF_OUT_DIR} 时样例报 `matmul SPIR-V bytecode not embedded`(手写原语 shader 嵌入头在 ${CMAKE_BINARY_DIR}/generated,而该 目录**同时含库自身的 fused_registry.hpp**)。现在两者都加且 **NNF_OUT_DIR 在前**(__has_include 取第一个匹配),否则使用者的自定义结构会解析到库的 注册表 → 全部闭合世界报错; · add_dependencies(... compile_gpu_shaders)。 验收:build 零告警;ctest 19/19;库内注册表 SHA256 仍为 3D47775F…84D (逐字节不变);CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变;样例退出码 0。 --- AGENTS.md | 3 +- CMakeLists.txt | 10 +- docs/history.md | 4 +- examples/fusion_custom_layer_main.cpp | 131 +++++++++++++++++++++----- 4 files changed, 122 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index eb7805b3..60ec5153 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -182,7 +182,8 @@ GPT 序列展平: 列序 i = b*seq + t(batch-major,全局唯一约定) 2. **生成**:`tools/fused_generate.hpp`(原独立工具 `gen_fused`)对**每个结构发三份**:V0 全 f32(键 = `key`)/ **V1 运行期精度分派**(键 = `key#x`)/ V2 native16(键 = `key#a`,按 `expr_prec_sig_native16` 的**结构谓词**判定)→ 经 `emitter_registry` 选后端(默认 `"glsl"` = `GlslEmitter`)生成 GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。**不经 `.bin` 中间序列化**(`write_registry/read_registry` 已删除)。 - **精度签名不再是构建期集合**("签名从身份降级为参数"):V1 的 GLSL 对每个输入/输出各声明 f32 + `float16_t` 双视图,加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支,**一份 shader 覆盖任意签名**。因此运行期**不存在 miss、不存在边界 cast 回退、不存在回填清单**(`tools/prec_backfill.txt` 已删除,`NN_PREC_TRACE` 的"补变体"流程随之失效)。V1 对每个结构无条件生成 ⇒ 任何签名都命中。 - **新增调用点无需改动其他文件**(实测):在 Layer 头里新写一个 `dsl::compute`(其结构不在任何 dry-run / 模型 pass 覆盖内)→ 锚点独有 13 → 14、结构 84 → 85、注册表新增 `key` / `key#x` / `key#a` 三条,**其他文件零改动**。 -- **库外使用者:`nn_enable_gpu_fusion( MAIN <含 main 的源>)`**(`CMakeLists.txt`):使用者在自己的层头里写 `dsl::compute` + 在 CMake 加这一行 → 自动生成**该目标专属**的 `fused_registry.hpp` 并加进它的 include path。机制 = 收集器由**库内收集逻辑**(`tools/scan_exprs.cpp`:锚点 + dry-run + 模型 pass + 显式登记)**加上本目标的全部 TU**(`-DNN_EXPR_SCAN` 编译,让使用者的调用点自登记)组成。**⚠ 必须保留库内收集逻辑**:84 个结构里只有 13 个来自编译期锚点、71 个来自 dry-run/模型 pass,"纯锚点收集器"会漏库自身的结构。样例见 `examples/fusion_custom_layer*`(自研层 → 应用注册表多出 `94a2cc052598fe09` + `#x`,库内文件零改动)。 +- **库外使用者:`nn_enable_gpu_fusion( MAIN <含 main 的源>)`**(`CMakeLists.txt`):使用者在自己的层头里写 `dsl::compute` + 在 CMake 加这一行 → 自动生成**该目标专属**的 `fused_registry.hpp` 并加进它的 include path。机制 = 收集器由**库内收集逻辑**(`tools/scan_exprs.cpp`:锚点 + dry-run + 模型 pass + 显式登记)**加上本目标的全部 TU**(`-DNN_EXPR_SCAN` 编译,让使用者的调用点自登记)组成。**⚠ 必须保留库内收集逻辑**:84 个结构里只有 13 个来自编译期锚点、71 个来自 dry-run/模型 pass,"纯锚点收集器"会漏库自身的结构。**⚠ include 顺序有语义**:`${NNF_OUT_DIR}` 必须排在 `${CMAKE_BINARY_DIR}/generated` 之前(后者含库自身注册表与手写原语 shader 嵌入头),否则 `__has_include("fused_registry.hpp")` 会解析到库的那一份、使用者的自定义结构全部闭合世界报错。样例见 `examples/fusion_custom_layer*`。 +- **端到端验收(库外 + f16 分派)**:`build/fusion_custom_layer_example.exe` 实测 —— CPU f32 `0.6712035`(差 0);**GPU f32(V0)`0.6712036`(差 5.96e-08)**;**GPU f16(V1 运行期精度分派)`0.6708984`(差 3.05e-04,f16 舍入内)**。该表达式含 rparams ⇒ native16 谓词不通过 ⇒ f16 那一跑命中的必然是 `key#x` 分派 shader;注册表若漏了它就是闭合世界硬报错,所以"跑通"本身就是断言。 - **库是 header-only,多 TU 必须可链接**:头里的类外定义一律要 `inline`(曾漏 `GpuBuffer::~GpuBuffer` → ≥2 个 TU 的程序链接期 duplicate symbol,`examples/fusion_custom_layer*` 抓到)。 - **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`)。 diff --git a/CMakeLists.txt b/CMakeLists.txt index ca0ce5b9..7a78a86e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -537,8 +537,14 @@ if(Vulkan_FOUND) DEPENDS ${target}_nnfusion_collect ${SCAN_SRC} ${FUSED_GEN_SRC} ${_nnf_inputs} COMMENT "[nn] 为 ${target} 收集 + 合成融合 shader → ${_nnf_reg}") add_custom_target(${target}_nnfusion_registry DEPENDS ${_nnf_reg}) - target_include_directories(${target} PRIVATE ${NNF_OUT_DIR}) - add_dependencies(${target} ${target}_nnfusion_registry) + # ⚠ include 顺序有意义:本目标自己的 `fused_registry.hpp` 必须排在 + # `${CMAKE_BINARY_DIR}/generated` **之前**——后者含库自身的注册表 + # (不含使用者的自定义结构)与手写原语 shader 的嵌入头。放在前面 + # 才能让 `__has_include("fused_registry.hpp")`(见 + # backend/compute_vk_backend.hpp)解析到本目标专属的那一份。 + target_include_directories(${target} PRIVATE + ${NNF_OUT_DIR} ${CMAKE_BINARY_DIR}/generated) + add_dependencies(${target} ${target}_nnfusion_registry compile_gpu_shaders) endfunction() if(NOT GLSLC) diff --git a/docs/history.md b/docs/history.md index b5e6eff3..6c638d0f 100644 --- a/docs/history.md +++ b/docs/history.md @@ -57,7 +57,9 @@ expr_opt / expr_registry / expr_spec)「历史状态类注释」摘录。整 - **落地**:CMake 函数 `nn_enable_gpu_fusion( MAIN <含 main 的源> [OUT_DIR <目录>])`。收集器 = **库内收集逻辑**(`tools/scan_exprs.cpp`)+ **本目标的全部 TU**(`-DNN_EXPR_SCAN` 编译,令使用者调用点经 `FusedAnchor` 自登记)链接成一个可执行文件,产物目录只作该目标的私有 include 目录。样例 `examples/fusion_custom_layer{,.hpp,_main.cpp}`("库外使用者"形态:自定义层 + 一行 CMake)。 - **实测**:样例目标的注册表 = 库内 203 条 + 自研层 `94a2cc052598fe09` / `94a2cc052598fe09#x`(该表达式含 rparams → native16 谓词不通过,无 `#a`,符合预期);样例可执行文件运行结果与期望值 `0.6712035` 逐位一致。库内文件零改动。 - **顺带修的 header-only 缺陷**:`backend/compute_vk_backend.hpp` 的 `GpuBuffer::~GpuBuffer()` 类外定义**缺 `inline`** → 任何 ≥2 个 TU 的程序链接期 `duplicate symbol`(样例两 TU 复现)。已加 `inline`。这条此前从未暴露,因为库自身的可执行目标都是单 TU。 -- **未做**:把样例注册进 ctest(本次保持 ctest 19/19 口径不变;样例由默认构建覆盖编译,运行时断言需手工跑 `build/fusion_custom_layer_example.exe`)。 +- **GPU 端到端验收(本轮补齐)**:样例原先只验 CPU——而 CPU 走模板求值、与注册表无关,等于**没验到本 PR 的核心机制**。补上 GPU 后实测:CPU f32 `0.6712035`(差 0);GPU f32(V0)`0.6712036`(差 5.96e-08);**GPU f16(V1 运行期精度分派)`0.6708984`(差 3.05e-04,f16 舍入内)**。该表达式含 rparams ⇒ `expr_prec_sig_native16` 谓词不通过 ⇒ f16 那一跑命中的必然是 `key#x` 分派 shader;注册表若漏它就是闭合世界硬报错,"跑通"本身即断言。 +- **CMake include 顺序有语义(踩坑)**:`nn_enable_gpu_fusion` 起初只把 `${NNF_OUT_DIR}` 加进目标,样例 GPU 初始化报 `matmul SPIR-V bytecode not embedded`——手写原语 shader 的嵌入头在 `${CMAKE_BINARY_DIR}/generated`;但该目录**同时含库自身的 `fused_registry.hpp`**。最终把两者都加、且 `${NNF_OUT_DIR}` **排在前面**(`__has_include` 取第一个匹配),否则使用者的自定义结构全部闭合世界报错。已写进 AGENTS §7。 +- **未做**:把样例注册进 ctest(本次保持 ctest 19/19 口径不变;样例由默认构建覆盖编译,运行时断言需手工跑 `build/fusion_custom_layer_example.exe`,无 Vulkan 时退出码 77 = skip)。 ## 融合 matmul BK 取值 A/B 流水(原位置 include/neuralnet.cpp/expr_glsl_gen.hpp:262-269、303-310) - 类型:性能 A/B / 否决方案 diff --git a/examples/fusion_custom_layer_main.cpp b/examples/fusion_custom_layer_main.cpp index cf7fa8c3..aa50b53f 100644 --- a/examples/fusion_custom_layer_main.cpp +++ b/examples/fusion_custom_layer_main.cpp @@ -1,13 +1,17 @@ // ═══════════════════════════════════════════════════════════════════════════ // examples/fusion_custom_layer_main.cpp — A2 验收样例的可执行入口 // -// 验收断言(构建期即完成,见 CMake 的 nn_enable_gpu_fusion): -// 本目标**只**依赖自己的 `fused_registry.hpp`(由 nn_enable_gpu_fusion -// 用本应用的 TU + 库内收集逻辑生成),该注册表里必须出现自研表达式 -// 的结构键(以及 `#x` 运行期精度分派变体)——**库内文件零改动**。 +// 这是"写 dsl::compute → 只跑构建 → **GPU 能跑**"的端到端验收: // -// 运行时这里只验证 CPU 路径可用(模板求值,与注册表无关);GPU 路径的 -// 结构登记与分派命中由 A1 的端到端实测覆盖(见 docs/history.md)。 +// ① 结构收集:本目标**只**依赖自己的 `fused_registry.hpp`(由 +// `nn_enable_gpu_fusion` 用本应用的 TU + 库内收集逻辑生成)。该注册表里 +// 必须出现自研表达式的结构键 `key` 与 `key#x`(运行期精度分派)—— +// 库内文件零改动。 +// ② **f16 分派路径**:下面用 f16 张量 + f16 输出跑同一个自研表达式。本表达式 +// 含 rparams ⇒ `expr_prec_sig_native16` 谓词不通过 ⇒ 命中的必然是 +// `key#x`(运行期精度分派 shader,双视图 + PC `prec`)。注册表若缺它, +// 运行期会**闭合世界硬报错**——所以"跑通"本身就是断言。 +// ③ f32 路径(V0)与 CPU 参考逐位/紧容差对拍。 // ═══════════════════════════════════════════════════════════════════════════ #include "fusion_custom_layer.hpp" @@ -16,38 +20,121 @@ #include #include +namespace +{ + +// exp(0.5*0.7) / (sqrt(2.0) + 0.7) +constexpr float kExpect = 1.419067549f / (1.414213562f + 0.7f); + +float abs_diff(float a, float b) { return a > b ? a - b : b - a; } + +} // namespace + int main() { + // ── CPU 参考 ───────────────────────────────────────────────────────── nn::CpuEngine cpu; nn::Tensor x = cpu.create_tensor(4, 3, nn::Precision::F32, nn::InitSpec::constant(0.5f)); nn::Tensor y = cpu.create_tensor(4, 3, nn::Precision::F32, nn::InitSpec::constant(2.0f)); - auto out = nn_example::fused_custom_op(cpu, x, y, 0.7f); - if (!out) + auto cout_r = nn_example::fused_custom_op(cpu, x, y, 0.7f); + if (!cout_r) { - std::fprintf(stderr, "[example] fused_custom_op 失败: %s\n", - out.error().message.c_str()); + std::fprintf(stderr, "[example] CPU 求值失败: %s\n", + cout_r.error().message.c_str()); return 1; } - std::vector buf(12, 0.0f); - if (auto r = cpu.read(*out, std::span(buf)); !r) + std::vector cbuf(12, 0.0f); + if (auto r = cpu.read(*cout_r, std::span(cbuf)); !r) { - std::fprintf(stderr, "[example] read 失败\n"); + std::fprintf(stderr, "[example] CPU read 失败\n"); return 1; } - // exp(0.5*0.7) / (sqrt(2.0) + 0.7) - const float expect = 1.419067549f / (1.414213562f + 0.7f); - const float got = buf[0]; - const float diff = got > expect ? got - expect : expect - got; - std::printf("[example] 自研融合层 CPU 结果 %.7f(期望 %.7f,差 %.3e)\n", - static_cast(got), static_cast(expect), - static_cast(diff)); - if (diff > 1e-5f) + const float cpu_val = cbuf[0]; + std::printf("[example] CPU f32 = %.7f(期望 %.7f,差 %.3e)\n", + static_cast(cpu_val), static_cast(kExpect), + static_cast(abs_diff(cpu_val, kExpect))); + if (abs_diff(cpu_val, kExpect) > 1e-5f) { - std::fprintf(stderr, "[example] 数值不符\n"); + std::fprintf(stderr, "[example] CPU 数值不符\n"); return 1; } + +#ifdef NN_HAS_VULKAN + // ── GPU ────────────────────────────────────────────────────────────── + nn::GpuBackend& backend = nn::GpuBackend::instance(); + if (auto ir = backend.initialize(); !ir) + { + std::fprintf(stderr, "[example] GPU 初始化失败(跳过):%s\n", + ir.error().message.c_str()); + return 77; // ctest 约定:77 = skip + } + nn::GpuEngine gpu(backend); + + // ③ f32 路径(注册表命中 V0) + { + nn::Tensor gx = gpu.create_tensor(4, 3, nn::Precision::F32, + nn::InitSpec::constant(0.5f)); + nn::Tensor gy = gpu.create_tensor(4, 3, nn::Precision::F32, + nn::InitSpec::constant(2.0f)); + auto g_r = nn_example::fused_custom_op(gpu, gx, gy, 0.7f); + if (!g_r) + { + std::fprintf(stderr, "[example] GPU f32 融合未命中(闭合世界): %s\n", + g_r.error().message.c_str()); + return 1; + } + std::vector gb(12, 0.0f); + if (auto r = gpu.read(*g_r, std::span(gb)); !r) + { + std::fprintf(stderr, "[example] GPU f32 read 失败\n"); + return 1; + } + std::printf("[example] GPU f32 (V0) = %.7f(与 CPU 差 %.3e)\n", + static_cast(gb[0]), + static_cast(abs_diff(gb[0], cpu_val))); + if (abs_diff(gb[0], cpu_val) > 1e-5f) + { + std::fprintf(stderr, "[example] GPU f32 与 CPU 不符\n"); + return 1; + } + } + + // ② f16 路径(注册表命中 V1 运行期精度分派:双视图 + PC `prec`) + { + nn::Tensor gx = gpu.create_tensor(4, 3, nn::Precision::F16, + nn::InitSpec::constant(0.5f)); + nn::Tensor gy = gpu.create_tensor(4, 3, nn::Precision::F16, + nn::InitSpec::constant(2.0f)); + auto g_r = nn_example::fused_custom_op(gpu, gx, gy, 0.7f, + nn::Precision::F16); + if (!g_r) + { + std::fprintf(stderr, + "[example] GPU f16 分派变体未命中(闭合世界): %s\n", + g_r.error().message.c_str()); + return 1; + } + std::vector hb(12, nn::f16(0.0f)); + if (auto r = gpu.read(*g_r, std::span(hb)); !r) + { + std::fprintf(stderr, "[example] GPU f16 read 失败\n"); + return 1; + } + const float gv = static_cast(hb[0]); + std::printf("[example] GPU f16 (V1) = %.7f(与 CPU 差 %.3e,f16 容差内)\n", + static_cast(gv), + static_cast(abs_diff(gv, cpu_val))); + if (abs_diff(gv, cpu_val) > 2e-3f) + { + std::fprintf(stderr, "[example] GPU f16 与 CPU 不符\n"); + return 1; + } + } + std::puts("[example] GPU OK"); +#endif + std::puts("[example] OK"); return 0; } From 7c3af60eb406d421239978416b37f83b50b22af7 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 19:12:17 +0800 Subject: [PATCH 25/30] =?UTF-8?q?=E8=AF=8A=E6=96=AD=EF=BC=9A`NN=5FSCAN=5FD?= =?UTF-8?q?UMP=5FSOURCES=3D1`=20=E9=80=90=E6=9D=A1=E6=89=93=E5=8D=B0?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E6=9D=A5=E6=BA=90=20+=20=E9=94=9A=E7=82=B9/d?= =?UTF-8?q?ry-run=20=E8=BE=B9=E7=95=8C=E5=AE=9E=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:⑥ 的目标是"删除扫描",此前只知"dry-run 贡献 71/84",**不知道那 71 条 为什么锚点看不到** —— 没这个答案就无法判断还能不能再进一步。 改动(纯诊断,env 门控,默认零打扰): · scan_exprs 的合并处计算 dry_keys / anchor_keys 两个集合; · NN_SCAN_DUMP_SOURCES=1 时逐条打印 `[scan][dry-only]` / `[scan][anchor-only]` + 结构摘要(raxis / mm / fold / nreg / ninstr / nview / viewkinds); · "[scan] 结构来源"台账行补 `dry-run 独有 N`。 实测(84 条结构):per-layer dry-run 38(模型 pass +25、显式登记 +8)、锚点 59, 重叠 46 → **锚点独有 13、dry-run 独有 25**。 dry-run 独有 25 条分类:fold 8(= 显式登记的掩码×偏置组合与通用样例)、含 matmul 段 7、RowAccess(12) 4(共享内存 SwiGLU 半偏移行切分)、RowGather(9)/ ColBroadcast(8) 若干(稀疏 CE 的 5 视图形态)。共同点:**结构由运行期配置 决定**(视图种类/项数/掩码组合),类型层面推不出;符号实例 `Expr{}` 与真实 实例折叠出不同 key —— 最硬的证据是镜像对:dry-only `784781c7b82fa76c` 与 anchor-only `28cdf7c6d5df0e1d` 摘要逐字段相同,只有 key 不同。 结论:锚点与 dry-run **互补、不冗余**,单独删任一方都会丢结构。 `scan_exprs` 这一构建步骤本身删不掉;能删的只有签名维(已删,工作量减半)。 验收:build 零告警;ctest 19/19;**build/generated/fused_registry.hpp 与改前 SHA256 逐字节一致**(3D47775F77D34888069482F3BFDA410B837D55F9A8A68FE5B5A15A5BA1B7F64D) ——诊断只加打印,不改产物;CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变;样例仍 OK。 --- AGENTS.md | 2 +- docs/development/02-operator-fusion.md | 10 +++-- docs/history.md | 14 ++++++ tools/scan_exprs.cpp | 61 +++++++++++++++++++++++++- 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 60ec5153..094f5374 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -178,7 +178,7 @@ GPT 序列展平: 列序 i = b*seq + t(batch-major,全局唯一约定) - 主要入口:`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 内联表达式后重跑构建即可)——`scan_exprs` 同进程**收集 + 生成**: - 1. **收集**:只收集**结构**(= 唯一需要构建期枚举的东西)。`FusedAnchor` 在静态初始化期按表达式**类型**自登记(编译期可达,dry-run 跑不到的分支也覆盖),再由 per-layer dry-run + 模型 pass 执行补齐"运行期配置相关"的结构;fold spec 与 matmul trans 族由 `scan_exprs` 显式登记。**只跑 f32 一遍**——实测 f16 遍对结构贡献为 0(跳过它签名 66 → 0、结构恒 84),该两遍已删除。 + 1. **收集**:只收集**结构**(= 唯一需要构建期枚举的东西)。两个来源**互补、不冗余**(`NN_SCAN_DUMP_SOURCES=1` 可逐条打印来源):`FusedAnchor` 在静态初始化期按表达式**类型**自登记(编译期可达,dry-run 跑不到的分支也覆盖);per-layer dry-run + 模型 pass 执行补齐**运行期配置决定**的变体(视图种类/项数在类型层面推不出,符号实例 `Expr{}` 与真实实例会折叠出不同 key);fold spec(3 掩码 × 2 偏置的 5 个组合等 8 条)与 matmul trans 族由 `scan_exprs` **显式登记**。**只跑 f32 一遍**——实测 f16 遍对结构贡献为 0(跳过它签名 66 → 0、结构恒 84),该两遍已删除。**实测分工(84 条)**:per-layer dry-run 38(含模型 pass +25、显式登记 +8)、锚点 59,重叠 46 → **锚点独有 13、dry-run 独有 25**。所以**任何一方单独删掉都会丢结构**——dry-run/model pass 是结构的主要来源,删不掉。 2. **生成**:`tools/fused_generate.hpp`(原独立工具 `gen_fused`)对**每个结构发三份**:V0 全 f32(键 = `key`)/ **V1 运行期精度分派**(键 = `key#x`)/ V2 native16(键 = `key#a`,按 `expr_prec_sig_native16` 的**结构谓词**判定)→ 经 `emitter_registry` 选后端(默认 `"glsl"` = `GlslEmitter`)生成 GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。**不经 `.bin` 中间序列化**(`write_registry/read_registry` 已删除)。 - **精度签名不再是构建期集合**("签名从身份降级为参数"):V1 的 GLSL 对每个输入/输出各声明 f32 + `float16_t` 双视图,加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支,**一份 shader 覆盖任意签名**。因此运行期**不存在 miss、不存在边界 cast 回退、不存在回填清单**(`tools/prec_backfill.txt` 已删除,`NN_PREC_TRACE` 的"补变体"流程随之失效)。V1 对每个结构无条件生成 ⇒ 任何签名都命中。 - **新增调用点无需改动其他文件**(实测):在 Layer 头里新写一个 `dsl::compute`(其结构不在任何 dry-run / 模型 pass 覆盖内)→ 锚点独有 13 → 14、结构 84 → 85、注册表新增 `key` / `key#x` / `key#a` 三条,**其他文件零改动**。 diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index bfc0bb56..e4551209 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -267,7 +267,11 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba 手写 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`):结构数 **71(仅 dry-run/模型 pass)→ 84(+锚点)**,多出的 13 条都是编译进来的真实调用点(含 matmul、归约、16 个 vp 槽的宽表达式等)。⇒ 锚点与 dry-run **互补**:dry-run/模型 pass 是结构的主要来源(71/84),锚点覆盖它跑不到的调用点(13/84)。 +- **覆盖语义**:调用点在函数体里,**函数被编译即实例化**——与运行期是否走到该分支无关,因此是"编译期可达"而非"运行期可达"。A/B 实测(`-DNN_SCAN_NO_ANCHOR`):结构数 **71(仅 dry-run/模型 pass)→ 84(+锚点)**,多出的 13 条都是编译进来的真实调用点(含 matmul、归约、16 个 vp 槽的宽表达式等)。 +- **两者互补,不冗余**(`NN_SCAN_DUMP_SOURCES=1` 逐条打印来源,实测 84 条):per-layer dry-run 38(含模型 pass +25、显式登记 +8)、锚点 59,**重叠 46 → 锚点独有 13、dry-run 独有 25**。所以**单独删任何一方都会丢结构**: + - **dry-run 独有 25 条**按形态分:**fold 8 条**(= 显式登记的掩码×偏置组合与通用 fold 样例,见下)、**含 matmul 段 7 条**(注意力反向 / CE / 优化器步)、**RowAccess(12) 4 条**(共享内存 SwiGLU 的半偏移行切分)、**RowGather(9)/ColBroadcast(8) 若干**(稀疏 CE 的 5 视图形态 `[0,8,8,8,8]` 等)——共同点是**结构由运行期配置决定**(视图种类/项数/掩码组合),类型层面推不出来,符号实例 `Expr{}` 与真实实例会折叠出**不同 key**(实测存在"视图种类与指令数完全相同、只有 key 不同"的镜像对,如 dry-only `784781c7b82fa76c` ↔ anchor-only `28cdf7c6d5df0e1d`)。 + - **锚点独有 13 条**是镜像面:dry-run/模型 pass 没跑到的编译期可见调用点(最著名的是 `9ca81b4967cdfa20`——关掉锚点后 `text_infer` KV-cache 增量解码在 GPU 上闭合世界硬报错)。 + - **结论**:dry-run + 模型 pass 是结构的主要来源,**删不掉**;锚点补它跑不到的部分。这也是"扫描步骤本身无法删除"的精确根因。 - **常驻台账**:锚点登记进独立注册表 `anchor_registry()`,`scan_exprs` 末尾合并并打印来源分项(见下)。**锚点独有数突然变大 = 有新的层路径没纳入任何驱动**(回归信号)。 - **只登记结构**:⚠ 自登记只能给**结构**(`sig=0`)。**精度签名由生成期的运行期分派变体覆盖**(见上节"精度签名已不是构建期集合")——自登记 + V1 合起来才构成完整的"零注解"链路:结构靠编译期可达,签名靠运行期参数。 - **模型级 pass(结构侧)**:per-layer dry-run 块只覆盖"层被单独造出来"的路径;有些调用点只在**完整模型**里才执行(GPT/RAPT 的 LM head、PatchEmbedding、模型级位置编码等)。`scan_exprs` 因此跑一遍**配置矩阵**(只跑 f32):`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 + 锚点覆盖)。 @@ -277,9 +281,9 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba 保留的是**整模型跑不到**的来源:优化器(5 变体)、损失(MSE / CE 稠密 / CE 稀疏)、RoPE `apply_step`(增量推理)、CSA×4(掩码/偏置组合,含模型未覆盖的 doc 变体)、MHA、以及 `scan_exprs` 内的**显式测试覆盖登记**(matmul+bias+relu / bmm_reduce / reduce_consts)。 - **常驻分项台账**(每次构建打印,结构侧): ``` - [scan] 结构来源:dry-run 38(含模型 pass +25、显式登记 +8) + 锚点 59 → 合并 84(锚点独有 13) + [scan] 结构来源:dry-run 38(含模型 pass +25、显式登记 +8) + 锚点 59 → 合并 84(锚点独有 13、dry-run 独有 25) ``` - A/B 开关:`-DNN_SCAN_NO_ANCHOR`(关锚点)/ `-DNN_SCAN_NO_DRYRUN`(关 dry-run/模型 pass),均只影响登记集合、不改语义。 + A/B 开关:`-DNN_SCAN_NO_ANCHOR`(关锚点)/ `-DNN_SCAN_NO_DRYRUN`(关 dry-run/模型 pass),均只影响登记集合、不改语义。逐条来源用 `NN_SCAN_DUMP_SOURCES=1` 打印(`[scan][dry-only]` / `[scan][anchor-only]` + 结构摘要)。 - **run-only 签名回填已删除**(A1e):`--f16` 下曾有一批只有真实训练才暴露的签名(注意力 fold 的文档掩码 5 输入形态、MNIST/CNN f16 训练的 8 条"f32 入 + f16 出"混合签名),靠 `NN_PREC_TRACE=1` 的 `[prec][miss]` 收集后写进 `tools/prec_backfill.txt`。**该清单与其工作流已随 A1 整体删除**——V1 分派 shader 覆盖任意签名,f16 训练实测 `miss=0` 且边界 cast 归因表为空。历史清单条目与复现命令归档在 `docs/history.md`。 - **契约回归**:上述三条 key 语义 + "符号实例 key ≡ 真实实例 key" 由 `src/expr_fused_key_test.cpp` 锁定(并入 `expr_cpu_test` 聚合目标,ctest 目标数不变)。 diff --git a/docs/history.md b/docs/history.md index 6c638d0f..33ef8a89 100644 --- a/docs/history.md +++ b/docs/history.md @@ -61,6 +61,20 @@ expr_opt / expr_registry / expr_spec)「历史状态类注释」摘录。整 - **CMake include 顺序有语义(踩坑)**:`nn_enable_gpu_fusion` 起初只把 `${NNF_OUT_DIR}` 加进目标,样例 GPU 初始化报 `matmul SPIR-V bytecode not embedded`——手写原语 shader 的嵌入头在 `${CMAKE_BINARY_DIR}/generated`;但该目录**同时含库自身的 `fused_registry.hpp`**。最终把两者都加、且 `${NNF_OUT_DIR}` **排在前面**(`__has_include` 取第一个匹配),否则使用者的自定义结构全部闭合世界报错。已写进 AGENTS §7。 - **未做**:把样例注册进 ctest(本次保持 ctest 19/19 口径不变;样例由默认构建覆盖编译,运行时断言需手工跑 `build/fusion_custom_layer_example.exe`,无 Vulkan 时退出码 77 = skip)。 +## 「锚点 vs dry-run」边界实测刻画(2026-10-01,round 15) + +- 类型:覆盖缺口 / 否决方案("删掉 dry-run") +- **动机**:⑥ 的目标是"删除扫描"。此前只知"dry-run 贡献 71/84",但**不知道那 71 条为什么锚点看不到**——没有这个答案就无法判断"能否再进一步"。为此给 `scan_exprs` 加了 env 门控诊断 `NN_SCAN_DUMP_SOURCES=1`:逐条打印 `[scan][dry-only]` / `[scan][anchor-only]` 加结构摘要(raxis / mm / fold / nreg / ninstr / nview / viewkinds)。 +- **实测(84 条结构)**:per-layer dry-run 38(含模型 pass +25、显式登记 +8)、锚点 59,**重叠 46 → 锚点独有 13、dry-run 独有 25**。 +- **dry-run 独有 25 条的分类**: + - **fold 8 条** = 显式登记的掩码×偏置组合与通用 fold 样例(`fold=1`,viewkinds `[0]`/`[0,0,0]`/`[0,0,0,10]`/`[0,0,0,7,11]`/`[0,0,0,10,7,11]`); + - **含 matmul 段 7 条**(`mm=1`:注意力反向 / CE / 优化器步); + - **RowAccess(12) 4 条**(共享内存 SwiGLU 的半偏移行切分); + - **RowGather(9)/ColBroadcast(8) 若干**(稀疏 CE 的 5 视图形态 `[0,8,8,8,8]` 等)。 + - 共同点:**结构由运行期配置决定**(视图种类 / 项数 / 掩码组合),类型层面推不出来;符号实例 `Expr{}` 与真实实例折叠出**不同 key**。最直接的证据是**镜像对**——dry-only `784781c7b82fa76c` 与 anchor-only `28cdf7c6d5df0e1d` 的摘要逐字段相同(`raxis=-1 mm=0 fold=0 nreg=2 ninstr=2 nview=2 viewkinds=[0,0]`),只有 key 不同。 +- **锚点独有 13 条**是镜像面:dry-run/模型 pass 没跑到的编译期可见调用点。最著名的是 `9ca81b4967cdfa20`——关掉锚点(`-DNN_SCAN_NO_ANCHOR`)后 `text_infer`(KV-cache 增量解码)在 GPU 上闭合世界硬报错。 +- **结论(否决"删掉 dry-run")**:锚点与 dry-run **互补、不冗余**,单独删任何一方都会丢结构(dry-only 25 / anchor-only 13)。dry-run + 模型 pass 是结构的**主要**来源,`scan_exprs` 这一**构建步骤本身无法删除**;能删的只有签名维(已完成,扫描工作量减半)。若将来真要清零,路径是"把运行期配置维度类型化"(让视图种类/项数进入表达式类型),使符号实例等价于真实实例——那是另一次架构变更,不属于本阶段。 + ## 融合 matmul BK 取值 A/B 流水(原位置 include/neuralnet.cpp/expr_glsl_gen.hpp:262-269、303-310) - 类型:性能 A/B / 否决方案 - 内容:40HX 变体 trade-off 记录(% = vs 单缓冲耗时,负=更快):BK=32 单缓冲(16KB) 为深网格基线、 diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index 3a6e88ae..a0b964b6 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -594,15 +594,72 @@ int main(int argc, char* argv[]) // 调用点(配置分支、未被 dry-run 造过的层路径)。两者合并后写盘。 { auto& anchor = nn::dsl::anchor_registry(); + // NN_SCAN_DUMP_SOURCES=1:逐条打印"只有执行 Layer/模型才拿得到"的结构。 + // 这是"能否进一步删掉 dry-run"的唯一依据——锚点按表达式类型登记,但 + // **符号实例(`Expr{}`)与真实实例可能折叠出不同结构**(运行期配置 + // 维度:视图种类、输入个数、掩码组合…);这些结构锚点看不到。 + const bool dump_src = [] { +#if defined(_MSC_VER) + char* b = nullptr; std::size_t n = 0; + _dupenv_s(&b, &n, "NN_SCAN_DUMP_SOURCES"); + const bool v = (b != nullptr && b[0] == '1'); + std::free(b); + return v; +#else + const char* v = std::getenv("NN_SCAN_DUMP_SOURCES"); + return v != nullptr && v[0] == '1'; +#endif + }(); + std::unordered_set dry_keys; + for (const auto& s : reg.specs) + dry_keys.insert(nn::expr_spec_key(s)); + std::unordered_set anchor_keys; + for (const auto& s : anchor.specs) + anchor_keys.insert(nn::expr_spec_key(s)); + const auto summarize = [](const nn::ExprSpec& s) + { + const int raxis = nn::expr_spec_reduce_axis(s); + std::string v; + for (const auto& x : s.views) + { + if (!v.empty()) v += ","; + v += std::to_string(static_cast(x.kind)); + } + return "raxis=" + std::to_string(raxis) + + " mm=" + (s.matmul ? "1" : "0") + + " fold=" + (s.fold ? "1" : "0") + + " nreg=" + std::to_string(s.num_regs) + + " ninstr=" + std::to_string(s.instrs.size()) + + " nview=" + std::to_string(s.views.size()) + + " viewkinds=[" + v + "]"; + }; + std::size_t n_dry_only = 0; + for (const auto& s : reg.specs) + { + const std::string k = nn::expr_spec_key(s); + if (anchor_keys.count(k)) + continue; + ++n_dry_only; + if (dump_src) + std::printf("[scan][dry-only] %s %s\n", k.c_str(), summarize(s).c_str()); + } + for (const auto& s : anchor.specs) + { + const std::string k = nn::expr_spec_key(s); + if (dry_keys.count(k)) + continue; + if (dump_src) + std::printf("[scan][anchor-only] %s %s\n", k.c_str(), summarize(s).c_str()); + } for (const auto& s : anchor.specs) reg.add(s); std::printf("[scan] 结构来源:dry-run %zu(含模型 pass +%zu、显式登记 +%zu)" - " + 锚点 %zu → 合并 %zu(锚点独有 %zu)\n", + " + 锚点 %zu → 合并 %zu(锚点独有 %zu、dry-run 独有 %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); + reg.specs.size() - n_before_anchor, n_dry_only); // 锚点独有 = 手写 dry-run 覆盖不到的调用点(该数突然变大 → 有新层路径 // 没纳入 dry-run)。 if (anchor.specs.empty() || n_before_anchor == reg.specs.size()) From bf8d10ea21f4baa22e373a9dcd0565bdca2fda29 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 19:20:18 +0800 Subject: [PATCH 26/30] =?UTF-8?q?A2c:=20=E6=89=A9=E5=B1=95=E6=9D=A5?= =?UTF-8?q?=E6=BA=90=E8=AF=8A=E6=96=AD=EF=BC=88=E5=90=AB=E6=8C=87=E4=BB=A4?= =?UTF-8?q?=E5=BA=8F=E5=88=97=EF=BC=89+=20=E5=AE=9E=E6=B5=8B=E7=AC=AC?= =?UTF-8?q?=E4=BA=8C=E7=A7=8D=E8=87=AA=E5=AE=9A=E4=B9=89=E7=AE=97=E5=AD=90?= =?UTF-8?q?=E5=BD=A2=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 起因:round 15 测出"25 条结构只有执行 Layer/模型才拿得到",由此引出一个更要紧 的问题——**库外自定义层没有 dry-run,`nn_enable_gpu_fusion` 是否就不可靠?** 实验(而非推理):给样例加第二种自研形态——归一化 `rsqrt(col_reduce_sum(x)·inv + ε)`,与库内 LayerNorm/RMSNorm 的 std_inv 同形 (raxis=1、viewkinds=[0,8],正落在 dry-run 独有 25 条的形态类别里),**只用锚点** 在 GPU 上以 f16 跑通: [example] GPU f16 归一化形态 = 1.2246094 ⇒ 锚点对常见自定义层可靠,A2 机制成立。 顺带排除一个错误假设:曾怀疑"符号实例常量默认值(全 0)触发 CSE 合并出不同 结构"。查 expr_opt.hpp:CSE key = op+a.kind/idx+b.kind/idx+c.kind/idx,**不含 常量值** ⇒ 与值无关,假设排除。同时发现此前的"镜像对"是误判——粗摘要相同但 指令序列完全不同(nconst=2 [17(1/0,1/1);19(0/0,2/0,2/1)] vs nconst=1 [1(2/0,0/0);10(1/0,0/0)]),是两个不同表达式。 诊断增强:NN_SCAN_DUMP_SOURCES=1 现在除形态摘要外还打印 nconst/nrparam 与**完整指令序列**(op(a.kind/idx,b.kind/idx,c.kind/idx)), 使"同源不同 key"这类判断不再依赖粗摘要。 验收:build 零告警;注册表 SHA256 仍 3D47775F…84D(逐字节不变); 样例 CPU/GPU f32/f16/归一化四路全过、退出码 0。 --- AGENTS.md | 2 +- docs/history.md | 3 +++ examples/fusion_custom_layer.hpp | 16 ++++++++++++++++ examples/fusion_custom_layer_main.cpp | 25 +++++++++++++++++++++++++ tools/scan_exprs.cpp | 18 ++++++++++++++++-- 5 files changed, 61 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 094f5374..1f4a466a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -183,7 +183,7 @@ GPT 序列展平: 列序 i = b*seq + t(batch-major,全局唯一约定) - **精度签名不再是构建期集合**("签名从身份降级为参数"):V1 的 GLSL 对每个输入/输出各声明 f32 + `float16_t` 双视图,加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支,**一份 shader 覆盖任意签名**。因此运行期**不存在 miss、不存在边界 cast 回退、不存在回填清单**(`tools/prec_backfill.txt` 已删除,`NN_PREC_TRACE` 的"补变体"流程随之失效)。V1 对每个结构无条件生成 ⇒ 任何签名都命中。 - **新增调用点无需改动其他文件**(实测):在 Layer 头里新写一个 `dsl::compute`(其结构不在任何 dry-run / 模型 pass 覆盖内)→ 锚点独有 13 → 14、结构 84 → 85、注册表新增 `key` / `key#x` / `key#a` 三条,**其他文件零改动**。 - **库外使用者:`nn_enable_gpu_fusion( MAIN <含 main 的源>)`**(`CMakeLists.txt`):使用者在自己的层头里写 `dsl::compute` + 在 CMake 加这一行 → 自动生成**该目标专属**的 `fused_registry.hpp` 并加进它的 include path。机制 = 收集器由**库内收集逻辑**(`tools/scan_exprs.cpp`:锚点 + dry-run + 模型 pass + 显式登记)**加上本目标的全部 TU**(`-DNN_EXPR_SCAN` 编译,让使用者的调用点自登记)组成。**⚠ 必须保留库内收集逻辑**:84 个结构里只有 13 个来自编译期锚点、71 个来自 dry-run/模型 pass,"纯锚点收集器"会漏库自身的结构。**⚠ include 顺序有语义**:`${NNF_OUT_DIR}` 必须排在 `${CMAKE_BINARY_DIR}/generated` 之前(后者含库自身注册表与手写原语 shader 嵌入头),否则 `__has_include("fused_registry.hpp")` 会解析到库的那一份、使用者的自定义结构全部闭合世界报错。样例见 `examples/fusion_custom_layer*`。 -- **端到端验收(库外 + f16 分派)**:`build/fusion_custom_layer_example.exe` 实测 —— CPU f32 `0.6712035`(差 0);**GPU f32(V0)`0.6712036`(差 5.96e-08)**;**GPU f16(V1 运行期精度分派)`0.6708984`(差 3.05e-04,f16 舍入内)**。该表达式含 rparams ⇒ native16 谓词不通过 ⇒ f16 那一跑命中的必然是 `key#x` 分派 shader;注册表若漏了它就是闭合世界硬报错,所以"跑通"本身就是断言。 +- **端到端验收(库外 + f16 分派)**:`build/fusion_custom_layer_example.exe` 实测两种**自研算子形态**:①逐元素 `exp(|x|·α)/(√|y|+α)`;②归一化形态 `rsqrt(col_reduce_sum(x)·inv + ε)`(= 库内 LayerNorm/RMSNorm 的 `std_inv` 同形,`raxis=1`、`viewkinds=[0,8]`,属库里"只有执行 Layer 才拿得到"的那一类)。两者都只用**锚点**(自定义层没有 dry-run)→ 都命中并跑通 ⇒ `nn_enable_gpu_fusion` 对常见自定义层可靠。实测值:CPU f32 `0.6712035`(差 0);**GPU f32(V0)`0.6712036`(差 5.96e-08)**;**GPU f16(V1 运行期精度分派)`0.6708984`(差 3.05e-04,f16 舍入内)**;归一化形态 GPU f16 `1.2246094`。含 rparams ⇒ native16 谓词不通过 ⇒ f16 命中必然是 `key#x`;注册表若漏了它就是闭合世界硬报错,"跑通"本身即断言。**残余不确定性**:库内确有 25 条结构只有执行 Layer/模型才拿得到(锚点的符号实例 `Expr{}` 与运行期实例折叠结果不同);实测的两种自定义形态不在其中,但无法先验排除。若使用者的自定义层遇到闭合世界报错,下一步是给 `nn_enable_gpu_fusion` 加 `COLLECT ` 钩子(由使用者提供"跑一遍自己的层"的收集函数,等价于库内的模型 pass)。 - **库是 header-only,多 TU 必须可链接**:头里的类外定义一律要 `inline`(曾漏 `GpuBuffer::~GpuBuffer` → ≥2 个 TU 的程序链接期 duplicate symbol,`examples/fusion_custom_layer*` 抓到)。 - **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`)。 diff --git a/docs/history.md b/docs/history.md index 33ef8a89..6377cd79 100644 --- a/docs/history.md +++ b/docs/history.md @@ -74,6 +74,9 @@ expr_opt / expr_registry / expr_spec)「历史状态类注释」摘录。整 - 共同点:**结构由运行期配置决定**(视图种类 / 项数 / 掩码组合),类型层面推不出来;符号实例 `Expr{}` 与真实实例折叠出**不同 key**。最直接的证据是**镜像对**——dry-only `784781c7b82fa76c` 与 anchor-only `28cdf7c6d5df0e1d` 的摘要逐字段相同(`raxis=-1 mm=0 fold=0 nreg=2 ninstr=2 nview=2 viewkinds=[0,0]`),只有 key 不同。 - **锚点独有 13 条**是镜像面:dry-run/模型 pass 没跑到的编译期可见调用点。最著名的是 `9ca81b4967cdfa20`——关掉锚点(`-DNN_SCAN_NO_ANCHOR`)后 `text_infer`(KV-cache 增量解码)在 GPU 上闭合世界硬报错。 - **结论(否决"删掉 dry-run")**:锚点与 dry-run **互补、不冗余**,单独删任何一方都会丢结构(dry-only 25 / anchor-only 13)。dry-run + 模型 pass 是结构的**主要**来源,`scan_exprs` 这一**构建步骤本身无法删除**;能删的只有签名维(已完成,扫描工作量减半)。若将来真要清零,路径是"把运行期配置维度类型化"(让视图种类/项数进入表达式类型),使符号实例等价于真实实例——那是另一次架构变更,不属于本阶段。 +- **推论与实测反驳(round 16)**:既然 25 条"只有执行才拿得到",那**库外自定义层(没有 dry-run)是否就不可靠?** 直接用实验回答:给 `examples/fusion_custom_layer*` 加了第二种自研形态——**归一化形态** `rsqrt(col_reduce_sum(x)·inv + ε)`,与库内 LayerNorm/RMSNorm 的 `std_inv` 同形(`raxis=1`、`viewkinds=[0,8]`,正落在 dry-un-only 的形态类别里),**只用锚点**在 GPU 上以 f16 跑通(`1.2246094`,走 `key#x` 分派 shader)。⇒ **锚点对常见自定义层可靠**,A2 机制成立。 +- **同时排除了一个错误假设**:曾怀疑"符号实例的常量默认值(全 0)触发 CSE 合并出不同结构"。查 `expr_opt.hpp` 的 CSE key = `op(8)+a.kind/idx+b.kind/idx+c.kind/idx`,**不含常量值** ⇒ 与值无关,假设排除。(此前的"镜像对"是错的:粗摘要相同但指令序列完全不同——`nconst=2 instrs=[17(1/0,1/1);19(0/0,2/0,2/1)]` vs `nconst=1 instrs=[1(2/0,0/0);10(1/0,0/0)]`,是两个不同表达式,不是同一表达式的两个 key。) +- **仍记录的残余不确定性**:25 条 divergence 的**根因未定位**(已知不是 CSE-值依赖、不是视图种类-运行期选择——`ExprViewKind` 全部由类型决定)。若将来使用者的自定义层遇到闭合世界报错,下一步是给 `nn_enable_gpu_fusion` 加 `COLLECT ` 钩子(使用者提供"跑一遍自己的层"的收集函数,等价于库内模型 pass)。诊断工具已就位:`NN_SCAN_DUMP_SOURCES=1` 打印逐条来源 + 完整指令序列。 ## 融合 matmul BK 取值 A/B 流水(原位置 include/neuralnet.cpp/expr_glsl_gen.hpp:262-269、303-310) - 类型:性能 A/B / 否决方案 diff --git a/examples/fusion_custom_layer.hpp b/examples/fusion_custom_layer.hpp index 7a274c64..3582dabe 100644 --- a/examples/fusion_custom_layer.hpp +++ b/examples/fusion_custom_layer.hpp @@ -32,4 +32,20 @@ namespace nn_example x.rows(), x.cols(), P); } +// ── (b) 归一化形态:列归约 + 广播 + rsqrt ────────────────────────────── +// 与库内 LayerNorm / RMSNorm 的 std_inv 表达式**同形**(raxis=1、 +// viewkinds=[0,8])。库里这一类属于实测"只有执行 Layer 才拿得到"的 +// 25 条之一——即**锚点的符号实例与运行期实例折叠出不同结构**。 +// 库外自定义层没有 dry-run/模型 pass,因此本函数是对 `nn_enable_gpu_fusion` +// **可靠性**的直接检验:注册表若漏了运行期那一条,GPU 会闭合世界硬报错。 +[[nodiscard]] inline nn::Result fused_custom_norm( + nn::ComputeEngine& engine, nn::Tensor& x, float inv_cols, float eps, + nn::Precision P = nn::Precision::F32) +{ + return nn::dsl::compute(engine, + nn::dsl::rsqrt(nn::dsl::col_reduce_sum(nn::dsl::leaf(x)) + * nn::dsl::rparam(inv_cols) + nn::dsl::rparam(eps)), + x.rows(), x.cols(), P); +} + } // namespace nn_example diff --git a/examples/fusion_custom_layer_main.cpp b/examples/fusion_custom_layer_main.cpp index aa50b53f..c01ca59e 100644 --- a/examples/fusion_custom_layer_main.cpp +++ b/examples/fusion_custom_layer_main.cpp @@ -132,6 +132,31 @@ int main() return 1; } } + + // ②′ 归一化形态(列归约 + 广播):库内属"只有执行 Layer 才拿得到"的一类, + // 本层没有 dry-run —— 注册表若只靠锚点而符号实例与运行期不一致, + // 这里会闭合世界硬报错。 + { + nn::Tensor gx = gpu.create_tensor(4, 3, nn::Precision::F16, + nn::InitSpec::constant(0.5f)); + auto r = nn_example::fused_custom_norm(gpu, gx, 1.0f / 3.0f, 1e-5f, + nn::Precision::F16); + if (!r) + { + std::fprintf(stderr, + "[example] 归一化形态 GPU 未命中(闭合世界):%s\n", + r.error().message.c_str()); + return 1; + } + std::vector hb(12, nn::f16(0.0f)); + if (auto rr = gpu.read(*r, std::span(hb)); !rr) + { + std::fprintf(stderr, "[example] 归一化形态 read 失败\n"); + return 1; + } + std::printf("[example] GPU f16 归一化形态 = %.7f\n", + static_cast(static_cast(hb[0]))); + } std::puts("[example] GPU OK"); #endif diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index a0b964b6..3a666dc4 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -625,13 +625,27 @@ int main(int argc, char* argv[]) if (!v.empty()) v += ","; v += std::to_string(static_cast(x.kind)); } + std::string ins; + for (const auto& i : s.instrs) + { + if (!ins.empty()) ins += ";"; + ins += std::to_string(static_cast(i.op)) + "(" + + std::to_string(static_cast(i.a.kind)) + "/" + + std::to_string(static_cast(i.a.idx)) + "," + + std::to_string(static_cast(i.b.kind)) + "/" + + std::to_string(static_cast(i.b.idx)) + "," + + std::to_string(static_cast(i.c.kind)) + "/" + + std::to_string(static_cast(i.c.idx)) + ")"; + } return "raxis=" + std::to_string(raxis) + " mm=" + (s.matmul ? "1" : "0") + " fold=" + (s.fold ? "1" : "0") + " nreg=" + std::to_string(s.num_regs) - + " ninstr=" + std::to_string(s.instrs.size()) + + " nconst=" + std::to_string(s.consts.size()) + + " nrparam=" + std::to_string(s.rparams.size()) + " nview=" + std::to_string(s.views.size()) - + " viewkinds=[" + v + "]"; + + " viewkinds=[" + v + "]" + + " instrs=[" + ins + "]"; }; std::size_t n_dry_only = 0; for (const auto& s : reg.specs) From 371ffb918738e88d1ee61dfa61c9c36b7dc8429e Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 19:24:34 +0800 Subject: [PATCH 27/30] =?UTF-8?q?A4:=20=E6=8A=8A=20A2=20=E7=AB=AF=E5=88=B0?= =?UTF-8?q?=E7=AB=AF=E9=AA=8C=E6=94=B6=E6=B3=A8=E5=86=8C=E4=B8=BA=E5=B8=B8?= =?UTF-8?q?=E9=A9=BB=20ctest=EF=BC=88ctest=2019=20=E2=86=92=2020=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:`examples/fusion_custom_layer*` 此前只由默认构建覆盖编译——**运行时断言 断言需手工跑**(返回码/数值对拍),会静默腐化。而它正是"库外使用者写 dsl::compute + 一行 CMake → GPU 能跑"的唯一自动门禁。 改动: · CMakeLists:`nn_enable_gpu_fusion` 调用之后 `add_test(NAME fusion_custom_layer_example ...)` + `SKIP_RETURN_CODE 77`(无 Vulkan 设备 时跳过,与仓库既有约定一致); · 文档当前数字同步:AGENTS.md §12 测试条与 §11 索引行、docs/development/12 头部「当前数字」——19 → **20**(18 个测试目标 + `cnn_test_gpu` + `fusion_custom_layer_example`)。 验收:build 零告警;**ctest 20/20**(含新门禁,2.0s); 库内注册表 SHA256 仍 3D47775F…84D(逐字节不变); CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变; doc_align [A]/[E]/[F] 可行动 0;doc_inventory virtual 49、L2-VIOLATIONS 0。 --- AGENTS.md | 6 +++--- CMakeLists.txt | 10 ++++++++++ docs/development/12-compute-engine-inventory.md | 5 +++-- docs/history.md | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1f4a466a..46b6e9ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -264,7 +264,7 @@ optimizer.step(); | `development/06-rapt-algorithm.md` | **线性注意力(RLA → RAPT → RLA-2):直观理解 + 数学定义 + 扫描原语工程落地 + 训练显存开关** | | `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/12-compute-engine-inventory.md` | **引擎接口盘点(复现:`bench/doc_inventory.ps1`):49 个 virtual 方法、Layer 直调 21 个(基础设施/数据搬运/状态扫描/fold 登记)、两套 CPU 求值机制(DSL 模板 + IR 解释器)、ctest 20 个测试;演进记录见 `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 项** | @@ -287,7 +287,7 @@ optimizer.step(); ### 已知问题 -- **GPU `stable=f16` 训练 loss 打印冻结(未修)**:`text_train --precision-stable f16` 时 step 恒 4.0137、跨 epoch/跨进程逐位相同;**权重照常更新**(同进程跨 epoch 模型快照 63% 参数字节不同,冻结仅在 loss 回读链);CPU 同配置健康;ctest 全绿(19/19)但**无 GPU+stable=f16 用例**。触发矩阵、覆盖缺口与证据见 `docs/development/14-f16-stable-gpu-loss-frozen.md`。推荐路径 `--f16`(stable/optimizer=f32)实测健康。 +- **GPU `stable=f16` 训练 loss 打印冻结(未修)**:`text_train --precision-stable f16` 时 step 恒 4.0137、跨 epoch/跨进程逐位相同;**权重照常更新**(同进程跨 epoch 模型快照 63% 参数字节不同,冻结仅在 loss 回读链);CPU 同配置健康;ctest 全绿(20/20)但**无 GPU+stable=f16 用例**。触发矩阵、覆盖缺口与证据见 `docs/development/14-f16-stable-gpu-loss-frozen.md`。推荐路径 `--f16`(stable/optimizer=f32)实测健康。 - **数值性限制**:四字段全 f16 不可训练——`optimizer=f16` 单独即令 Adam 更新爆炸、`stable=f16` 链约 200 步 NaN,见 `docs/development/05-mixed-precision.md` §12.5;常规 f32 训练健康收敛。 - **不存在的 CLI 参数**:`--tdr-retry`/`--max-tdr-retries` 与 `mnist_train --osc-guard`/`--osc-window`/`--osc-threshold`(**均已移除**:前两者从未实现,后三者属"帮助声明了、解析分支不存在"的幽灵选项,2026-10-01 已从帮助/控制器/GUI 三层清除,详见 `docs/history.md`「GUI / CLI 参数一致性清理」)。 - **`text_train --model zipt` 启动即中止(未修)** → **已随 ZiPT 整体移除而消失(2026-10-01)**:AttnZip/ZiPT 已从主线移除(`--model zipt` 现在直接报"未知模型架构"并给出迁移提示),代码保留在 **`legacy/zipt` 分支**。**恢复前提(两条同时满足)**:① **算法层**——压缩向量必须因果(压缩器只能看当前位置之前的内容);② **代码层**——实现质量对齐 RAPT/GPT(支持 `forward_recompute` 梯度检查点与 activation offload、CLI/序列化/测试齐备)。裁决依据见 `docs/history.md`「ZiPT 移除」条。已移除的 API:`ZiPTModel`/`ZiPTBlock`/`CrossAttention`、`build_zipt_model*`/`make_zipt_spec`/`ZiPTConfig`/`ZIPT_MEMORY_TOKENS`、`ModelSpec::is_zipt()` 与 `memory_tokens`/`window` 字段、CLI `--model zipt`/`--window`/`--memory-tokens`、`zipt_test`;`ModelType` 的 6 号枚举保留为 `Reserved_ZiPT` 占位,用于对旧 `.bin`(type=6)给出明确错误。 @@ -314,7 +314,7 @@ optimizer.step(); - **评估分块**:`evaluate_mnist` 的 `eval_batch`(默认 1000)分块前向 + 每块 `release_idle_pool_blocks()`,防大 batch 评估 OOM。 - **BPE 保序并行 encode**:`Tokenizer::set_encode_threads`(0=自动/1=顺序/>1=指定)+ `encode_segments_`——按空白安全切分点分段、段内经全局线程池并发编码、**按段下标升序拼接**,任意并行度与顺序执行**逐字节一致**(铁律 #8;切分点必为 chunk 边界、标记不含空白不会被切断);文本 < 256 KiB 或找不到切分点回退顺序路径。`bpe_merge_impl_` 改 thread_local `BpeMergeScratch` + 手写堆,每 chunk 合并**零堆分配**。`text_train::parallel_tokenize` 收编到 `nn::parallel_for_samples`,`tokenizer_infer` 新增 `--threads`。实测 8 MiB 多样文本 `encode` 0.93s→0.134s(32 线程,7.0x);`.tokcache` 重新生成 SHA256 与旧实现一致。**训练期合并循环仍不可并行**(链式依赖,见 `docs/development/08` §4.2)。 - **CPU 性能**:DSL 模板路径向量化/并行、`dsl::compute_into` 零分配原地更新、`Tensor::cpu_get_ptr`、分块 GEMM 内核(BLOCK_SIZE=64)。 -- **测试**:ctest 注册 19 个测试(18 个测试目标 + `cnn_test_gpu` = `cnn_test --gpu`;`-DNN_ENABLE_TESTS=ON`;需 Vulkan 的用例退出码 77 = skip)。 +- **测试**:ctest 注册 **20** 个测试(**18** 个测试目标 + `cnn_test_gpu` = `cnn_test --gpu` + `fusion_custom_layer_example` = 库外使用者形态的 AOT 融合端到端门禁;`-DNN_ENABLE_TESTS=ON`;需 Vulkan 的用例退出码 77 = skip)。 ### 融合二期状态与 IR 编码约束(改融合/IR 代码前必读) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a78a86e..411e6524 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -583,6 +583,16 @@ if(Vulkan_FOUND) nn_enable_gpu_fusion(fusion_custom_layer_example MAIN ${CMAKE_SOURCE_DIR}/examples/fusion_custom_layer_main.cpp) + # A2 验收注册为**常驻 ctest**:这是"库外使用者写 dsl::compute + 一行 + # CMake → GPU 能跑"的唯一自动门禁(含 f16 运行期精度分派 `key#x` 路径 + # 与归一化形态)。无 Vulkan 设备时退出码 77 = skip。 + if(NN_ENABLE_TESTS) + add_test(NAME fusion_custom_layer_example + COMMAND fusion_custom_layer_example) + set_tests_properties(fusion_custom_layer_example + PROPERTIES SKIP_RETURN_CODE 77) + endif() + message(STATUS "GPU acceleration enabled (Vulkan compute)") endif() else() diff --git a/docs/development/12-compute-engine-inventory.md b/docs/development/12-compute-engine-inventory.md index 1934dcbb..81c0f2f2 100644 --- a/docs/development/12-compute-engine-inventory.md +++ b/docs/development/12-compute-engine-inventory.md @@ -6,8 +6,9 @@ > > **当前数字**:引擎 virtual **49 个方法**(`bench/doc_inventory.ps1` 口径;M6 新增 `import_impl` 使其由 48 → 49。注意 `grep -c "\bvirtual\b"` 原始计 **51** 处 = 49 方法 + 析构 1 + 注释里的 "Non-Virtual" 1,勿混用口径);Layer/Loss/Optimizer 直调 **21 个**(其余 28 个 > 只服务 DSL lowering / 序列化 / CLI / 适配层 / 测试,见 §2);CPU 求值机制 **2 套** -> (DSL 模板路径 + IR 解释器,见 §3);ctest **19** 个测试(18 个测试目标 + `cnn_test_gpu`; -> `zipt_test` 随 ZiPT 于 2026-10-01 移除,见 `docs/history.md`)。 +> (DSL 模板路径 + IR 解释器,见 §3);ctest **20** 个测试(**18** 个测试目标 + `cnn_test_gpu` +> = `cnn_test --gpu` + `fusion_custom_layer_example` = 库外使用者形态的 AOT 融合端到端门禁, +> 见 `AGENTS.md` §7;`zipt_test` 随 ZiPT 于 2026-10-01 移除,见 `docs/history.md`)。 > > 历史演进与收敛记录(2026-09-27 计算类原语全量迁 DSL、2026-09-26 算子收敛与遗留物 > 清理、2026-09-19 IR-C 删除、接口数字 58→49 / 直调 35→23 的过程)已移入 diff --git a/docs/history.md b/docs/history.md index 6377cd79..f09d322b 100644 --- a/docs/history.md +++ b/docs/history.md @@ -59,7 +59,7 @@ expr_opt / expr_registry / expr_spec)「历史状态类注释」摘录。整 - **顺带修的 header-only 缺陷**:`backend/compute_vk_backend.hpp` 的 `GpuBuffer::~GpuBuffer()` 类外定义**缺 `inline`** → 任何 ≥2 个 TU 的程序链接期 `duplicate symbol`(样例两 TU 复现)。已加 `inline`。这条此前从未暴露,因为库自身的可执行目标都是单 TU。 - **GPU 端到端验收(本轮补齐)**:样例原先只验 CPU——而 CPU 走模板求值、与注册表无关,等于**没验到本 PR 的核心机制**。补上 GPU 后实测:CPU f32 `0.6712035`(差 0);GPU f32(V0)`0.6712036`(差 5.96e-08);**GPU f16(V1 运行期精度分派)`0.6708984`(差 3.05e-04,f16 舍入内)**。该表达式含 rparams ⇒ `expr_prec_sig_native16` 谓词不通过 ⇒ f16 那一跑命中的必然是 `key#x` 分派 shader;注册表若漏它就是闭合世界硬报错,"跑通"本身即断言。 - **CMake include 顺序有语义(踩坑)**:`nn_enable_gpu_fusion` 起初只把 `${NNF_OUT_DIR}` 加进目标,样例 GPU 初始化报 `matmul SPIR-V bytecode not embedded`——手写原语 shader 的嵌入头在 `${CMAKE_BINARY_DIR}/generated`;但该目录**同时含库自身的 `fused_registry.hpp`**。最终把两者都加、且 `${NNF_OUT_DIR}` **排在前面**(`__has_include` 取第一个匹配),否则使用者的自定义结构全部闭合世界报错。已写进 AGENTS §7。 -- **未做**:把样例注册进 ctest(本次保持 ctest 19/19 口径不变;样例由默认构建覆盖编译,运行时断言需手工跑 `build/fusion_custom_layer_example.exe`,无 Vulkan 时退出码 77 = skip)。 +- **已注册为常驻 ctest(round 17)**:样例的目标原先只由默认构建覆盖编译(运行时断言需手工跑),会静默腐化。现已 `add_test(fusion_custom_layer_example)` + `SKIP_RETURN_CODE 77`(无 Vulkan 设备时跳过)——它是**库外使用者形态的 AOT 融合端到端门禁**。ctest 总数 19 → **20**(18 个测试目标 + `cnn_test_gpu` + 本样例;`AGENTS.md` §12 与 `docs/development/12` 的当前数字已同步)。 ## 「锚点 vs dry-run」边界实测刻画(2026-10-01,round 15) From d0c960906aaae9a6288b7f9cfa2cd7ff94c0b1e3 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 19:31:49 +0800 Subject: [PATCH 28/30] =?UTF-8?q?A5:=20=E6=8E=A5=E5=85=A5=E5=AD=A4?= =?UTF-8?q?=E5=84=BF=E6=B5=8B=E8=AF=95=20`fused=5Fgpu=5Ftest`=EF=BC=88ctes?= =?UTF-8?q?t=2020=20=E2=86=92=2021=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 发现:`src/fused_gpu_test.cpp`(44 KB,融合 shader **逐形态** GPU 对拍) **未被任何 CMake 目标引用**——既不编译也不注册。git log -S 显示最后一次改动 是 516d781(Release v1.2.0),此后再无引用。 它覆盖的正是 AOT 融合最核心的回归面:rope / swiglu / gelu / softmax / matmul / matmul+列归约 / norm / reduce_consts / fold v1(rowmax/rowsum/ softmax_denom)/ fold attn(plain/causal/alibi/doc/alibidoc × 5 形状)/ 未扫描表达式硬报错。 改动:CMakeLists 紧邻 expr_gpu_test 处 `nn_add_executable(fused_gpu_test ...)` + `list(APPEND NN_TEST_TARGETS fused_gpu_test)`;文档当前数字 20 → 21。 实测:编译零告警、`ALL PASS`、退出码 0;ctest 21/21 全绿。 仍存在的缺口(如实记录,本轮不修):该文件 0 处 f16 引用 ⇒ 覆盖 V0(全 f32) 逐形态路径,**不覆盖 V1 运行期精度分派**。V1 目前只有 7 个真实 f16 负载与样例 第二形态在压测,无逐形态数值断言。 验收:build 零告警;ctest 21/21;注册表 SHA256 仍 3D47775F…84D(逐字节不变); CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变。 --- AGENTS.md | 6 +++--- CMakeLists.txt | 8 ++++++++ docs/development/12-compute-engine-inventory.md | 5 +++-- docs/history.md | 9 +++++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 46b6e9ef..ebdda183 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -264,7 +264,7 @@ optimizer.step(); | `development/06-rapt-algorithm.md` | **线性注意力(RLA → RAPT → RLA-2):直观理解 + 数学定义 + 扫描原语工程落地 + 训练显存开关** | | `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 20 个测试;演进记录见 `docs/history.md`** | +| `development/12-compute-engine-inventory.md` | **引擎接口盘点(复现:`bench/doc_inventory.ps1`):49 个 virtual 方法、Layer 直调 21 个(基础设施/数据搬运/状态扫描/fold 登记)、两套 CPU 求值机制(DSL 模板 + IR 解释器)、ctest 21 个测试;演进记录见 `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 项** | @@ -287,7 +287,7 @@ optimizer.step(); ### 已知问题 -- **GPU `stable=f16` 训练 loss 打印冻结(未修)**:`text_train --precision-stable f16` 时 step 恒 4.0137、跨 epoch/跨进程逐位相同;**权重照常更新**(同进程跨 epoch 模型快照 63% 参数字节不同,冻结仅在 loss 回读链);CPU 同配置健康;ctest 全绿(20/20)但**无 GPU+stable=f16 用例**。触发矩阵、覆盖缺口与证据见 `docs/development/14-f16-stable-gpu-loss-frozen.md`。推荐路径 `--f16`(stable/optimizer=f32)实测健康。 +- **GPU `stable=f16` 训练 loss 打印冻结(未修)**:`text_train --precision-stable f16` 时 step 恒 4.0137、跨 epoch/跨进程逐位相同;**权重照常更新**(同进程跨 epoch 模型快照 63% 参数字节不同,冻结仅在 loss 回读链);CPU 同配置健康;ctest 全绿(21/21)但**无 GPU+stable=f16 用例**。触发矩阵、覆盖缺口与证据见 `docs/development/14-f16-stable-gpu-loss-frozen.md`。推荐路径 `--f16`(stable/optimizer=f32)实测健康。 - **数值性限制**:四字段全 f16 不可训练——`optimizer=f16` 单独即令 Adam 更新爆炸、`stable=f16` 链约 200 步 NaN,见 `docs/development/05-mixed-precision.md` §12.5;常规 f32 训练健康收敛。 - **不存在的 CLI 参数**:`--tdr-retry`/`--max-tdr-retries` 与 `mnist_train --osc-guard`/`--osc-window`/`--osc-threshold`(**均已移除**:前两者从未实现,后三者属"帮助声明了、解析分支不存在"的幽灵选项,2026-10-01 已从帮助/控制器/GUI 三层清除,详见 `docs/history.md`「GUI / CLI 参数一致性清理」)。 - **`text_train --model zipt` 启动即中止(未修)** → **已随 ZiPT 整体移除而消失(2026-10-01)**:AttnZip/ZiPT 已从主线移除(`--model zipt` 现在直接报"未知模型架构"并给出迁移提示),代码保留在 **`legacy/zipt` 分支**。**恢复前提(两条同时满足)**:① **算法层**——压缩向量必须因果(压缩器只能看当前位置之前的内容);② **代码层**——实现质量对齐 RAPT/GPT(支持 `forward_recompute` 梯度检查点与 activation offload、CLI/序列化/测试齐备)。裁决依据见 `docs/history.md`「ZiPT 移除」条。已移除的 API:`ZiPTModel`/`ZiPTBlock`/`CrossAttention`、`build_zipt_model*`/`make_zipt_spec`/`ZiPTConfig`/`ZIPT_MEMORY_TOKENS`、`ModelSpec::is_zipt()` 与 `memory_tokens`/`window` 字段、CLI `--model zipt`/`--window`/`--memory-tokens`、`zipt_test`;`ModelType` 的 6 号枚举保留为 `Reserved_ZiPT` 占位,用于对旧 `.bin`(type=6)给出明确错误。 @@ -314,7 +314,7 @@ optimizer.step(); - **评估分块**:`evaluate_mnist` 的 `eval_batch`(默认 1000)分块前向 + 每块 `release_idle_pool_blocks()`,防大 batch 评估 OOM。 - **BPE 保序并行 encode**:`Tokenizer::set_encode_threads`(0=自动/1=顺序/>1=指定)+ `encode_segments_`——按空白安全切分点分段、段内经全局线程池并发编码、**按段下标升序拼接**,任意并行度与顺序执行**逐字节一致**(铁律 #8;切分点必为 chunk 边界、标记不含空白不会被切断);文本 < 256 KiB 或找不到切分点回退顺序路径。`bpe_merge_impl_` 改 thread_local `BpeMergeScratch` + 手写堆,每 chunk 合并**零堆分配**。`text_train::parallel_tokenize` 收编到 `nn::parallel_for_samples`,`tokenizer_infer` 新增 `--threads`。实测 8 MiB 多样文本 `encode` 0.93s→0.134s(32 线程,7.0x);`.tokcache` 重新生成 SHA256 与旧实现一致。**训练期合并循环仍不可并行**(链式依赖,见 `docs/development/08` §4.2)。 - **CPU 性能**:DSL 模板路径向量化/并行、`dsl::compute_into` 零分配原地更新、`Tensor::cpu_get_ptr`、分块 GEMM 内核(BLOCK_SIZE=64)。 -- **测试**:ctest 注册 **20** 个测试(**18** 个测试目标 + `cnn_test_gpu` = `cnn_test --gpu` + `fusion_custom_layer_example` = 库外使用者形态的 AOT 融合端到端门禁;`-DNN_ENABLE_TESTS=ON`;需 Vulkan 的用例退出码 77 = skip)。 +- **测试**:ctest 注册 **21** 个测试(**19** 个测试目标 + `cnn_test_gpu` = `cnn_test --gpu` + `fusion_custom_layer_example` = 库外使用者形态的 AOT 融合端到端门禁;`-DNN_ENABLE_TESTS=ON`;需 Vulkan 的用例退出码 77 = skip)。其中 `fused_gpu_test` = **融合 shader 逐形态** GPU 对拍(rope/swiglu/gelu/softmax/matmul/matmul+reduce/norm/reduce_consts/fold v1/fold attn/回退硬报错)。 ### 融合二期状态与 IR 编码约束(改融合/IR 代码前必读) diff --git a/CMakeLists.txt b/CMakeLists.txt index 411e6524..06a40da9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -294,6 +294,14 @@ if(NN_ENABLE_TESTS) nn_add_executable(expr_gpu_test src/expr_gpu_test.cpp) list(APPEND NN_TEST_TARGETS expr_gpu_test) + # ── 融合 shader **逐形态** GPU 对拍(rope/swiglu/gelu/softmax/matmul/ + # matmul+reduce/norm/reduce_consts/fold v1/fold attn/回退)────────── + # ⚠ 此前该文件**未被任何 CMake 目标引用**(未编译、未注册)——它恰好是 + # 覆盖"每个生成器各自的 V1 运行期精度分派形态"的测试,属于 AOT 融合的 + # 核心回归面。重新接入。 + nn_add_executable(fused_gpu_test src/fused_gpu_test.cpp) + list(APPEND NN_TEST_TARGETS fused_gpu_test) + # ── ModelSpec 架构校验(load_model 文件头 vs 模型规格比对)────────── nn_add_executable(model_spec_validation_test src/model_spec_validation_test.cpp) list(APPEND NN_TEST_TARGETS model_spec_validation_test) diff --git a/docs/development/12-compute-engine-inventory.md b/docs/development/12-compute-engine-inventory.md index 81c0f2f2..73b44e06 100644 --- a/docs/development/12-compute-engine-inventory.md +++ b/docs/development/12-compute-engine-inventory.md @@ -6,9 +6,10 @@ > > **当前数字**:引擎 virtual **49 个方法**(`bench/doc_inventory.ps1` 口径;M6 新增 `import_impl` 使其由 48 → 49。注意 `grep -c "\bvirtual\b"` 原始计 **51** 处 = 49 方法 + 析构 1 + 注释里的 "Non-Virtual" 1,勿混用口径);Layer/Loss/Optimizer 直调 **21 个**(其余 28 个 > 只服务 DSL lowering / 序列化 / CLI / 适配层 / 测试,见 §2);CPU 求值机制 **2 套** -> (DSL 模板路径 + IR 解释器,见 §3);ctest **20** 个测试(**18** 个测试目标 + `cnn_test_gpu` +> (DSL 模板路径 + IR 解释器,见 §3);ctest **21** 个测试(**19** 个测试目标 + `cnn_test_gpu` > = `cnn_test --gpu` + `fusion_custom_layer_example` = 库外使用者形态的 AOT 融合端到端门禁, -> 见 `AGENTS.md` §7;`zipt_test` 随 ZiPT 于 2026-10-01 移除,见 `docs/history.md`)。 +> 见 `AGENTS.md` §7。其中 `fused_gpu_test` = 融合 shader **逐形态** GPU 对拍;`zipt_test` 随 +> ZiPT 于 2026-10-01 移除,见 `docs/history.md`)。 > > 历史演进与收敛记录(2026-09-27 计算类原语全量迁 DSL、2026-09-26 算子收敛与遗留物 > 清理、2026-09-19 IR-C 删除、接口数字 58→49 / 直调 35→23 的过程)已移入 diff --git a/docs/history.md b/docs/history.md index f09d322b..933a342c 100644 --- a/docs/history.md +++ b/docs/history.md @@ -61,6 +61,15 @@ expr_opt / expr_registry / expr_spec)「历史状态类注释」摘录。整 - **CMake include 顺序有语义(踩坑)**:`nn_enable_gpu_fusion` 起初只把 `${NNF_OUT_DIR}` 加进目标,样例 GPU 初始化报 `matmul SPIR-V bytecode not embedded`——手写原语 shader 的嵌入头在 `${CMAKE_BINARY_DIR}/generated`;但该目录**同时含库自身的 `fused_registry.hpp`**。最终把两者都加、且 `${NNF_OUT_DIR}` **排在前面**(`__has_include` 取第一个匹配),否则使用者的自定义结构全部闭合世界报错。已写进 AGENTS §7。 - **已注册为常驻 ctest(round 17)**:样例的目标原先只由默认构建覆盖编译(运行时断言需手工跑),会静默腐化。现已 `add_test(fusion_custom_layer_example)` + `SKIP_RETURN_CODE 77`(无 Vulkan 设备时跳过)——它是**库外使用者形态的 AOT 融合端到端门禁**。ctest 总数 19 → **20**(18 个测试目标 + `cnn_test_gpu` + 本样例;`AGENTS.md` §12 与 `docs/development/12` 的当前数字已同步)。 +## 融合回归测试 `fused_gpu_test` 曾是孤儿(2026-10-01,round 18) + +- 类型:已修复缺陷(测试覆盖缺口 / 构建配置遗漏) +- **发现**:`src/fused_gpu_test.cpp`(44 KB)**未被任何 CMake 目标引用**——既不编译也不注册 ctest。`git log -S'fused_gpu_test' -- CMakeLists.txt` 最后一次改动是 `516d781`(Release v1.2.0),此后再无引用。 +- **它是什么**:融合 shader **逐形态** GPU 对拍——rope / swiglu / gelu / softmax / matmul / matmul+列归约 / norm / reduce_consts / **fold v1**(rowmax/rowsum/softmax_denom)/ **fold attn**(plain/causal/alibi/doc/alibidoc × 5 种形状)/ **未扫描表达式硬报错**。正是 AOT 融合最核心的回归面。 +- **处置**:接入构建(`nn_add_executable` + `list(APPEND NN_TEST_TARGETS ...)`,CMakeLists 紧邻 `expr_gpu_test`)。 +- **实测**:编译零告警、输出 `ALL PASS`、退出码 0;接入后 **ctest 20 → 21**(19 个测试目标 + `cnn_test_gpu` + `fusion_custom_layer_example`),21/21 全绿。 +- **仍存在的缺口(未修,记录在案)**:该文件 **0 处 f16 引用** ⇒ 它覆盖的是 **V0(全 f32)** 逐形态路径,**不覆盖 V1 运行期精度分派(`key#x`)**。V1 目前只有 7 个真实 f16 负载(`NN_PREC_TRACE=1` 实测 `[prec][miss]=0`)与样例的第二形态在压测它,**没有逐形态的数值断言**。若将来要补,做法是给该文件的每个 form 加一个 f16 变体(CPU 参考 + GPU f16,容差按 f16)。 + ## 「锚点 vs dry-run」边界实测刻画(2026-10-01,round 15) - 类型:覆盖缺口 / 否决方案("删掉 dry-run") From 17381ab132e4c76b433f9814ec7620286e29e69a Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 19:39:47 +0800 Subject: [PATCH 29/30] =?UTF-8?q?A6:=20=E4=BF=AE=20`NN=5FPREC=5FTRACE`=20?= =?UTF-8?q?=E5=AF=B9=20V1=20=E7=9A=84=E7=9B=B2=E5=8C=BA=20+=20=E5=AE=9E?= =?UTF-8?q?=E6=B5=8B=20V1=20=E8=A6=86=E7=9B=96=EF=BC=88=E5=B9=B6=E6=9B=B4?= =?UTF-8?q?=E6=AD=A3=E4=B8=8A=E4=B8=80=E8=BD=AE=E7=9A=84=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=BB=93=E8=AE=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 起因:想量"V1 运行期精度分派的逐形态覆盖",用 NN_PREC_TRACE=1 跑 6 个 GPU 测试 → 得到 0 条 `#x`、只有 4 条 `#a`。 **对照实验救了这一轮**:跑已知必然走 V1 的库外样例(它含 rparam ⇒ native16 结构谓词不通过)——**样例也一行都不打** ⇒ 是测量工具瞎,不是"V1 没被用"。 根因:`compute_gpu_engine.hpp::find_prec_variant_` 的 `[prec][alu-hit]` 打印**只写在 V2(`#a`)分支**,V1(`#x`)与 V0 分支都静默返回。 改动: · V1 分支补同款日志(V0 默认路径仍不记——f32 热路径逐调用打日志会淹没 输出)。现规则:**变体选择事件(V1/V2)一律打日志**; · `scan_exprs` 的来源 dump 补 `[scan][both]`(锚点∩dry-run 交集,84 条里 38 条在此)——否则"某结构由哪个生成器负责、运行时是否按 V1 分派"无法从 dump 回答(V1 覆盖的 20 条里 18 条在交集中)。 修正后的实测(单跑 `f16_precision_test` 即够):12040 次分派、24 条 distinct = **20 条 V1(`#x`)+ 4 条 V2(`#a`)**;V1 的 20 条**覆盖全部四个生成器族** (元素 9 / 归约 6 / matmul 4 / fold 1),且该测试用 CHECK_NEAR 容差断言。 ⇒ **更正上一轮(A5)的错误结论**:"V1 无逐形态数值断言"**不成立**。`fused_gpu_test` 缺的是"**本套件的** rope/swiglu/gelu/softmax/fold-attn 形状的 f16 变体",不是 "V1 无覆盖"。history 与 AGENTS.md §7 均已更正。 验收:build 零告警;ctest 21/21;注册表 SHA256 仍 3D47775F…84D(逐字节不变); CPU 锚 6f8849f14da23110 / GPU dev2 锚 8ef51b2927253c50 逐位不变。 --- AGENTS.md | 2 +- docs/history.md | 13 ++++++++++++- include/neuralnet.cpp/compute_gpu_engine.hpp | 13 ++++++++++++- tools/scan_exprs.cpp | 8 ++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ebdda183..301f14df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -180,7 +180,7 @@ GPT 序列展平: 列序 i = b*seq + t(batch-major,全局唯一约定) - **构建期单步**(CMake 自动编排,改 Layer 内联表达式后重跑构建即可)——`scan_exprs` 同进程**收集 + 生成**: 1. **收集**:只收集**结构**(= 唯一需要构建期枚举的东西)。两个来源**互补、不冗余**(`NN_SCAN_DUMP_SOURCES=1` 可逐条打印来源):`FusedAnchor` 在静态初始化期按表达式**类型**自登记(编译期可达,dry-run 跑不到的分支也覆盖);per-layer dry-run + 模型 pass 执行补齐**运行期配置决定**的变体(视图种类/项数在类型层面推不出,符号实例 `Expr{}` 与真实实例会折叠出不同 key);fold spec(3 掩码 × 2 偏置的 5 个组合等 8 条)与 matmul trans 族由 `scan_exprs` **显式登记**。**只跑 f32 一遍**——实测 f16 遍对结构贡献为 0(跳过它签名 66 → 0、结构恒 84),该两遍已删除。**实测分工(84 条)**:per-layer dry-run 38(含模型 pass +25、显式登记 +8)、锚点 59,重叠 46 → **锚点独有 13、dry-run 独有 25**。所以**任何一方单独删掉都会丢结构**——dry-run/model pass 是结构的主要来源,删不掉。 2. **生成**:`tools/fused_generate.hpp`(原独立工具 `gen_fused`)对**每个结构发三份**:V0 全 f32(键 = `key`)/ **V1 运行期精度分派**(键 = `key#x`)/ V2 native16(键 = `key#a`,按 `expr_prec_sig_native16` 的**结构谓词**判定)→ 经 `emitter_registry` 选后端(默认 `"glsl"` = `GlslEmitter`)生成 GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。**不经 `.bin` 中间序列化**(`write_registry/read_registry` 已删除)。 -- **精度签名不再是构建期集合**("签名从身份降级为参数"):V1 的 GLSL 对每个输入/输出各声明 f32 + `float16_t` 双视图,加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支,**一份 shader 覆盖任意签名**。因此运行期**不存在 miss、不存在边界 cast 回退、不存在回填清单**(`tools/prec_backfill.txt` 已删除,`NN_PREC_TRACE` 的"补变体"流程随之失效)。V1 对每个结构无条件生成 ⇒ 任何签名都命中。 +- **精度签名不再是构建期集合**("签名从身份降级为参数"):V1 的 GLSL 对每个输入/输出各声明 f32 + `float16_t` 双视图,加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支,**一份 shader 覆盖任意签名**。因此运行期**不存在 miss、不存在边界 cast 回退、不存在回填清单**(`tools/prec_backfill.txt` 已删除,`NN_PREC_TRACE` 的"补变体"流程随之失效)。V1 对每个结构无条件生成 ⇒ 任何签名都命中。**变体选择事件(V1/V2)一律打 `NN_PREC_TRACE` 日志**(V0 默认路径不打——f32 热路径会淹没输出;V1 分支曾漏打,导致"V1 从未被使用"的错误判断)。**实测 V1 覆盖**(`NN_PREC_TRACE=1 f16_precision_test`):12040 次分派 / 24 条 distinct 结构 = **20 条 V1 + 4 条 V2**,V1 覆盖**全部四个生成器族**(元素 9 / 归约 6 / matmul 4 / fold 1),且该测试用 `CHECK_NEAR` 容差断言。 - **新增调用点无需改动其他文件**(实测):在 Layer 头里新写一个 `dsl::compute`(其结构不在任何 dry-run / 模型 pass 覆盖内)→ 锚点独有 13 → 14、结构 84 → 85、注册表新增 `key` / `key#x` / `key#a` 三条,**其他文件零改动**。 - **库外使用者:`nn_enable_gpu_fusion( MAIN <含 main 的源>)`**(`CMakeLists.txt`):使用者在自己的层头里写 `dsl::compute` + 在 CMake 加这一行 → 自动生成**该目标专属**的 `fused_registry.hpp` 并加进它的 include path。机制 = 收集器由**库内收集逻辑**(`tools/scan_exprs.cpp`:锚点 + dry-run + 模型 pass + 显式登记)**加上本目标的全部 TU**(`-DNN_EXPR_SCAN` 编译,让使用者的调用点自登记)组成。**⚠ 必须保留库内收集逻辑**:84 个结构里只有 13 个来自编译期锚点、71 个来自 dry-run/模型 pass,"纯锚点收集器"会漏库自身的结构。**⚠ include 顺序有语义**:`${NNF_OUT_DIR}` 必须排在 `${CMAKE_BINARY_DIR}/generated` 之前(后者含库自身注册表与手写原语 shader 嵌入头),否则 `__has_include("fused_registry.hpp")` 会解析到库的那一份、使用者的自定义结构全部闭合世界报错。样例见 `examples/fusion_custom_layer*`。 - **端到端验收(库外 + f16 分派)**:`build/fusion_custom_layer_example.exe` 实测两种**自研算子形态**:①逐元素 `exp(|x|·α)/(√|y|+α)`;②归一化形态 `rsqrt(col_reduce_sum(x)·inv + ε)`(= 库内 LayerNorm/RMSNorm 的 `std_inv` 同形,`raxis=1`、`viewkinds=[0,8]`,属库里"只有执行 Layer 才拿得到"的那一类)。两者都只用**锚点**(自定义层没有 dry-run)→ 都命中并跑通 ⇒ `nn_enable_gpu_fusion` 对常见自定义层可靠。实测值:CPU f32 `0.6712035`(差 0);**GPU f32(V0)`0.6712036`(差 5.96e-08)**;**GPU f16(V1 运行期精度分派)`0.6708984`(差 3.05e-04,f16 舍入内)**;归一化形态 GPU f16 `1.2246094`。含 rparams ⇒ native16 谓词不通过 ⇒ f16 命中必然是 `key#x`;注册表若漏了它就是闭合世界硬报错,"跑通"本身即断言。**残余不确定性**:库内确有 25 条结构只有执行 Layer/模型才拿得到(锚点的符号实例 `Expr{}` 与运行期实例折叠结果不同);实测的两种自定义形态不在其中,但无法先验排除。若使用者的自定义层遇到闭合世界报错,下一步是给 `nn_enable_gpu_fusion` 加 `COLLECT ` 钩子(由使用者提供"跑一遍自己的层"的收集函数,等价于库内的模型 pass)。 diff --git a/docs/history.md b/docs/history.md index 933a342c..33ab0ece 100644 --- a/docs/history.md +++ b/docs/history.md @@ -68,7 +68,18 @@ expr_opt / expr_registry / expr_spec)「历史状态类注释」摘录。整 - **它是什么**:融合 shader **逐形态** GPU 对拍——rope / swiglu / gelu / softmax / matmul / matmul+列归约 / norm / reduce_consts / **fold v1**(rowmax/rowsum/softmax_denom)/ **fold attn**(plain/causal/alibi/doc/alibidoc × 5 种形状)/ **未扫描表达式硬报错**。正是 AOT 融合最核心的回归面。 - **处置**:接入构建(`nn_add_executable` + `list(APPEND NN_TEST_TARGETS ...)`,CMakeLists 紧邻 `expr_gpu_test`)。 - **实测**:编译零告警、输出 `ALL PASS`、退出码 0;接入后 **ctest 20 → 21**(19 个测试目标 + `cnn_test_gpu` + `fusion_custom_layer_example`),21/21 全绿。 -- **仍存在的缺口(未修,记录在案)**:该文件 **0 处 f16 引用** ⇒ 它覆盖的是 **V0(全 f32)** 逐形态路径,**不覆盖 V1 运行期精度分派(`key#x`)**。V1 目前只有 7 个真实 f16 负载(`NN_PREC_TRACE=1` 实测 `[prec][miss]=0`)与样例的第二形态在压测它,**没有逐形态的数值断言**。若将来要补,做法是给该文件的每个 form 加一个 f16 变体(CPU 参考 + GPU f16,容差按 f16)。 +- **该文件的边界(round 19 实测更正)**:它 **0 处 f16 引用** ⇒ 覆盖的是 **V0(全 f32)** 逐形态路径。 + - ⚠ **曾据此误判**为"V1 无覆盖、无逐形态数值断言"——**该结论是错的**,见下条:V1 由 `f16_precision_test` 覆盖(20 条结构、四大生成器族、`CHECK_NEAR` 容差断言)。本文件缺的是"**本套件的** rope/swiglu/gelu/softmax/fold-attn 形状的 f16 变体",不是"V1 无覆盖"。 + +## `NN_PREC_TRACE` 对 V1 是盲的(2026-10-01,round 19) + +- 类型:已修复缺陷(诊断不对称 → 直接导致一次错误判断) +- **发现过程**:想量"V1 逐形态覆盖",用 `NN_PREC_TRACE=1` 跑 6 个 GPU 测试,得到 **0 条 `#x`**、只有 4 条 `#a`。做**对照实验**(跑已知必然走 V1 的库外样例——它含 rparam ⇒ native16 结构谓词不通过)——**样例也一行都不打** ⇒ 说明是**测量工具**瞎,不是"V1 没被用"。 +- **根因**:`compute_gpu_engine.hpp::find_prec_variant_` 的 `[prec][alu-hit]` 打印**只写在 V2(`#a`)分支**(原 1391-1392 行),V1(`#x`)分支与 V0 分支都**静默返回**。 +- **处置**:V1 分支补同款日志(V0 默认路径**仍不记**——f32 热路径逐调用打日志会淹没输出)。现规则:**变体选择事件(V1/V2)一律打日志**。 +- **修正后的实测(`f16_precision_test` 单测即可)**:12040 次分派、**24 条 distinct 结构 = 20 条 V1(`#x`)+ 4 条 V2(`#a`)**;V1 的 20 条**覆盖全部四个生成器族**:元素 9 / 归约 6 / matmul 4 / fold 1。该测试用 `CHECK_NEAR` 容差断言 ⇒ **V1 有真实的逐结构数值覆盖**,"V1 无覆盖"不成立。 +- **顺带**:`scan_exprs` 的来源 dump 新增 `[scan][both]`(锚点∩dry-run 交集,原 84 条里 38 条落在这里)——否则"某结构由哪个生成器负责、运行时是否按 V1 分派"无法从 dump 回答(V1 覆盖的 20 条里 18 条在交集中)。 +- **教训(本会话第二次)**:一个覆盖/一致性结论在写进文档前,**必须先有一个已知必然命中/必然不命中的对照样本**。上一次是"镜像对"(粗摘要相同但指令序列不同),这一次是"trace 对 V1 盲"。 ## 「锚点 vs dry-run」边界实测刻画(2026-10-01,round 15) diff --git a/include/neuralnet.cpp/compute_gpu_engine.hpp b/include/neuralnet.cpp/compute_gpu_engine.hpp index c9107a14..0f264057 100644 --- a/include/neuralnet.cpp/compute_gpu_engine.hpp +++ b/include/neuralnet.cpp/compute_gpu_engine.hpp @@ -1395,7 +1395,18 @@ class GpuEngine final : public ComputeEngine } // V1(运行期精度分派):键 = key#x —— 一个 shader 覆盖任意签名, // 因此**任何非零签名都必定命中**(不再有 miss / 边界 cast 回退)。 - return nn::fused::find_fused(key + nn::EXPR_PREC_DISPATCH_SUFFIX); + // + // 诊断(NN_PREC_TRACE=1):**变体选择事件一律打日志**。此前只有上面的 + // V2 分支打,V1 分支静默 —— 后果是"某结构实际按 V1 走"在 trace 里完全 + // 不可见:实测库外样例(含 rparam ⇒ native16 结构谓词不通过 ⇒ 必然走 + // V1)在 trace 下**一行 `[prec][alu-hit]` 都不打**,足以误导出"V1 从未被 + // 使用、测试零覆盖"的结论(本分支加日志前作者就据此误判过一次)。 + // 只记变体选择(V1/V2),不记 V0 默认路径——后者是 f32 热路径,逐调用 + // 打日志会把输出淹没。 + const std::string xkey = key + nn::EXPR_PREC_DISPATCH_SUFFIX; + if (nn::dsl::env_flag("NN_PREC_TRACE")) + std::fprintf(stderr, "[prec][alu-hit] %s\n", xkey.c_str()); + return nn::fused::find_fused(xkey); } #endif // ══════════════════════════════════════════════════════════════════════ diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index 3a666dc4..5fdd94ef 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -665,6 +665,14 @@ int main(int argc, char* argv[]) if (dump_src) std::printf("[scan][anchor-only] %s %s\n", k.c_str(), summarize(s).c_str()); } + // 交集(锚点与 dry-run 都产出的结构)也打印:否则"某结构由哪个生成器 + // 负责、运行时是否按 V1 分派"这类问题无法从 dump 里回答——V1 覆盖统计 + // 的 20 条里有 18 条落在交集里。 + if (dump_src) + for (const auto& s : reg.specs) + if (anchor_keys.count(nn::expr_spec_key(s))) + std::printf("[scan][both] %s %s\n", + nn::expr_spec_key(s).c_str(), summarize(s).c_str()); for (const auto& s : anchor.specs) reg.add(s); std::printf("[scan] 结构来源:dry-run %zu(含模型 pass +%zu、显式登记 +%zu)" From 14892f52ab81814225b43084d6a87b12065e2fc3 Mon Sep 17 00:00:00 2001 From: EthanPeng-2048 Date: Thu, 1 Oct 2026 20:08:51 +0800 Subject: [PATCH 30/30] =?UTF-8?q?=E5=AE=A1=E6=A0=B8=EF=BC=9A=E4=BF=AE=209?= =?UTF-8?q?=20=E5=A4=84=E7=9C=9F=E7=BC=BA=E9=99=B7=20+=20=E6=B6=88?= =?UTF-8?q?=E9=99=A4=204=20=E7=B1=BB"=E4=B8=A4=E5=A4=84=E5=90=8C=E6=AD=A5"?= =?UTF-8?q?=E7=82=B9=EF=BC=88=E8=9E=8D=E5=90=88=20AOT=20=E5=88=86=E6=94=AF?= =?UTF-8?q?=E6=94=B6=E5=B0=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对 feat/fused-runtime-prec-dispatch 相对 main 的全部改动做完整代码审核 (简化/规范化/指针与内存/逻辑),本轮修掉的真缺陷(非风格问题): 1. native16 谓词与 key 错配(**静默错值**):expr_prec_sig_native16 含常量值 判据(0<|c|<6e-8 在 f16 下 flush-to-0),而步骤①让常量值退出 key ⇒ 生成期 用"生成那一份时的常量"判定、运行期按位图选 #a,同结构换一组常量即被套上会 flush 的 native16 shader。修:选 #a 前用**真实实例的 spec 重跑同一谓词** (find_prec_variant_ 改收 const ExprSpec&)。 2. [prec][alu-hit] 先打后查 + 双重打点:V1 分支在 find_fused 返回前就打印 (miss 也报 hit),且 supports_expr_precision_variant(每 op 前置查询)也打 ⇒ 计数翻倍(实测 12040 行 = 6020 次真实分派 × 2)。修:确认命中后才打、 只由真正 dispatch 的三个入口传 log_hit;miss 打 [prec][fallback]。 3. push constant 固定头长度两侧各推一遍(创建侧 vs 写入侧入参来源不同): 降为**注册时算一次 + 存 map**(fused_pc_base_fixed_),写入侧只读。 4. in_rd 用子串替换拼 f16 视图名:找不到前缀时静默保留 f32 视图名 ⇒ 把 2B 缓冲当 float 读。改为显式拼 b[…] / bh[…]。 5. nn_enable_gpu_fusion 的 include 顺序不成立:普通 target_include_directories 对重复项保留首次出现位置,库内 GPU 循环先加了 generated ⇒ 追加的 NNF_OUT_DIR 反排在后面。改用 **BEFORE**。 6. 收集器不继承目标的编译条件(NN_HAS_VULKAN/私有 include/自定义宏)⇒ 使用者 调用点静默消失。改为把目标的 INCLUDE_DIRECTORIES/COMPILE_DEFINITIONS/ COMPILE_OPTIONS 用生成器表达式传给收集器;MAIN 未匹配即 FATAL_ERROR。 7. 模型 pass 失败只 warning(它贡献 25/84 条结构)⇒ 改为**失败即构建失败**, 与 dry-run 的 fail-fast 一致;NN_SCAN_NO_DRYRUN 同时门控模型 pass(与文档语义对齐)。 8. 两条"永远通过"的测试:fold tri_skip 进 key 用 Causal vs Plain 比 key(body 本就不同)→ 改为只翻该字段;expr_fused_key_test 里与 expr_matmul_test 逐字 重复的 mm_trans 位编码断言删掉一份。 9. read_spv 不校验 tellg()(失败返回 -1 → size_t(-1)/4 请求 ~4GB)+ 忽略截断读。 删除死物:prec_manifest 形参、ExprRegistry::contains、fusion_anchor_touch (实测删除后库外样例的 2 条自定义结构仍被锚点登记:registry 84 → 86 不变)、 std::exit(1)、scan_exprs 里重复 4 次的 matmul trans 登记、AGENTS 里 4 处有争议 的 ctest 19/19。 简化/去重(消掉"同一份知识写在两处"的同步点):glsl_vp_slot() 取代 6 处逐字 重复的 vp 槽累计;scan_register() 取代 3 个 dsl 入口各写一遍的 锚点+折叠+校验+登记;is_fusable/fused_vec_width_for 取代三个循环各写一遍的 判据;expr_variant_out_f16() 取代 4 处输出精度选择;EXPR_PREC_ALU_SUFFIX 取代 3 处硬编码 "#a";tool_env_flag() 取代两份 _dupenv_s 样板;删除 f16 双 float16_t() 冗余转换与 GroupedReduce 在分派模式下的重复三元。 检测口径收紧:expr_prec_sig_in_f16 先与 EXPR_PREC_SIG_INPUT_MASK 相与(否则 哨兵 0x00020000 的 bit17 被读成"第 17 个输入是 f16");分组归约辅助函数在 matmul/归约生成器也发射(此前只有 elementwise 发,含该视图的 spec 会生成未声明 函数);--list-backends 排序(铁律 8);分发查询改用 reg.keys 而非重建集合。 文档纠偏:AGENTS §7/§12、docs/development/02/05/18、docs/usage/03 中"运行期 永不 miss""[prec][miss] 已删除""bin v9 变体段""gen_fused 产物""ctest 19" "12040 次分派"等表述统一为当前事实;05 §12.7 改为历史机制(标注已删除)、 §12.10 ③ 标注为 V0 时代口径;history.md 补本轮条目与 19/20/21 计数口径来源。 验收(本机 Debug+Ninja+clang):build 零告警(-Werror);ctest 21/21; NN_BIND_DEBUG=1 ctest 全绿;CPU 字节锚 6f8849f14da23110 与 GPU dev2 锚 8ef51b2927253c50 逐位不变(GLSL 文本改动全部数值等价);库外样例端到端值不变 (0.6712035 / 0.6712036 / 0.6708984 / 1.2246094);结构数 84(例 86)不变; NN_PREC_TRACE 12040 → 6022 行、distinct 仍 24(20 #x + 4 #a); L2-VIOLATIONS: 0;doc_align_audit A/D/E/F 可行动项 0。 教训:9 条真缺陷里 4 条同形——同一份知识写在两处、靠注释维持同步(谓词 vs key、 创建侧 vs 写入侧 PC 长度、f16 视图名 vs 子串替换、目标编译条件 vs 收集器); 另两条是"测量/断言必须先证明它能失败"的延续(round 15/19 已各记一次)。 --- AGENTS.md | 16 +- CMakeLists.txt | 83 +++++-- docs/development/02-operator-fusion.md | 12 +- docs/development/05-mixed-precision.md | 40 ++-- docs/development/18-roadmap.md | 22 +- docs/history.md | 41 +++- docs/usage/03-compute-engine-usage.md | 2 +- examples/fusion_custom_layer.cpp | 25 +- examples/fusion_custom_layer.hpp | 5 +- .../backend/compute_vk_backend.hpp | 81 ++++--- include/neuralnet.cpp/compute_gpu_engine.hpp | 143 ++++++----- include/neuralnet.cpp/expr_dsl.hpp | 73 +++--- include/neuralnet.cpp/expr_glsl_gen.hpp | 204 ++++++++-------- include/neuralnet.cpp/expr_registry.hpp | 10 +- include/neuralnet.cpp/expr_spec.hpp | 35 ++- src/expr_cpu_test.cpp | 4 +- src/expr_fold_test.cpp | 18 +- src/expr_fused_key_test.cpp | 29 +-- src/maxpool_gradcheck.cpp | 6 +- tools/fused_generate.hpp | 225 +++++++++--------- tools/scan_exprs.cpp | 211 ++++++++-------- 21 files changed, 707 insertions(+), 578 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 301f14df..a2e9e4b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -180,9 +180,9 @@ GPT 序列展平: 列序 i = b*seq + t(batch-major,全局唯一约定) - **构建期单步**(CMake 自动编排,改 Layer 内联表达式后重跑构建即可)——`scan_exprs` 同进程**收集 + 生成**: 1. **收集**:只收集**结构**(= 唯一需要构建期枚举的东西)。两个来源**互补、不冗余**(`NN_SCAN_DUMP_SOURCES=1` 可逐条打印来源):`FusedAnchor` 在静态初始化期按表达式**类型**自登记(编译期可达,dry-run 跑不到的分支也覆盖);per-layer dry-run + 模型 pass 执行补齐**运行期配置决定**的变体(视图种类/项数在类型层面推不出,符号实例 `Expr{}` 与真实实例会折叠出不同 key);fold spec(3 掩码 × 2 偏置的 5 个组合等 8 条)与 matmul trans 族由 `scan_exprs` **显式登记**。**只跑 f32 一遍**——实测 f16 遍对结构贡献为 0(跳过它签名 66 → 0、结构恒 84),该两遍已删除。**实测分工(84 条)**:per-layer dry-run 38(含模型 pass +25、显式登记 +8)、锚点 59,重叠 46 → **锚点独有 13、dry-run 独有 25**。所以**任何一方单独删掉都会丢结构**——dry-run/model pass 是结构的主要来源,删不掉。 2. **生成**:`tools/fused_generate.hpp`(原独立工具 `gen_fused`)对**每个结构发三份**:V0 全 f32(键 = `key`)/ **V1 运行期精度分派**(键 = `key#x`)/ V2 native16(键 = `key#a`,按 `expr_prec_sig_native16` 的**结构谓词**判定)→ 经 `emitter_registry` 选后端(默认 `"glsl"` = `GlslEmitter`)生成 GLSL → glslc → 内联 SPIR-V → `build/generated/fused_registry.hpp`。**不经 `.bin` 中间序列化**(`write_registry/read_registry` 已删除)。 -- **精度签名不再是构建期集合**("签名从身份降级为参数"):V1 的 GLSL 对每个输入/输出各声明 f32 + `float16_t` 双视图,加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支,**一份 shader 覆盖任意签名**。因此运行期**不存在 miss、不存在边界 cast 回退、不存在回填清单**(`tools/prec_backfill.txt` 已删除,`NN_PREC_TRACE` 的"补变体"流程随之失效)。V1 对每个结构无条件生成 ⇒ 任何签名都命中。**变体选择事件(V1/V2)一律打 `NN_PREC_TRACE` 日志**(V0 默认路径不打——f32 热路径会淹没输出;V1 分支曾漏打,导致"V1 从未被使用"的错误判断)。**实测 V1 覆盖**(`NN_PREC_TRACE=1 f16_precision_test`):12040 次分派 / 24 条 distinct 结构 = **20 条 V1 + 4 条 V2**,V1 覆盖**全部四个生成器族**(元素 9 / 归约 6 / matmul 4 / fold 1),且该测试用 `CHECK_NEAR` 容差断言。 +- **精度签名不再是构建期集合**("签名从身份降级为参数"):V1 的 GLSL 对每个输入/输出各声明 f32 + `float16_t` 双视图,加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支,**一份 shader 覆盖任意签名**。因此构建期不需要枚举签名,也**不存在回填清单**(`tools/prec_backfill.txt` 已删除,`NN_PREC_TRACE` 的"补变体"流程随之失效)。⚠ 精度路径仍有**三档**、不是"永不 miss":`key#a`(native16)→ `key#x`(V1 运行期分派)→ **基类边界 cast**;当某个结构的 `#x` 生成被跳过(`[skip] 分派变体…`)、`NN_SCAN_NO_DISPATCH=1`、或设备无 16bit 存储时,运行期会 miss 并落到边界 cast(`[prec][miss]` 仍会触发,它是通用诊断)。**V2 谓词含常量值判据而常量值不进 key**:运行时选 `#a` 前会用**真实实例的 spec 重跑** `expr_prec_sig_native16`,否则"生成时那一个实例的常量"会被套用到同结构但常量不同的实例上(微值在 f16 下 flush-to-0 = 静默错值)。**变体选择事件(V1/V2)一律打 `NN_PREC_TRACE` 日志**(V0 默认路径不打——f32 热路径会淹没输出;V1 分支曾漏打,导致"V1 从未被使用"的错误判断),且**只在确认命中后打、只由真正 dispatch 的入口打**(`supports_expr_precision_variant` 只是查询,它打日志会让计数翻倍:实测修复前 12040 行 = 6020 次真实分派 × 2)。**实测 V1 覆盖**(`NN_PREC_TRACE=1 f16_precision_test`):6022 条命中日志 / 24 条 distinct 结构 = **20 条 V1(`#x`)+ 4 条 V2(`#a`)**,V1 覆盖**全部四个生成器族**(元素 9 / 归约 6 / matmul 4 / fold 1),且该测试用 `CHECK_NEAR` 容差断言;miss 为 0。 - **新增调用点无需改动其他文件**(实测):在 Layer 头里新写一个 `dsl::compute`(其结构不在任何 dry-run / 模型 pass 覆盖内)→ 锚点独有 13 → 14、结构 84 → 85、注册表新增 `key` / `key#x` / `key#a` 三条,**其他文件零改动**。 -- **库外使用者:`nn_enable_gpu_fusion( MAIN <含 main 的源>)`**(`CMakeLists.txt`):使用者在自己的层头里写 `dsl::compute` + 在 CMake 加这一行 → 自动生成**该目标专属**的 `fused_registry.hpp` 并加进它的 include path。机制 = 收集器由**库内收集逻辑**(`tools/scan_exprs.cpp`:锚点 + dry-run + 模型 pass + 显式登记)**加上本目标的全部 TU**(`-DNN_EXPR_SCAN` 编译,让使用者的调用点自登记)组成。**⚠ 必须保留库内收集逻辑**:84 个结构里只有 13 个来自编译期锚点、71 个来自 dry-run/模型 pass,"纯锚点收集器"会漏库自身的结构。**⚠ include 顺序有语义**:`${NNF_OUT_DIR}` 必须排在 `${CMAKE_BINARY_DIR}/generated` 之前(后者含库自身注册表与手写原语 shader 嵌入头),否则 `__has_include("fused_registry.hpp")` 会解析到库的那一份、使用者的自定义结构全部闭合世界报错。样例见 `examples/fusion_custom_layer*`。 +- **库外使用者:`nn_enable_gpu_fusion( MAIN <含 main 的源>)`**(`CMakeLists.txt`):使用者在自己的层头里写 `dsl::compute` + 在 CMake 加这一行 → 自动生成**该目标专属**的 `fused_registry.hpp` 并加进它的 include path。机制 = 收集器由**库内收集逻辑**(`tools/scan_exprs.cpp`:锚点 + dry-run + 模型 pass + 显式登记)**加上本目标的全部 TU**(`-DNN_EXPR_SCAN` 编译,让使用者的调用点自登记)组成。**⚠ 必须保留库内收集逻辑**:84 个结构里只有 13 个来自编译期锚点、71 个来自 dry-run/模型 pass,"纯锚点收集器"会漏库自身的结构。**⚠ include 顺序有语义**:`${NNF_OUT_DIR}` 必须排在 `${CMAKE_BINARY_DIR}/generated` 之前(后者含库自身注册表与手写原语 shader 嵌入头),否则 `__has_include("fused_registry.hpp")` 会解析到库的那一份、使用者的自定义结构全部闭合世界报错。样例见 `examples/fusion_custom_layer*`(**库内目标**形态;函数本身定义在本仓 `CMakeLists.txt` 的 `if(Vulkan_FOUND)` 内,且仓库无 `install()`/export——真正**树外**工程要复用它需先做 install/export,见 `docs/development/18-roadmap.md` E2)。 - **端到端验收(库外 + f16 分派)**:`build/fusion_custom_layer_example.exe` 实测两种**自研算子形态**:①逐元素 `exp(|x|·α)/(√|y|+α)`;②归一化形态 `rsqrt(col_reduce_sum(x)·inv + ε)`(= 库内 LayerNorm/RMSNorm 的 `std_inv` 同形,`raxis=1`、`viewkinds=[0,8]`,属库里"只有执行 Layer 才拿得到"的那一类)。两者都只用**锚点**(自定义层没有 dry-run)→ 都命中并跑通 ⇒ `nn_enable_gpu_fusion` 对常见自定义层可靠。实测值:CPU f32 `0.6712035`(差 0);**GPU f32(V0)`0.6712036`(差 5.96e-08)**;**GPU f16(V1 运行期精度分派)`0.6708984`(差 3.05e-04,f16 舍入内)**;归一化形态 GPU f16 `1.2246094`。含 rparams ⇒ native16 谓词不通过 ⇒ f16 命中必然是 `key#x`;注册表若漏了它就是闭合世界硬报错,"跑通"本身即断言。**残余不确定性**:库内确有 25 条结构只有执行 Layer/模型才拿得到(锚点的符号实例 `Expr{}` 与运行期实例折叠结果不同);实测的两种自定义形态不在其中,但无法先验排除。若使用者的自定义层遇到闭合世界报错,下一步是给 `nn_enable_gpu_fusion` 加 `COLLECT ` 钩子(由使用者提供"跑一遍自己的层"的收集函数,等价于库内的模型 pass)。 - **库是 header-only,多 TU 必须可链接**:头里的类外定义一律要 `inline`(曾漏 `GpuBuffer::~GpuBuffer` → ≥2 个 TU 的程序链接期 duplicate symbol,`examples/fusion_custom_layer*` 抓到)。 - **IR-D emitter 抽象**(`expr_emitter.hpp`):把后端代码生成从 GLSL 专用抽象为 emitter 接口(一份 canonical IR → 多后端代码),`--list-backends` 可列出注册后端。目前仅 `glsl` 后端注册(无 `CpuEmitter`/`CudaEmitter` 实现)。 @@ -297,13 +297,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 19/19 + 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 全绿 + 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 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`)。 +- **批量读写(统一总纲 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 全绿 + `--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 全绿(详见 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` 打通成**可执行门禁**(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`)。 +- **跨设备拉取 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` 打通成**可执行门禁**(含修两处库内中转漏网 + `bind_error_` 同步打 stderr)。验收:ctest 全绿(带与不带 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 全绿(带与不带 `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`)。 @@ -314,7 +314,7 @@ optimizer.step(); - **评估分块**:`evaluate_mnist` 的 `eval_batch`(默认 1000)分块前向 + 每块 `release_idle_pool_blocks()`,防大 batch 评估 OOM。 - **BPE 保序并行 encode**:`Tokenizer::set_encode_threads`(0=自动/1=顺序/>1=指定)+ `encode_segments_`——按空白安全切分点分段、段内经全局线程池并发编码、**按段下标升序拼接**,任意并行度与顺序执行**逐字节一致**(铁律 #8;切分点必为 chunk 边界、标记不含空白不会被切断);文本 < 256 KiB 或找不到切分点回退顺序路径。`bpe_merge_impl_` 改 thread_local `BpeMergeScratch` + 手写堆,每 chunk 合并**零堆分配**。`text_train::parallel_tokenize` 收编到 `nn::parallel_for_samples`,`tokenizer_infer` 新增 `--threads`。实测 8 MiB 多样文本 `encode` 0.93s→0.134s(32 线程,7.0x);`.tokcache` 重新生成 SHA256 与旧实现一致。**训练期合并循环仍不可并行**(链式依赖,见 `docs/development/08` §4.2)。 - **CPU 性能**:DSL 模板路径向量化/并行、`dsl::compute_into` 零分配原地更新、`Tensor::cpu_get_ptr`、分块 GEMM 内核(BLOCK_SIZE=64)。 -- **测试**:ctest 注册 **21** 个测试(**19** 个测试目标 + `cnn_test_gpu` = `cnn_test --gpu` + `fusion_custom_layer_example` = 库外使用者形态的 AOT 融合端到端门禁;`-DNN_ENABLE_TESTS=ON`;需 Vulkan 的用例退出码 77 = skip)。其中 `fused_gpu_test` = **融合 shader 逐形态** GPU 对拍(rope/swiglu/gelu/softmax/matmul/matmul+reduce/norm/reduce_consts/fold v1/fold attn/回退硬报错)。 +- **测试**:ctest 注册 **21** 个测试(**19** 个测试目标 + `cnn_test_gpu` = `cnn_test --gpu` + `fusion_custom_layer_example` = 下游自定义层形态的 AOT 融合端到端门禁;`-DNN_ENABLE_TESTS=ON`;需 Vulkan 的用例退出码 77 = skip)。其中 `fused_gpu_test` = **融合 shader 逐形态** GPU 对拍(rope/swiglu/gelu/softmax/matmul/matmul+reduce/norm/reduce_consts/fold v1/fold attn/回退硬报错)——覆盖口径是 **V0(全 f32)**,V1/V2 的覆盖在 `f16_precision_test`(`NN_PREC_TRACE` 实测 20 条 `#x`、4 条 `#a`)。 ### 融合二期状态与 IR 编码约束(改融合/IR 代码前必读) diff --git a/CMakeLists.txt b/CMakeLists.txt index 06a40da9..85869fd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -296,9 +296,11 @@ if(NN_ENABLE_TESTS) # ── 融合 shader **逐形态** GPU 对拍(rope/swiglu/gelu/softmax/matmul/ # matmul+reduce/norm/reduce_consts/fold v1/fold attn/回退)────────── - # ⚠ 此前该文件**未被任何 CMake 目标引用**(未编译、未注册)——它恰好是 - # 覆盖"每个生成器各自的 V1 运行期精度分派形态"的测试,属于 AOT 融合的 - # 核心回归面。重新接入。 + # ⚠ 此前该文件**未被任何 CMake 目标引用**(未编译、未注册)——它是融合 + # shader 逐形态的回归面,重新接入。 + # 覆盖口径:**V0(全 f32)**逐形态对拍(该文件无 f16/精度引用)。 + # V1(`key#x` 运行期精度分派)的覆盖在 `f16_precision_test`(NN_PREC_TRACE + # 实测 20 条 #x / 4 条 #a);本测试不覆盖 V1。 nn_add_executable(fused_gpu_test src/fused_gpu_test.cpp) list(APPEND NN_TEST_TARGETS fused_gpu_test) @@ -375,13 +377,12 @@ if(Vulkan_FOUND) scan_suffix_outer_gen outer_col cast) - # ── 融合 shader:构建期从 Layer 内联表达式自动合成 ─────────────── + # 融合 shader:构建期从 Layer 内联表达式自动合成(**单步**)────────── # 表达式**文本只出现在 Layer**(compute / compute_reduce / compute_into)。 - # 构建期两步: - # ① scan_exprs:dry-run 跑 Layer 的 forward/backward,收集折叠出的 - # ExprSpec 结构(去重)→ expr_specs.bin(派生物,非手写定义) - # ② gen_fused:读 bin → glsl_gen → glslc → 内联 SPIR-V - # → fused_registry.hpp(key=expr_spec_key → 融合 shader) + # scan_exprs 同进程内完成:收集折叠出的 ExprSpec 结构(锚点自登记 + dry-run + # + 模型 pass + 显式登记,按 key 去重)→ glsl_gen → glslc → 内联 SPIR-V + # → fused_registry.hpp(key=expr_spec_key[#x|#a] → 融合 shader)。 + # 不经 .bin 中间序列化(原独立工具 gen_fused 已并入 tools/fused_generate.hpp)。 # 运行时按 key 精确匹配 dispatch;未命中硬报错(闭合世界)。 set(GPU_SHADER_HPPS "") @@ -515,6 +516,13 @@ if(Vulkan_FOUND) # docs/development/02)。纯锚点收集器会漏掉库自身的结构。 function(nn_enable_gpu_fusion target) cmake_parse_arguments(NNF "" "MAIN;OUT_DIR" "" ${ARGN}) + if(NOT TARGET ${target}) + message(FATAL_ERROR "nn_enable_gpu_fusion: 目标 ${target} 不存在") + endif() + if(TARGET ${target}_nnfusion_collect) + message(FATAL_ERROR + "nn_enable_gpu_fusion: 目标 ${target} 已调用过(${target}_nnfusion_collect 已存在)") + endif() if(NOT NNF_OUT_DIR) set(NNF_OUT_DIR "${CMAKE_BINARY_DIR}/nn_fusion/${target}") endif() @@ -523,20 +531,37 @@ if(Vulkan_FOUND) endif() get_target_property(_nnf_srcs ${target} SOURCES) set(_nnf_inputs "") + set(_nnf_main_found FALSE) foreach(_s IN LISTS _nnf_srcs) if(_s MATCHES "^\\$<") # 生成器表达式源(跳过,罕见) continue() endif() get_filename_component(_abs "${_s}" ABSOLUTE) - if(NOT NNF_MAIN OR NOT _abs STREQUAL NNF_MAIN) - list(APPEND _nnf_inputs "${_abs}") + if(NNF_MAIN AND _abs STREQUAL NNF_MAIN) + set(_nnf_main_found TRUE) + continue() # 收集器自带 main(SCAN_SRC),排除目标的 endif() + list(APPEND _nnf_inputs "${_abs}") endforeach() + # MAIN 拼写/路径不一致会让目标的 main 进入收集器 → 链接期 duplicate main。 + # 静默通过不了(链接会报),但报错点离根因很远,这里提前拦住。 + if(NNF_MAIN AND NOT _nnf_main_found) + message(FATAL_ERROR + "nn_enable_gpu_fusion: MAIN=${NNF_MAIN} 不在 ${target} 的源文件列表里") + endif() set(_nnf_reg "${NNF_OUT_DIR}/fused_registry.hpp") add_executable(${target}_nnfusion_collect ${SCAN_SRC} ${_nnf_inputs}) target_include_directories(${target}_nnfusion_collect PRIVATE ${CMAKE_SOURCE_DIR}/include/neuralnet.cpp) - target_compile_definitions(${target}_nnfusion_collect PRIVATE NN_EXPR_SCAN) + # 收集器必须按**与目标相同的编译条件**编译其 TU:使用者的层代码常在 + # `#ifdef NN_HAS_VULKAN` / 私有 include / 自定义宏之内,条件缺失会让那些 + # 调用点静默消失 → 收集不到 → GPU 运行期闭合世界硬报错(离根因很远)。 + target_include_directories(${target}_nnfusion_collect PRIVATE + $) + target_compile_options(${target}_nnfusion_collect PRIVATE + $) + target_compile_definitions(${target}_nnfusion_collect PRIVATE + NN_EXPR_SCAN $) target_link_libraries(${target}_nnfusion_collect PRIVATE nn_core Vulkan::Vulkan) add_custom_command( OUTPUT ${_nnf_reg} @@ -550,7 +575,10 @@ if(Vulkan_FOUND) # (不含使用者的自定义结构)与手写原语 shader 的嵌入头。放在前面 # 才能让 `__has_include("fused_registry.hpp")`(见 # backend/compute_vk_backend.hpp)解析到本目标专属的那一份。 - target_include_directories(${target} PRIVATE + # **必须用 BEFORE**:CMake 的 INCLUDE_DIRECTORIES 属性对重复项保留 + # 首次出现的位置,若目标此前已(由 GPU 统一启用循环)拿到 + # `generated`,普通 append 会把它排在前面,顺序语义反而失效。 + target_include_directories(${target} BEFORE PRIVATE ${NNF_OUT_DIR} ${CMAKE_BINARY_DIR}/generated) add_dependencies(${target} ${target}_nnfusion_registry compile_gpu_shaders) endfunction() @@ -581,20 +609,23 @@ if(Vulkan_FOUND) # 它有自己的自研融合层(examples/fusion_custom_layer.hpp),**不使用** # 库内 build/generated 的注册表,而是靠 nn_enable_gpu_fusion 生成自己 # 的那一份 —— 证明"写 dsl::compute + 一行 CMake 即可"。 - add_executable(fusion_custom_layer_example - examples/fusion_custom_layer_main.cpp - examples/fusion_custom_layer.cpp) - target_include_directories(fusion_custom_layer_example PRIVATE - ${CMAKE_SOURCE_DIR}/include/neuralnet.cpp) - target_compile_definitions(fusion_custom_layer_example PRIVATE NN_HAS_VULKAN) - target_link_libraries(fusion_custom_layer_example PRIVATE nn_core Vulkan::Vulkan) - nn_enable_gpu_fusion(fusion_custom_layer_example - MAIN ${CMAKE_SOURCE_DIR}/examples/fusion_custom_layer_main.cpp) - - # A2 验收注册为**常驻 ctest**:这是"库外使用者写 dsl::compute + 一行 - # CMake → GPU 能跑"的唯一自动门禁(含 f16 运行期精度分派 `key#x` 路径 - # 与归一化形态)。无 Vulkan 设备时退出码 77 = skip。 + # 只在 NN_ENABLE_TESTS 下构建:它的收集器会为 ~85 个库内结构各跑一遍 + # glslc(三份变体),是纯验收成本,不该加到默认构建上。 if(NN_ENABLE_TESTS) + nn_add_executable(fusion_custom_layer_example + examples/fusion_custom_layer_main.cpp + examples/fusion_custom_layer.cpp) + target_compile_definitions(fusion_custom_layer_example PRIVATE NN_HAS_VULKAN) + target_link_libraries(fusion_custom_layer_example PRIVATE Vulkan::Vulkan) + nn_enable_gpu_fusion(fusion_custom_layer_example + MAIN ${CMAKE_SOURCE_DIR}/examples/fusion_custom_layer_main.cpp) + + # 注册为**常驻 ctest**(注册循环在 Vulkan 探测之前跑,故此处手工注册, + # 口径与循环一致:输出目录 + SKIP_RETURN_CODE 77 = 无设备时 skip)。 + # 这是"库外使用者写 dsl::compute + 一行 CMake → GPU 能跑"的唯一自动 + # 门禁(含 f16 运行期精度分派 `key#x` 路径与归一化形态)。 + set_target_properties(fusion_custom_layer_example PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/test) add_test(NAME fusion_custom_layer_example COMMAND fusion_custom_layer_example) set_tests_properties(fusion_custom_layer_example diff --git a/docs/development/02-operator-fusion.md b/docs/development/02-operator-fusion.md index e4551209..1c8ae329 100644 --- a/docs/development/02-operator-fusion.md +++ b/docs/development/02-operator-fusion.md @@ -239,19 +239,19 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba ### 精度签名已不是构建期集合(2026-10-01,A1) -**问题**:签名(哪些输入/输出是 f16)由运行期张量精度决定、类型层面不可见,因此历史上"每个 (结构, 签名) 一份 shader"必须靠**执行**发现(dry-run 跑 f32/f16 两遍 + 模型 pass + `NN_PREC_TRACE` 回填清单)。这是扫描里唯一无法用自登记取代的部分。 +**问题(历史)**:签名(哪些输入/输出是 f16)由运行期张量精度决定、类型层面不可见,因此历史上"每个 (结构, 签名) 一份 shader"必须靠**执行**发现(dry-run 跑 f32/f16 两遍 + 模型 pass + `NN_PREC_TRACE` 回填清单)。**该问题已随"签名降级为参数"消失**,下面的实测数据保留作为历史依据。这是扫描里唯一无法用自登记取代的部分。 **解法 = 把签名从「身份」降级为「参数」**:V1 变体的 GLSL 对每个输入/输出各声明 **f32 + `float16_t` 双视图**(同一 `VkBuffer` 绑两次,只解引用与真实元素类型一致的那一个),加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 **uniform 分支**。索引数学**逐字不变**(两种视图共用同一逻辑下标)→ 与旧带类型变体数值等价。 于是: -- V1 对**每个结构无条件生成** ⇒ **任何签名都命中**,运行期**不存在 miss**、**不存在边界 cast 回退**、**不存在回填清单**(`tools/prec_backfill.txt` 已删除); +- V1 默认对**每个结构**都尝试生成 ⇒ 一份 shader 覆盖任意签名,运行期不再需要"发现签名";精度路径仍是三档 `key#a` → `key#x` → **基类边界 cast**(生成被 `[skip]` / `NN_SCAN_NO_DISPATCH=1` / 设备无 16bit 存储时落到 cast,`[prec][miss]` 仍会触发);**`tools/prec_backfill.txt` 与回填工作流已删除**; - V2(native16,原生 f16 算术)改由**结构谓词**判定生成(代入"全输入 f16 + 输出 f16"这一常量签名),运行时按真实签名确认 —— 生成期同样不需要发现; -- **实测 f16 扫描遍对结构贡献为 0**(`NN_SCAN_F32_ONLY` 探针:跳过它签名 66 → 0、结构恒 84)→ f16 dry-run 遍与 f16 模型 pass 已删除,扫描工作量减半。 +- **实测 f16 扫描遍对结构贡献为 0**(当时的 `NN_SCAN_F32_ONLY` 探针——该宏随删除一并移除:跳过它签名 66 → 0、结构恒 84)→ f16 dry-run 遍与 f16 模型 pass 已删除,扫描工作量减半。 **端到端实测**("写 `dsl::compute` 即自动融合"):在 Layer 头里新写一个结构不在任何 dry-run/模型 pass 覆盖内的表达式 → ``` -[scan] 结构来源:dry-run 38(含模型 pass +25、显式登记 +8) + 锚点 60 → 合并 85(锚点独有 14) +[scan] 结构来源:dry-run 侧 71(per-layer+显式 38、模型 pass +25、fold 显式 +8) + 锚点 60(新增 14)= 合计 85(锚点独有 14、dry-run 独有 25) [scan] 收集到 85 条融合表达式 新增键:7d5efd96f337a4a9 / 7d5efd96f337a4a9#x / 7d5efd96f337a4a9#a ``` @@ -268,7 +268,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`):结构数 **71(仅 dry-run/模型 pass)→ 84(+锚点)**,多出的 13 条都是编译进来的真实调用点(含 matmul、归约、16 个 vp 槽的宽表达式等)。 -- **两者互补,不冗余**(`NN_SCAN_DUMP_SOURCES=1` 逐条打印来源,实测 84 条):per-layer dry-run 38(含模型 pass +25、显式登记 +8)、锚点 59,**重叠 46 → 锚点独有 13、dry-run 独有 25**。所以**单独删任何一方都会丢结构**: +- **两者互补,不冗余**(`NN_SCAN_DUMP_SOURCES=1` 逐条打印来源,实测 84 条):per-layer+显式登记 38 + 模型 pass 25 + fold 显式 8 = **dry-run 侧 71**、锚点 59,**重叠 46 → 锚点独有 13、dry-run 独有 25**(口径:三段互不重叠;打印行见下)。所以**单独删任何一方都会丢结构**: - **dry-run 独有 25 条**按形态分:**fold 8 条**(= 显式登记的掩码×偏置组合与通用 fold 样例,见下)、**含 matmul 段 7 条**(注意力反向 / CE / 优化器步)、**RowAccess(12) 4 条**(共享内存 SwiGLU 的半偏移行切分)、**RowGather(9)/ColBroadcast(8) 若干**(稀疏 CE 的 5 视图形态 `[0,8,8,8,8]` 等)——共同点是**结构由运行期配置决定**(视图种类/项数/掩码组合),类型层面推不出来,符号实例 `Expr{}` 与真实实例会折叠出**不同 key**(实测存在"视图种类与指令数完全相同、只有 key 不同"的镜像对,如 dry-only `784781c7b82fa76c` ↔ anchor-only `28cdf7c6d5df0e1d`)。 - **锚点独有 13 条**是镜像面:dry-run/模型 pass 没跑到的编译期可见调用点(最著名的是 `9ca81b4967cdfa20`——关掉锚点后 `text_infer` KV-cache 增量解码在 GPU 上闭合世界硬报错)。 - **结论**:dry-run + 模型 pass 是结构的主要来源,**删不掉**;锚点补它跑不到的部分。这也是"扫描步骤本身无法删除"的精确根因。 @@ -281,7 +281,7 @@ forward = 单 fold kernel(`FoldSpec` 分块流式,见 §关键算法);ba 保留的是**整模型跑不到**的来源:优化器(5 变体)、损失(MSE / CE 稠密 / CE 稀疏)、RoPE `apply_step`(增量推理)、CSA×4(掩码/偏置组合,含模型未覆盖的 doc 变体)、MHA、以及 `scan_exprs` 内的**显式测试覆盖登记**(matmul+bias+relu / bmm_reduce / reduce_consts)。 - **常驻分项台账**(每次构建打印,结构侧): ``` - [scan] 结构来源:dry-run 38(含模型 pass +25、显式登记 +8) + 锚点 59 → 合并 84(锚点独有 13、dry-run 独有 25) + [scan] 结构来源:dry-run 侧 71(per-layer+显式 38、模型 pass +25、fold 显式 +8) + 锚点 59(新增 13)= 合计 84(锚点独有 13、dry-run 独有 25) ``` A/B 开关:`-DNN_SCAN_NO_ANCHOR`(关锚点)/ `-DNN_SCAN_NO_DRYRUN`(关 dry-run/模型 pass),均只影响登记集合、不改语义。逐条来源用 `NN_SCAN_DUMP_SOURCES=1` 打印(`[scan][dry-only]` / `[scan][anchor-only]` + 结构摘要)。 - **run-only 签名回填已删除**(A1e):`--f16` 下曾有一批只有真实训练才暴露的签名(注意力 fold 的文档掩码 5 输入形态、MNIST/CNN f16 训练的 8 条"f32 入 + f16 出"混合签名),靠 `NN_PREC_TRACE=1` 的 `[prec][miss]` 收集后写进 `tools/prec_backfill.txt`。**该清单与其工作流已随 A1 整体删除**——V1 分派 shader 覆盖任意签名,f16 训练实测 `miss=0` 且边界 cast 归因表为空。历史清单条目与复现命令归档在 `docs/history.md`。 diff --git a/docs/development/05-mixed-precision.md b/docs/development/05-mixed-precision.md index 94682919..689a2307 100644 --- a/docs/development/05-mixed-precision.md +++ b/docs/development/05-mixed-precision.md @@ -572,16 +572,20 @@ loss = ce.forward_sparse(engine, logits, labels, mask, vocab, - **结论:签名必须逐输入**。实测存在 `in=[f16,f32] out=f32`、`in=[f32,f16,f16] out=f32`、`in=[f16,f32,f32] out=f16` 等混合组合(LayerNorm/loss 链与参数/激活的精度不同源)——"全 f16 / 全 f32 两变体"的方案会漏掉相当一部分。 - 逐元素/归约占 84%(32/38),且是激活侧 transient 的主要来源(残差、激活、Norm 内部量都在这一类)→ 变体按类别逐批生成:纯逐元素(§12.7)→ matmul 段 / 含归约 / 目标传递(§12.10)→ op-level f16 GEMM(§12.11);fold 变体与运行时签名回填见 §12.10 ⑤。 -### 12.7 in-kernel f16 第一期:纯逐元素带类型变体(当前机制) +### 12.7 in-kernel f16 第一期:纯逐元素带类型变体(**历史机制**,已由 §12.10 ⑤ 取代) -| 组件 | 当前实现 | +> 本节记录的是 **2026-09 的第一期机制**(构建期按签名枚举变体 + `bin v9` 变体段 + `key#sig` 注册键)。 +> 该机制已于 **2026-10-01(A1 段)** 整体删除:签名从「身份」降级为「参数」,变体集合不再按签名枚举 +> (见 §12.10 ⑤)。保留本节是为了解释"为什么当初这么做、后来为什么不需要"——**不是当前实现**。 + +| 组件 | 当时的实现(**全部已删除/改写**) | |---|---| -| `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`。 | -| 生成阶段 | 每变体独立 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。 | +| ~~`scan_exprs` 双 pass~~ | 整段 dry-run 收进 `dry_run(engine, profile)`(`profile_f32` + `profile_f16`)。**已删除**:实测 f16 遍对结构贡献为 0(跳过它签名 66 → 0、结构恒 84),而签名已不是构建期集合。 | +| ~~`bin v9` 变体段~~ | 规格表之后的变体段,每变体只存 `{sig, 基础结构下标}`。**已删除**:`write_registry/read_registry` 连同整个 `.bin` 中间序列化一起移除(构建期单步)。 | +| `GlslEmitter` | `generate/generate_reduce` 的 `sig` 形参保留至今,但语义已扩展:`sig == 0` → V0 全 f32;`sig == EXPR_PREC_SIG_DISPATCH` → V1 运行期精度分派;真实位图 → V2 存储变体;`native16=true` → V2b 原生 f16 算术(键 `#a`)。 | +| ~~生成阶段按 `key#sig` 注册~~ | 当时的键 = `key#`(+ `#a`)。**已消失**:现在的键只有 `key`(V0)/ `key#x`(V1)/ `key#a`(V2b),不带签名 hex。 | +| 设备 | 仍有效:查询并启用 `storageBuffer16BitAccess`(`NN_VULKAN_NO_16BIT_STORAGE=1` 强制回退);未启用时后端跳过带 `#` 的键。 | +| 运行时 | 仍有效的两条经验:① `FusedInputs{owners, bufs}` **必须同时持有 owner**——只存裸 `GpuBuffer*` 会在录制中途释放上传缓冲(铁律 6,实测 `A+=B err=0.5`);② **f16 输出必须按 2B/元素分配缓冲并重贴 `GpuTensorF16`**(漏了 = shader 只写前半 + f32 标签 → 训练 loss=NaN)。变体选择改为 §12.10 ⑤。 | 纯逐元素变体阶段的实测(f32 / f16 边界 cast / in-kernel 三方对照)与当时的"剩余缺口"清单见 `docs/history.md`;**当前数字与下一步见 §12.11**。 @@ -617,15 +621,20 @@ loss = ce.forward_sparse(engine, logits, labels, mask, vocab, `ComputeEngine::note_temp_()`(`NN_PREC_TRACE=1` 时记录每次"物化临时量"的 `(rows, cols, 方向)` → 次数/字节)+ `ComputeEngine::dump_temp_stats()`(mem_probe 末尾按字节降序打印)。这份表把 GB 级 cast **直接落到具体形状**——判定"哪些 cast 还在、哪些已被变体消掉"的当前手段(如注意力反向物化的 `(32768,256)` W / grad_A,曾是单项最大头,补齐 matmul 段变体后该项消失,见 §12.11)。 -#### ③ 落地的三类带类型变体(当前) +#### ③ 落地的三类带类型变体(**V0 时代的按签名枚举**;其生成器代码现由 V1 复用) + +> 下表描述的三条生成路径**仍在**(同一批生成器、同一份索引数学),但变体已不再按签名枚举: +> 每条路径现在都被要求能发射 `EXPR_PREC_SIG_DISPATCH`(V1 双视图 + PC `prec`)形态,见 ⑤。 +> 表中"`(key,sig)` 匹配"的措辞已被 `find_prec_variant_` 的 `key`/`key#x`/`key#a` 取代。 | 项 | 机制 | 效果 | |---|---|---| -| **matmul 段** | `generate_glsl_matmul(name, spec, sig)`:A/B 槽 `float16_t` 声明 + `uvec2` 别名槽(4×half=8B,`unpackHalf2x16` 解 vec4)+ 全局加载处统一转 f32(共享 tile / VFMA 累加 / 尾链全 f32)+ 输出按 out 位 `float16_t()`;分块/双缓冲/barrier 节奏与 f32 逐字一致 | matmul 段变体可生成 | +| **matmul 段** | `generate_glsl_matmul(name, spec, sig)`:A/B 槽 `float16_t` 声明 + `uvec2` 别名槽(4×half=8B,`unpackHalf2x16` 解 vec4)+ 全局加载处统一转 f32(共享 tile / VFMA 累加 / 尾链全 f32)+ 输出按 out 位 `float16_t()`;分块/双缓冲/barrier 节奏与 f32 逐字一致(V1 下这两条加载路径**都发射**,运行时按 PC `mm_trans` 选) | matmul 段变体可生成 | | **归约 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 | +| **目标传递** | `GpuEngine::eval_expr_into` / `eval_expr_reduce` 带变体匹配(输出精度 = `dst.precision()` / `P`),f16 输出重贴 `GpuTensorF16`(归约向量形状按 raxis 取 `(rows,1)/(1,cols)`);两入口先查 `supports_expr_precision_variant` 再回退 cast | `compute_into` / `compute_reduce` 不再必然走边界 cast | -生成阶段 的基础结构数与变体条目数以构建输出 `[scan]`/`[gen]` 为准(变体含 scan 预测 + 回填清单,见 ⑤)。 +生成阶段的基础结构数与变体条目数**以构建输出 `[scan]` / `[gen]` 为准**(`[gen]` 打印"N 条融合表达式 + M 条变体")。 +**回填清单已不存在**(无 `tools/prec_backfill.txt`;M = V1 条数 + V2b 条数,全部由构建期一次算定)。 #### ④ 实测收益 @@ -635,10 +644,11 @@ matmul 段 / 归约 / 目标传递三类变体落地后的同窗交错实测(f **签名已从「身份」降级为「参数」**:融合 shader 的变体集合不再按签名枚举,而是对每个结构发一份**运行期精度分派** shader(键 `key#x`)——它声明每个输入/输出各 f32 + `float16_t` **双视图**(同一 `VkBuffer` 绑两次,只解引用与真实元素类型一致的那一个),加载/存储处按 push constant `uint prec`(bit i = 输入 i 为 f16,bit16 = 输出 f16)走 uniform 分支。索引数学与旧带类型变体**逐字相同** ⇒ 数值等价。 -- **无 miss**:V1 对每个结构无条件生成 ⇒ 任何签名都命中;`[prec][miss]` 与边界 cast 回退**不再是 f16 融合路径的一部分**。 -- **native16** 改由**结构谓词**判定生成(键 `key#a`,代入"全输入 f16 + 输出 f16"这一常量签名),运行时按真实签名确认。 +- **精度路径三档**(**不是"永不 miss"**):`key#a`(V2b native16)→ `key#x`(V1 运行期分派)→ **基类边界 cast**。V1 默认对每个结构无条件生成,但生成被跳过(`[skip] 分派变体…`)、`NN_SCAN_NO_DISPATCH=1`、或设备无 16bit 存储时会 miss 并落到边界 cast —— `[prec][miss]` 因此**仍是通用诊断**(实测 f16 融合路径 miss = 0)。 +- **native16** 改由**结构谓词**判定生成(键 `key#a`,代入"全输入 f16 + 输出 f16"这一常量签名)。⚠ 该谓词含**常量值**判据(`0 < |c| < 6e-8` 会 flush-to-0),而常量值**不进 key** ⇒ 运行时选 `#a` 前必须用**真实实例的 spec 重跑同一谓词**(`GpuEngine::find_prec_variant_`),否则"生成时那一个实例的常量"会被套用到同结构但常量不同的实例上(静默错值)。 - **回填清单已删除**:`tools/prec_backfill.txt` 连同 `NN_PREC_TRACE` 的"补变体"工作流一并移除;f16 扫描遍也因"对结构贡献为 0"而删除(实测:跳过它签名 66 → 0、结构恒 84)。 -- **实测**(GPT f16 训练,`NN_PREC_TRACE=1`):`[prec][miss]=0`、边界 cast 归因表为空、`[prec][alu-hit]` 6670。 +- **追踪日志口径**:变体命中只在确认命中后、且只由真正 dispatch 的入口打(`supports_expr_precision_variant` 只是查询)。修复前该函数也打日志 ⇒ 计数翻倍(实测 12040 行 = 6020 次真实分派 × 2;修复后同为 f16_precision_test 实测 **6022 行 / 24 条 distinct = 20 条 `#x` + 4 条 `#a`**)。 +- **实测**(GPT f16 训练,`NN_PREC_TRACE=1`):`[prec][miss]=0`、边界 cast 归因表为空、`[prec][alu-hit]` 6670 —— ⚠ **6670 是修复"双重打点"前的口径**,真实分派数为其一半。 --- diff --git a/docs/development/18-roadmap.md b/docs/development/18-roadmap.md index 7ab4c179..8fb5be85 100644 --- a/docs/development/18-roadmap.md +++ b/docs/development/18-roadmap.md @@ -40,14 +40,14 @@ | `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'` | +| 最大源文件 | `compute_vk_backend.hpp` **5246** 行、`expr_glsl_gen.hpp` **2746**、`compute_cpu_engine.hpp` **2353**、`compute_engine.hpp` **2087**、`src/text_train.cpp` **1746**、`gui.py` **1646**(非空 1456) | 逐文件 `(Get-Content $f).Count` | +| ctest | **21 个测试** = 19 个测试目标(`list(APPEND NN_TEST_TARGETS` 计数)+ `cnn_test_gpu`(= `cnn_test --gpu`)+ `fusion_custom_layer_example`(AOT 融合端到端) | `ctest --test-dir build -N` | | 引擎接口 | **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 条)。 +> `21` 是当前唯一正确计数。历史上仓库里同时存在"ctest 计数 19"与"ctest 20 比 20"两种表述(§3 工程化第 4 条)——那是**计数口径不同**(是否含 `cnn_test_gpu`、以及后来新增的两个目标),2026-10-01 的 A4/A5 两轮把计数推到 21 并统一到本文与 `AGENTS.md`。 ### 2.1 已经稳固的底座(本轮不再动) @@ -88,7 +88,7 @@ | 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) | +| E4 | 文档内 ctest 计数曾自相矛盾(19 vs 20) | `AGENTS.md` §12 已统一为实测 21(19 目标 + `cnn_test_gpu` + `fusion_custom_layer_example`) | 已修;口径差异来源见 §2 口径说明 | | 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** | @@ -141,8 +141,8 @@ P2 = 3–6 月的结构优化;P3 = 择机(trigger 未到就不排期)。 | 项目 | 目标 | 验收口径 | 成本 | 依赖 | |---|---|---|---|---| -| **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-1 CI 真跑构建 + 测试**(最高优先) | workflow 加 `-DNN_ENABLE_TESTS=ON`,并在 Linux 与 Windows 各跑一轮 `ctest`(失败时输出完整失败日志) | GitHub Actions 绿且日志出现 21 个用例;需 Vulkan 的用例按退出码 77 记 skip 而非失败 | 小(改一个 `.yml`,约 10 行) | 无。**先于其它一切**——没有真门禁,后面所有验收都无法自动防回退 | +| **P0-2 文档一致性回正** | ① ctest 计数统一为实测 21(19 目标 + `cnn_test_gpu` + `fusion_custom_layer_example`);② `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` 两者都没有,新开关命名不得与它们冲突 | @@ -238,19 +238,19 @@ ZiPT 恢复、LRLA、CUDA、分布式。 | 基线 | 口径 | 当前值 / 复现 | |---|---|---| | 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` | +| ctest | **实测 21**(19 目标 + `cnn_test_gpu` + `fusion_custom_layer_example`);需 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` 产物 | +| scan 产物 | `fused_registry.hpp`(**唯一产物**;`expr_specs.bin` 已随构建期单步删除) | `tools/scan_exprs.cpp` + `tools/fused_generate.hpp` 产物 | | 初值锚 | `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)。 +1. **CI 必须执行 ctest**:日志需出现 21 个用例;77 = skip 不算失败(P0-1)。 +2. **开发文档不得出现 ctest 计数的旧写法**:统一写实测 21(P0-2)。 3. **BatchNorm 行为门禁**:构造 batchnorm 规格必须有可观测结果(报错或正确层),不允许静默降级(P0-3)。 4. **危险精度组合门禁**:危险组合必须启动期失败(P1-3)。 5. **审计覆盖补强**:`bench/doc_align_audit.ps1` 的 [A] 需扩展到 markdown 链接目标(E8),或由 P0-2 直接修掉已知死链。 @@ -292,5 +292,5 @@ ZiPT 恢复、LRLA、CUDA、分布式。 | 日期 | 变更 | 依据 | |---|---|---| | 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 双重核验 | +| 2026-10-01 | 订正实测口径:ctest 19(后于同日的 A4/A5 两轮由 19 → 20 → 21);`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 33ab0ece..fc525df0 100644 --- a/docs/history.md +++ b/docs/history.md @@ -61,6 +61,27 @@ expr_opt / expr_registry / expr_spec)「历史状态类注释」摘录。整 - **CMake include 顺序有语义(踩坑)**:`nn_enable_gpu_fusion` 起初只把 `${NNF_OUT_DIR}` 加进目标,样例 GPU 初始化报 `matmul SPIR-V bytecode not embedded`——手写原语 shader 的嵌入头在 `${CMAKE_BINARY_DIR}/generated`;但该目录**同时含库自身的 `fused_registry.hpp`**。最终把两者都加、且 `${NNF_OUT_DIR}` **排在前面**(`__has_include` 取第一个匹配),否则使用者的自定义结构全部闭合世界报错。已写进 AGENTS §7。 - **已注册为常驻 ctest(round 17)**:样例的目标原先只由默认构建覆盖编译(运行时断言需手工跑),会静默腐化。现已 `add_test(fusion_custom_layer_example)` + `SKIP_RETURN_CODE 77`(无 Vulkan 设备时跳过)——它是**库外使用者形态的 AOT 融合端到端门禁**。ctest 总数 19 → **20**(18 个测试目标 + `cnn_test_gpu` + 本样例;`AGENTS.md` §12 与 `docs/development/12` 的当前数字已同步)。 +## AOT 融合分支代码审核与本轮修复(2026-10-01,round 20) + +- 类型:代码审核(简化 / 规范化 / 指针与内存 / 逻辑审查)+ 已修复缺陷 +- **范围**:`feat/fused-runtime-prec-dispatch` 全部 29 个 commit 相对 `main` 的源码、工具、CMake、测试与文档。 +- **本轮修掉的真缺陷(不只是风格)**: + 1. **native16 谓词与 key 的错配(静默错值)**:`expr_prec_sig_native16` 含**常量值**判据(`0 < |c| < 6e-8` 在 f16 下 flush-to-0),而步骤①让常量值**退出 key** ⇒ 生成期用"生成那一份时的常量"判定、运行期却按**位图**选 `#a` —— 同结构换一组常量就会被套上会 flush 的 native16 shader。修复:选 `#a` 前用**真实实例的 spec 重跑同一谓词**(`find_prec_variant_` 改为收 `const ExprSpec&`)。 + 2. **`[prec][alu-hit]` 先打后查 + 双重打点**:见上一条节(12040 → 6022)。 + 3. **push constant 固定头长度两侧各推一遍**:创建侧用 `fs.spec.fold/has_matmul/reduce_axis`、写入侧用 `is_fold/matmul_k/has_mm/raxis` —— 注释里记着"漂移 = 超 range 被驱动丢弃 = 假 PASS",本次把它降为**注册时算一次 + 存 map**(`fused_pc_base_fixed_`),写入侧只读。 + 4. **`in_rd` 用子串替换拼 f16 视图名**:找不到前缀时静默保留 f32 视图名 ⇒ 把 2B 缓冲当 float 读(编译期不报错)。改为显式拼 `b[...]` / `bh[...]`。 + 5. **`nn_enable_gpu_fusion` 的 include 顺序不成立**:普通 `target_include_directories` 对重复项保留**首次出现位置**,库内 GPU 循环先加了 `${CMAKE_BINARY_DIR}/generated` ⇒ 追加的 `${NNF_OUT_DIR}` 反而排在后面。改 **`BEFORE`**。 + 6. **收集器不继承目标的编译条件**:`NN_HAS_VULKAN`/私有 include/自定义宏缺失 ⇒ 使用者的调用点静默消失(而后只在 GPU 运行期闭合世界报错)。改为把目标的 `INCLUDE_DIRECTORIES`/`COMPILE_DEFINITIONS`/`COMPILE_OPTIONS` 用生成器表达式传给收集器。 + 7. **模型 pass 失败只 warning**:它贡献 25/84 条结构,静默降级 = 注册表不完整。改为**失败即构建失败**(与 dry-run 的 fail-fast 一致)。 + 8. **两条"永远通过"的测试**:`fold tri_skip 进 key` 用 Causal vs Plain 比 key(两者 body 本就不同,删掉该字段的 feed 也照样过)→ 改为**只翻该字段**;`expr_fused_key_test` 里与 `expr_matmul_test` 逐字重复的 mm_trans 位编码断言删掉一份。 + 9. **`read_spv` 不校验 `tellg()`**:失败返回 -1 → `size_t(-1)/4` 请求 ~4GB;截断读也被忽略。改为校验 `sz > 0 && sz % 4 == 0` 与 `read_pod_span` 返回值。 +- **删除的死物**:`prec_manifest` 形参(回填工作流残留)、`ExprRegistry::contains`(无调用点)、`fusion_anchor_touch`(**实测删除后库外样例的 2 条自定义结构仍被锚点登记**——registry 84 → 86 不变,说明该 TU 靠"包含头文件"即可)、`std::exit(1)`(改 `return false`,与同函数其余错误路径一致)、`scan_exprs` 里重复 4 次的 matmul trans 登记(转置不进 key 后 4 份折叠成 1 条)、AGENTS 里 4 处有争议的 `ctest 19/19`。 +- **简化/去重**:`glsl_vp_slot()` 取代 6 处逐字重复的"按视图序累计 vp 槽"(漂移 = 读写错位 push constant);`scan_register()` 取代 3 个 dsl 入口各写一遍的锚点+折叠+校验+登记;`is_fusable`/`fused_vec_width_for` 取代生成/发射/元数据三循环各写一遍的判据;`expr_variant_out_f16()` 取代 4 处"分派变体按运行期签名、其余按变体身份";f16 双 `float16_t()` 冗余转换删除;`EXPR_PREC_ALU_SUFFIX` 常量取代 3 处硬编码 `"#a"`;`tool_env_flag()` 取代两份 `_dupenv_s` 样板。 +- **检测口径收紧**:`expr_prec_sig_in_f16` 先与 `EXPR_PREC_SIG_INPUT_MASK` 相与(否则哨兵 `0x00020000` 的 bit17 会被读成"第 17 个输入是 f16",改 `EXPR_MAX_INPUTS` 即踩雷);分发查询改用 `reg.keys` 而非重建集合;`--list-backends` 排序(铁律 8)。 +- **文档纠偏**(本轮统一):AGENTS §7/§12、`docs/development/02/05/18`、`docs/usage/03` 中"运行期永不 miss""`[prec][miss]` 已删除""bin v9 变体段""`gen_fused` 产物""ctest 19""12040 次分派"等表述全部改写为当前事实;`docs/development/05` §12.7 改为**历史机制**(明确标注已删除),§12.10 ③ 标注为 V0 时代口径。 +- **验收(本机 Debug+Ninja+clang)**:build 零告警(`-Werror`);**ctest 21/21**;`NN_BIND_DEBUG=1 ctest` 全绿;**CPU 字节锚 `6f8849f14da23110` / GPU dev2 锚 `8ef51b2927253c50` 逐位不变**(GLSL 文本改动全部数值等价);库外样例端到端值不变(CPU f32 `0.6712035`、GPU f32 `0.6712036`、GPU f16 `0.6708984`、归一化 f16 `1.2246094`);`scan_exprs` 结构数 **84**(例 86)不变;`NN_PREC_TRACE` 分派日志 12040 → **6022**、distinct 仍 24(20 `#x` + 4 `#a`);`L2-VIOLATIONS: 0`、`doc_align_audit` A/D/E/F 可行动项 0。 +- **教训**:这一轮 9 条真缺陷里有 4 条的形态是同一个——**同一份知识被写在两个地方、靠注释维持同步**(谓词 vs key、创建侧 vs 写入侧 PC 长度、f16 视图名 vs 子串替换、目标编译条件 vs 收集器)。"能简化的简化"在本仓的直接收益就是**消掉这些同步点**;剩下两条(trace 先打后查、测试假通过)则是"**测量/断言必须先证明它能失败**"的同一个教训的延续(round 15/19 已各记一次)。 + ## 融合回归测试 `fused_gpu_test` 曾是孤儿(2026-10-01,round 18) - 类型:已修复缺陷(测试覆盖缺口 / 构建配置遗漏) @@ -77,7 +98,8 @@ expr_opt / expr_registry / expr_spec)「历史状态类注释」摘录。整 - **发现过程**:想量"V1 逐形态覆盖",用 `NN_PREC_TRACE=1` 跑 6 个 GPU 测试,得到 **0 条 `#x`**、只有 4 条 `#a`。做**对照实验**(跑已知必然走 V1 的库外样例——它含 rparam ⇒ native16 结构谓词不通过)——**样例也一行都不打** ⇒ 说明是**测量工具**瞎,不是"V1 没被用"。 - **根因**:`compute_gpu_engine.hpp::find_prec_variant_` 的 `[prec][alu-hit]` 打印**只写在 V2(`#a`)分支**(原 1391-1392 行),V1(`#x`)分支与 V0 分支都**静默返回**。 - **处置**:V1 分支补同款日志(V0 默认路径**仍不记**——f32 热路径逐调用打日志会淹没输出)。现规则:**变体选择事件(V1/V2)一律打日志**。 -- **修正后的实测(`f16_precision_test` 单测即可)**:12040 次分派、**24 条 distinct 结构 = 20 条 V1(`#x`)+ 4 条 V2(`#a`)**;V1 的 20 条**覆盖全部四个生成器族**:元素 9 / 归约 6 / matmul 4 / fold 1。该测试用 `CHECK_NEAR` 容差断言 ⇒ **V1 有真实的逐结构数值覆盖**,"V1 无覆盖"不成立。 +- **round 20 复核(本次代码审核)**:上面那次"补日志"本身留了两个尾巴,本次一并修掉——① **先打后查**:V1 分支在 `find_fused` 返回前就打印,`#x` 缺失时也报 `alu-hit`(与刚被修掉的"V1 静默"是同一类错误的反面)→ 改为**确认命中后才打**,miss 时打 `[prec][fallback]`;② **双重打点**:`supports_expr_precision_variant`(每个 op 的 NVI 前置查询)也走同一函数并打日志 ⇒ 计数翻倍——**12040 行其实是 6020 次真实分派 × 2**。改为只有真正 dispatch 的三个入口传 `log_hit=true`。 +- **修正后的实测(`f16_precision_test` 单测即可)**:**6022 条命中日志 / 24 条 distinct 结构 = 20 条 V1(`#x`)+ 4 条 V2(`#a`)**(修复双重打点后的真实分派数);V1 的 20 条**覆盖全部四个生成器族**:元素 9 / 归约 6 / matmul 4 / fold 1。该测试用 `CHECK_NEAR` 容差断言 ⇒ **V1 有真实的逐结构数值覆盖**,"V1 无覆盖"不成立。 - **顺带**:`scan_exprs` 的来源 dump 新增 `[scan][both]`(锚点∩dry-run 交集,原 84 条里 38 条落在这里)——否则"某结构由哪个生成器负责、运行时是否按 V1 分派"无法从 dump 回答(V1 覆盖的 20 条里 18 条在交集中)。 - **教训(本会话第二次)**:一个覆盖/一致性结论在写进文档前,**必须先有一个已知必然命中/必然不命中的对照样本**。上一次是"镜像对"(粗摘要相同但指令序列不同),这一次是"trace 对 V1 盲"。 @@ -2127,12 +2149,17 @@ GPU 后端 / GPU 引擎头文件「历史状态类注释」摘录。整改原则 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)、 +现已过期——`include/neuralnet.cpp/backend/compute_vk_backend.hpp` **5246** 行(13 写 ≈4700; +A1 段改动前为 5174)、`include/neuralnet.cpp/expr_glsl_gen.hpp` **2746** 行(写 2310; +A1 段改动前为 2424)、`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 修订统一。 +**ctest 计数(口径已统一,2026-10-01 A4/A5 两轮 + 本次审核)**:审查当时实测 **19** +(= 18 个测试目标 + `cnn_test_gpu`),此后 A4 注册 `fusion_custom_layer_example`、A5 接回 +`fused_gpu_test` → **20 → 21**(= **19** 个测试目标 + `cnn_test_gpu` + `fusion_custom_layer_example`)。 +`AGENTS.md` §12 与 `docs/development/18-roadmap.md` §2/§7 已统一写 **21**; +`docs/release-notes/v1.5.0.md` 与 `docs/development/17-unified-tensor-engine.md` 记的 **20/20** 是 +**2026-09-30 里程碑当时的计数**(历史值,不回改)。19 / 20 / 21 三个数并存过一阵,根因就是 +"是否含 `cnn_test_gpu`"与"后来新增了两个目标"两件互不相关的事叠在一起——**这不构成矛盾**, +但必须写明口径(本次补记)。 diff --git a/docs/usage/03-compute-engine-usage.md b/docs/usage/03-compute-engine-usage.md index 33cedf39..5ce35d6a 100644 --- a/docs/usage/03-compute-engine-usage.md +++ b/docs/usage/03-compute-engine-usage.md @@ -1570,7 +1570,7 @@ public: | 环境变量 | 作用 | |---|---| | `NN_BIND_DEBUG=1` | 未绑定输入进引擎也报错(抓库内 stamp 漏网);错误同步打 stderr,带 `file:line` 与形状。**建议作为门禁运行 `NN_BIND_DEBUG=1 ctest`** | -| `NN_PREC_TRACE=1` | 打印每个 `(结构 key, 精度签名)`;`[prec][alu-hit]` = 命中 native16 变体;`[into] branch=…` 打印 `eval_expr_into` 的分支。**`[prec][miss]` 与"回填清单"工作流已随"运行期精度分派"删除**——分派 shader(键 `key#x`)覆盖任意签名,f16 路径实测 `miss=0` | +| `NN_PREC_TRACE=1` | 打印每个 `(结构 key, 精度签名)`;`[prec][alu-hit]` = 命中**变体**(`key#x` 运行期精度分派 **或** `key#a` native16,key 本身即区分);`[prec][fallback]` = 该结构没有可用的 `#x`(回退基类边界 cast);`[into] branch=…` 打印 `eval_expr_into` 的分支。**`[prec][miss]` 保留为通用诊断**(V1 覆盖后 f16 融合路径实测 `miss=0`);"回填清单"工作流(`tools/prec_backfill.txt`)已随"运行期精度分派"删除。日志只由真正 dispatch 的入口、且在确认命中后打——`supports_expr_precision_variant` 是查询,不产生命中日志 | | `NN_F16_DEBUG=1` | f16 中间量数值扫描(`nn_dbg_scan`)、`accumulate` 巨值打印、`compute_into` 预绑定失败原因 | | `NN_MEM_STATS=1` | 训练中内存采样(配合 `pool_stats()`) | | `NN_VULKAN_DEVICE` | 强制指定 Vulkan 计算设备(索引 `"2"` 或名称子串 `"NVIDIA"`)。优先级:显式 API > 本变量 > 自动打分 | diff --git a/examples/fusion_custom_layer.cpp b/examples/fusion_custom_layer.cpp index c6df6a3e..3b96cc31 100644 --- a/examples/fusion_custom_layer.cpp +++ b/examples/fusion_custom_layer.cpp @@ -1,22 +1,15 @@ // ═══════════════════════════════════════════════════════════════════════════ // examples/fusion_custom_layer.cpp — 自研层的翻译单元(A2 验收用) // -// 存在的唯一目的:让 `nn_enable_gpu_fusion` 编收集器时**编译到本 TU**, -// 从而让 fusion_custom_layer.hpp 里的 `dsl::compute` 调用点在静态初始化期 -// 经 `FusedAnchor` 自登记——这正是"收集器必须看到使用者的调用点"的 -// 落地方式(收集器 = `tools/scan_exprs.cpp` 的库内收集 + 本应用的全部 TU)。 +// 存在的唯一目的:让 `nn_enable_gpu_fusion` 编收集器时**编译到一个包含本层 +// 的非 main TU**(收集器排除 MAIN 源),从而让 fusion_custom_layer.hpp 里的 +// `dsl::compute` 调用点在静态初始化期经 `FusedAnchor` 自登记——这正是 +// "收集器必须看到使用者的调用点"的落地方式(收集器 = tools/scan_exprs.cpp +// 的库内收集 + 本应用的全部非 main TU)。 +// +// 本 TU 不需要额外函数:包含头文件即会编译 `fused_custom_op` / +// `fused_custom_norm` 的函数体,其中的 `dsl::compute` 实例化会 odr-use +// `FusedAnchor::reg`(静态数据成员 → 静态初始化期登记)。 // ═══════════════════════════════════════════════════════════════════════════ #include "fusion_custom_layer.hpp" - -namespace nn_example -{ - -// 非 inline:odr-use 该函数 → 其函数体被发射 → 内部的 `dsl::compute` 实例化 -// → `FusedAnchor` 静态初始化登记结构。不调用它、也不改任何库内文件。 -void fusion_anchor_touch() -{ - (void)&fused_custom_op; -} - -} // namespace nn_example diff --git a/examples/fusion_custom_layer.hpp b/examples/fusion_custom_layer.hpp index 3582dabe..3ee65934 100644 --- a/examples/fusion_custom_layer.hpp +++ b/examples/fusion_custom_layer.hpp @@ -21,8 +21,9 @@ namespace nn_example // 一个刻意与库内任何结构都不同的融合表达式: // out = exp(|x| * alpha) / (sqrt(|y|) + alpha) // 两个运行时标量参数(alpha)→ 值不进 key,同结构共享 shader。 +// 入参只读:`dsl::leaf` 按值收 Tensor(句柄拷贝),表达式不写输入。 [[nodiscard]] inline nn::Result fused_custom_op( - nn::ComputeEngine& engine, nn::Tensor& x, nn::Tensor& y, float alpha, + nn::ComputeEngine& engine, const nn::Tensor& x, const nn::Tensor& y, float alpha, nn::Precision P = nn::Precision::F32) { return nn::dsl::compute(engine, @@ -39,7 +40,7 @@ namespace nn_example // 库外自定义层没有 dry-run/模型 pass,因此本函数是对 `nn_enable_gpu_fusion` // **可靠性**的直接检验:注册表若漏了运行期那一条,GPU 会闭合世界硬报错。 [[nodiscard]] inline nn::Result fused_custom_norm( - nn::ComputeEngine& engine, nn::Tensor& x, float inv_cols, float eps, + nn::ComputeEngine& engine, const nn::Tensor& x, float inv_cols, float eps, nn::Precision P = nn::Precision::F32) { return nn::dsl::compute(engine, diff --git a/include/neuralnet.cpp/backend/compute_vk_backend.hpp b/include/neuralnet.cpp/backend/compute_vk_backend.hpp index ebeb3409..03f69054 100644 --- a/include/neuralnet.cpp/backend/compute_vk_backend.hpp +++ b/include/neuralnet.cpp/backend/compute_vk_backend.hpp @@ -543,10 +543,13 @@ class GpuBackend std::unordered_map fused_rparam_counts_; // 每线程处理元素数(1=标量, 4=vec4;决定 dispatch 宽度缩放) std::unordered_map fused_vec_width_; - // 运行期精度分派变体(键后缀 "#x"):描述符 = 2N 输入视图 + 2 输出视图, - // 且 push constants 多一个 `prec` 槽(位于固定头末尾)。**创建侧与写入侧 - // 必须同源判定**(见 run_fused_gpu 的 pc_base 注释)。 - std::unordered_map fused_is_dispatch_; + // push constant **固定头**长度(uint 槽数,不含 vp/常量池):**注册时算一次** + // 并在此存下,run_fused_gpu 只读不算 —— 该值此前在创建侧与写入侧各推导一遍 + // (两侧入参来源不同:fs.spec.fold/has_matmul/reduce_axis 对 + // is_fold/matmul_k/has_mm/raxis),任一侧漂移就是"vkCmdPushConstants 超 + // range 被驱动丢弃 → shader 读到未定义残留 → 假 PASS"(见注册处注释)。 + // 运行期精度分派变体(键后缀 "#x")的额外 `prec` 槽也含在此值内。 + std::unordered_map fused_pc_base_fixed_; std::unique_ptr memory_pool_; // 第二阶段:瞬态/持久分池。batch 录制期(batch_mode_=true)创建的 @@ -1703,8 +1706,11 @@ class GpuBackend #ifdef NN_FUSED_REGISTRY_EMBEDDED // 16. 注册 AOT 融合 shader pipelines(构建期 scan_exprs 收集 + - // gen_fused 合成;每个条目:N 输入 + 1 输出 binding; - // push constants = count + cols + 常量池;key = expr_spec_key) + // fused_generate.hpp 合成)。条目形态: + // · V0 / V2(键 `key` / `key#a`):N 输入 + 1 输出 binding; + // · V1 运行期精度分派(键 `key#x`):2N + 2 binding(每个输入/输出 + // 各绑 f32 与 f16 两个视图);PC 固定头末尾多一个 `prec` 槽。 + // push constants = 固定头(count/cols/…)+ vp 槽 + 常量池 + rp 槽。 for (const auto& fs : nn::fused::kFusedShaders) { if (!fs.spirv || fs.spirv_words == 0) @@ -1712,15 +1718,15 @@ class GpuBackend // 运行期精度分派变体(键 "#x"):输入/输出各双视图 + PC 多一个 prec 槽 const bool is_disp = std::string_view(fs.key).ends_with(nn::EXPR_PREC_DISPATCH_SUFFIX); - // 精度变体(key 含 '#',Phase 2 in-kernel f16)需要 SSBO 16 位存储; - // 设备未启用 → 跳过注册(运行时按 (key,sig) 查不到 → 回退边界 cast)。 + // 带 '#' 的变体(f16 视图)需要 SSBO 16 位存储;设备未启用 → + // 跳过注册(运行时查不到 `#x`/`#a` → 基类 NVI 入口回退边界 cast)。 if (std::string_view(fs.key).find('#') != std::string_view::npos && !device_.has_16bit_storage()) continue; // ALU 变体(key 以 "#a" 结尾,native16 原生 f16 算术)额外需要 - // shaderFloat16 特性;未启用 → 跳过 → 运行时回退同签名的 f32 算术 - // 变体(其 pipeline 独立存在)或边界 cast——正确性不变。 - if (std::string_view(fs.key).ends_with("#a") && + // shaderFloat16 特性;未启用 → 跳过 → 运行时回退 `#x`(其 pipeline + // 独立存在)或边界 cast——正确性不变。 + if (std::string_view(fs.key).ends_with(nn::EXPR_PREC_ALU_SUFFIX) && !device_.has_shader_float16()) continue; const std::uint32_t n_in = @@ -1729,16 +1735,16 @@ class GpuBackend // 输出 f32/f16 视图 binding=2*n_in / 2*n_in+1。 const std::uint32_t num_bindings = is_disp ? (2u * n_in + 2u) : (n_in + 1u); // 输入 + 输出 - // 归约 kernel 的 push constants 多 uint rows + uint vector_out; - // 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 部分被驱动丢弃 → - // fold_k 读到未定义残留(残留随前序 op 漂移,表现为时对时错的 - // 假 PASS,"全绿"验不出);改动 PC 形态时创建侧(本处)与写入侧 - // (run_fused_gpu)必须同改); - // 另加 fs.view_param_count 个运行时视图参数槽(RowMod/RotateHalf) + // push constant **固定头**槽数(uint):逐形态与生成器的 PC 声明 + // 逐槽一致—— + // 逐元素 2(count, cols)/ +matmul 6(…, rows, mm_k, mm_batch, + // mm_trans)/ 归约 4(…, rows, vector_out)/ 归约+matmul 7 / + // fold 5(…, vector_out, fold_k)/ fold+mm 7(…, fold_k, mm_k, + // mm_batch)+ 分派变体 1 个 `prec`。 + // ⚠ 该值**只在此处计算一次**并存进 fused_pc_base_fixed_,写入侧 + // (run_fused_gpu) 只读:此前两侧各推导一遍,漂移会让 + // vkCmdPushConstants 超 range 而被驱动丢弃 → shader 读到未定义 + // 残留(随前序 op 漂移,表现为时对时错的假 PASS,"全绿"验不出)。 const std::uint32_t pc_base_fixed = (fs.spec.fold && fs.spec.fold->matmul) ? 7u // fold+mm 7 槽 : (fs.spec.fold) ? 5u @@ -1772,7 +1778,7 @@ class GpuBackend fused_view_param_counts_.emplace(fs.key, fs.view_param_count); fused_rparam_counts_.emplace(fs.key, fs.rparam_count); fused_vec_width_.emplace(fs.key, fs.vec_width); - fused_is_dispatch_.emplace(fs.key, is_disp); + fused_pc_base_fixed_.emplace(fs.key, pc_base_fixed); } } #endif @@ -3373,8 +3379,18 @@ class GpuBackend if (it == fused_pipelines_.end()) return std::unexpected(Error{"fused shader not registered: " + shader_name}); const VulkanPipeline& pipeline = it->second; - const bool is_disp = fused_is_dispatch_.count(shader_name) - ? fused_is_dispatch_.at(shader_name) : false; + // 运行期精度分派变体(键后缀 "#x"):描述符 = 2N 输入视图 + 2 输出视图, + // 且 PC 固定头末尾多一个 `prec` 槽。 + const bool is_disp = + std::string_view(shader_name).ends_with(nn::EXPR_PREC_DISPATCH_SUFFIX); + // PC 固定头槽数:**注册时算定,此处只读**(唯一来源 = + // register_fused_shaders 的 pc_base_fixed;本地重推会让两侧漂移 → + // push constant 超 range 被驱动丢弃 → 静默读未定义残留)。 + const auto pcbase_it = fused_pc_base_fixed_.find(shader_name); + if (pcbase_it == fused_pc_base_fixed_.end()) + return std::unexpected(Error{ + "run_fused_gpu: shader 元数据缺失(pc_base_fixed):" + shader_name}); + const std::uint32_t pc_base_fixed = pcbase_it->second; // 归约轴:-1=逐元素, 0=行归约, 1=列归约 const int raxis = fused_reduce_axis_.count(shader_name) ? fused_reduce_axis_.at(shader_name) : -1; @@ -3483,8 +3499,9 @@ class GpuBackend std::vector writes(n_bindings); if (is_disp) { - // 同一 VkBuffer 绑两次(f32 视图 / f16 视图);shader 按 PC `prec` - // 只解引用与真实元素类型一致的那一个 → 另一个从不访问(只读无别名风险)。 + // 同一 VkBuffer 绑两次(f32 视图 / f16 视图);输入对是 readonly、 + // 输出对是 writeonly,shader 每个元素只解引用 PC `prec` 选中的那一个 + // → 另一个从不访问(无别名风险)。 for (std::size_t i = 0; i < n_in; ++i) { buf_infos[i] = {inputs[i]->impl(), 0, VK_WHOLE_SIZE}; @@ -3538,18 +3555,12 @@ class GpuBackend // matmul+归约: count, cols, rows, vector_out, mm_k, mm_batch, mm_trans, // [vp0..], c0.. // - // ⚠ 固定头长度必须**逐形态**与生成器的 PC 声明一致:逐元素 2 / - // 逐元素+matmul 6 / 归约 4 / 归约+matmul 7 / fold 5 / fold+matmul 7。 + // ⚠ 固定头长度**唯一来源 = 注册时的 pc_base_fixed**(本处只读,见上)。 + // 逐形态:逐元素 2 / 逐元素+matmul 6 / 归约 4 / 归约+matmul 7 / + // fold 5 / fold+matmul 7(+ 分派变体 1 个 prec)。 // 错一个 uint 即常量池整体后移 → shader 从错位处读常量,GPU 上带 // 常量的归约静默错值而 CPU 正常(实测把 select(cond,1,0) 的常量 // 读成垃圾)。 - const std::uint32_t pc_base_fixed = - (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) ? 7u // …, mm_k, mm_batch, mm_trans - : (raxis >= 0) ? 4u // count, cols, rows, vector_out - : (has_mm) ? 6u // count, cols, rows, mm_k, mm_batch, mm_trans - : 2u; // count, cols // 分派变体(键 "#x")在固定头**末尾**多一个 uint prec:生成器同位置 // 声明(各生成器的 PC 块都在固定槽之后、vp 之前发 `uint prec;`)。 const std::uint32_t pc_uints = pc_base_fixed + (is_disp ? 1u : 0u); diff --git a/include/neuralnet.cpp/compute_gpu_engine.hpp b/include/neuralnet.cpp/compute_gpu_engine.hpp index 0f264057..59255aaa 100644 --- a/include/neuralnet.cpp/compute_gpu_engine.hpp +++ b/include/neuralnet.cpp/compute_gpu_engine.hpp @@ -1361,52 +1361,54 @@ class GpuEngine final : public ComputeEngine } #ifdef NN_FUSED_REGISTRY_EMBEDDED - // ── (结构, 精度签名) 变体选择:native16 ALU 变体优先,f32 算术变体回退 ── - // 查找顺序(Phase 3 原生 f16): - // 1. key#sig#a(native16 原生 f16 算术)—— 注册表与 pipeline **都**命中 - // 才选(设备无 shaderFloat16 时 pipeline 不创建 → 自动落 2); - // 2. key#sig(f16 存储 + f32 算术,原行为)—— pipeline 命中与否交由 - // 调用方原有判断(fs && has_fused_shader(fs->key)); - // 3. psig == 0 → 全 f32 结构键(调用方处理)。 - // 两变体并存 → 任何设备都有最优可用路径,无回归。 + // ── 变体选择:native16(`key#a`)优先,运行期精度分派(`key#x`)回退 ── + // · psig == 0 → 全 f32 结构键 `key`(V0)。 + // · 结构谓词 expr_prec_sig_native16 通过 → `key#a`(V2b 原生 f16 算术)。 + // **必须在运行期用真实 spec 重跑谓词**:常量值已不进 key(同结构可带 + // 不同常量),生成期是对"生成那一份时的常量"判定,运行期实例的常量 + // 可能不满足谓词(如 0 < |c| < 6e-8 的 eps 在 f16 下 flush-to-0)。 + // 生成期判定与运行期判定不一致 = 静默错值。 + // · 其余 → `key#x`(V1 运行期精度分派):一份 shader 覆盖任意签名。 + // ⚠ "任何非零签名都必定命中"只在**生成端产出了 `#x`** 且设备启用了 + // 16bit 存储时成立;生成被跳过(`[skip]`)/ `NN_SCAN_NO_DISPATCH=1` / + // 设备无 16bit storage 时仍会 miss → 基类 NVI 入口回退边界 cast。 + // registry 与 pipeline **都**命中才选(设备无 shaderFloat16 时 `#a` 的 + // pipeline 不创建 → 自动落 `#x`)。 + // + // 诊断(NN_PREC_TRACE=1):记**实际选中的变体**(V1/V2),不记 V0 默认 + // 路径(f32 热路径,逐调用打日志会淹没输出)。两个约束: + // ① 只在**确认命中**后打(此前 V1 分支先打后查,miss 也报 alu-hit); + // ② 只由真正 dispatch 的调用方传 log_hit=true —— + // `supports_expr_precision_variant` 只是查询(NVI 入口每次都调), + // 它打日志会让计数翻倍(实测 12040 行 = 6020 次真实分派 × 2)。 [[nodiscard]] const nn::fused::FusedShader* find_prec_variant_( - const std::string& key, nn::ExprPrecSig psig, - std::span inputs) const + const std::string& key, nn::ExprPrecSig psig, const nn::ExprSpec& spec, + bool log_hit = false) const { if (psig == 0) return nn::fused::find_fused(key); // V0:全 f32 结构键 - // V2(native16 原生 f16 算术):键 = key#a,**结构谓词判定**(生成期 - // 已知),运行时仅在"全输入 f16 且输出 f16"时才走它。 - const bool all_in_f16 = [&] { - for (std::size_t i = 0; i < inputs.size(); ++i) - if (!nn::expr_prec_sig_in_f16(psig, i)) return false; - return true; - }(); - if (all_in_f16 && nn::expr_prec_sig_out_f16(psig)) - { - const std::string akey = key + "#a"; - if (const auto* fs = nn::fused::find_fused(akey); - fs && backend_.has_fused_shader(akey)) - { - if (nn::dsl::env_flag("NN_PREC_TRACE")) - std::fprintf(stderr, "[prec][alu-hit] %s\n", akey.c_str()); + const bool trace = log_hit && nn::dsl::env_flag("NN_PREC_TRACE"); + const auto hit = [&](const std::string& k) -> const nn::fused::FusedShader* { + const auto* fs = nn::fused::find_fused(k); + if (!fs || !backend_.has_fused_shader(k)) + return nullptr; + if (trace) + std::fprintf(stderr, "[prec][alu-hit] %s\n", k.c_str()); + return fs; + }; + // V2b:native16 原生 f16 算术(运行期用真实 spec 复核结构谓词) + if (nn::expr_prec_sig_native16(spec, psig)) + if (const auto* fs = hit(key + nn::EXPR_PREC_ALU_SUFFIX)) return fs; - } - } - // V1(运行期精度分派):键 = key#x —— 一个 shader 覆盖任意签名, - // 因此**任何非零签名都必定命中**(不再有 miss / 边界 cast 回退)。 - // - // 诊断(NN_PREC_TRACE=1):**变体选择事件一律打日志**。此前只有上面的 - // V2 分支打,V1 分支静默 —— 后果是"某结构实际按 V1 走"在 trace 里完全 - // 不可见:实测库外样例(含 rparam ⇒ native16 结构谓词不通过 ⇒ 必然走 - // V1)在 trace 下**一行 `[prec][alu-hit]` 都不打**,足以误导出"V1 从未被 - // 使用、测试零覆盖"的结论(本分支加日志前作者就据此误判过一次)。 - // 只记变体选择(V1/V2),不记 V0 默认路径——后者是 f32 热路径,逐调用 - // 打日志会把输出淹没。 + // V1:运行期精度分派 const std::string xkey = key + nn::EXPR_PREC_DISPATCH_SUFFIX; - if (nn::dsl::env_flag("NN_PREC_TRACE")) - std::fprintf(stderr, "[prec][alu-hit] %s\n", xkey.c_str()); - return nn::fused::find_fused(xkey); + if (const auto* fs = hit(xkey)) + return fs; + if (trace) + std::fprintf(stderr, + "[prec][fallback] %s 未预生成 → 回退基类边界 cast\n", + xkey.c_str()); + return nullptr; } #endif // ══════════════════════════════════════════════════════════════════════ @@ -1424,19 +1426,18 @@ class GpuEngine final : public ComputeEngine { const std::string fkey = nn::expr_spec_key(raw_spec); #ifdef NN_FUSED_REGISTRY_EMBEDDED - // fold 精度变体(键 fkey#sig;native16 谓词排除 fold - // → 只有 f32 算术变体,find_prec_variant_ 的 #a 探测自然落空) + // fold 变体:native16 谓词排除 fold,find_prec_variant_ 的 `#a` + // 探测必然落空 → 命中 `#x`(或有真实位图的存储变体) const nn::ExprPrecSig fpsig = nn::expr_prec_sig_of(inputs, P); - const nn::fused::FusedShader* ffs = find_prec_variant_(fkey, fpsig, inputs); + const nn::fused::FusedShader* ffs = + find_prec_variant_(fkey, fpsig, raw_spec, /*log_hit=*/true); if (ffs && backend_.has_fused_shader(ffs->key)) { auto fi_r = fused_buffers_(inputs); if (!fi_r) return std::unexpected(fi_r.error()); std::vector& gpu_inputs = fi_r->bufs; const auto fvp = nn::expr_spec_runtime_view_params(raw_spec); - const bool fdisp = nn::expr_prec_sig_is_dispatch(ffs->prec_sig); - const bool fout_f16 = fdisp ? nn::expr_prec_sig_out_f16(fpsig) - : nn::expr_prec_sig_out_f16(ffs->prec_sig); + const bool fout_f16 = nn::expr_variant_out_f16(ffs->prec_sig, fpsig); auto out = backend_.run_fused_gpu( ffs->key, gpu_inputs, raw_spec.consts, rows, cols, /*vector_out=*/false, fvp, raw_spec.rparams, @@ -1465,13 +1466,13 @@ class GpuEngine final : public ComputeEngine // ── AOT 匹配:按规范结构 key 查预编译融合 shader ────────────── const std::string key = nn::expr_spec_key(spec); #ifdef NN_FUSED_REGISTRY_EMBEDDED - // 精度变体优先(Phase 2 in-kernel f16 + Phase 3 native16 ALU):按 - // **真实输入精度** + 目标输出精度取变体——native16(原生 f16 算术) - // 优先,f32 算术变体回退(find_prec_variant_)。命中即 shader 直接 - // 半精度读/写,无需边界 cast;未命中则退回全 f32 key —— 此时输入必须 - // 已全 f32(由基类 NVI 入口 cast)。 + // 变体优先(native16 `#a` → 运行期精度分派 `#x`):按**真实输入精度** + // + 目标输出精度取变体(find_prec_variant_)。命中即 shader 直接半精度 + // 读/写,无需边界 cast;未命中则退回全 f32 key —— 此时输入必须已全 f32 + // (由基类 NVI 入口 cast)。 const nn::ExprPrecSig psig = nn::expr_prec_sig_of(inputs, P); - const nn::fused::FusedShader* fs = find_prec_variant_(key, psig, inputs); + const nn::fused::FusedShader* fs = + find_prec_variant_(key, psig, spec, /*log_hit=*/true); if (fs && backend_.has_fused_shader(fs->key)) { // 命中:收集 GPU 输入(同一 buffer 可重复绑定,如 RoPE 的 q×2) @@ -1487,9 +1488,7 @@ class GpuEngine final : public ComputeEngine // batch32 训练 loss=NaN)。缓冲布局由 out_f16 决定,返回的 GpuTensor // 只是占位标签,这里按实际精度重贴 GpuTensorF16。 // 输出存储精度:分派变体按**运行期真实签名**(bit16),其余按变体身份 - const bool disp = nn::expr_prec_sig_is_dispatch(fs->prec_sig); - const bool out_f16 = disp ? nn::expr_prec_sig_out_f16(psig) - : nn::expr_prec_sig_out_f16(fs->prec_sig); + const bool out_f16 = nn::expr_variant_out_f16(fs->prec_sig, psig); auto out = backend_.run_fused_gpu( fs->key, gpu_inputs, spec.consts, rows, cols, /*vector_out=*/false, vp, spec.rparams, /*output_override=*/nullptr, @@ -1540,16 +1539,18 @@ class GpuEngine final : public ComputeEngine { #ifdef NN_FUSED_REGISTRY_EMBEDDED // psig == 0(全 f32):基类快路径直通,无需变体。 - // psig != 0:选路与 eval_expr_impl 完全同源(V2 native16 优先 → - // V1 运行期分派)——V1 对每个结构无条件生成,故**任何签名都命中**, - // 基类不再有"边界 cast 回退"这条路径。 + // psig != 0:选路与 eval_expr_impl 完全同源(`#a` native16 优先 → + // `#x` 运行期分派)。**本函数只是查询**(NVI 入口每次都调)→ 传 + // log_hit=false,否则 NN_PREC_TRACE 的命中计数会翻倍。 const nn::ExprPrecSig psig = nn::expr_prec_sig_of(inputs, P); if (psig == 0) return false; - const std::string key = raw_spec.fold - ? nn::expr_spec_key(raw_spec) - : nn::expr_spec_key(nn::canonicalize_expr_spec(raw_spec)); - const auto* fs = find_prec_variant_(key, psig, inputs); + // 查表 key 与 native16 谓词都必须在 **canonical** 形态上求值:登记端 + // 存的是 canonical spec,生成期的谓词判定也基于它(如常量折叠会改 + // 常量值)。对 raw_spec 求值可能与生成端不一致 → 静默选错变体。 + const ExprSpec canon = nn::canonicalize_expr_spec(raw_spec); + const std::string key = nn::expr_spec_key(canon); + const auto* fs = find_prec_variant_(key, psig, canon); return fs != nullptr && backend_.has_fused_shader(fs->key); #else (void)raw_spec; (void)inputs; (void)P; @@ -1594,20 +1595,18 @@ class GpuEngine final : public ComputeEngine const std::string key = nn::expr_spec_key(spec); const nn::ExprPrecSig psig = nn::expr_prec_sig_of(inputs, P); #ifdef NN_FUSED_REGISTRY_EMBEDDED - // 精度变体优先(Phase 2 in-kernel f16 + Phase 3 native16):与 eval_expr - // 同款 (key,sig) 匹配;native16 ALU 变体优先(find_prec_variant_)—— - // 归约形态 native16 谓词恒 false,自动落 f32 算术变体 + // 变体优先(`#a` native16 → `#x` 运行期分派):与 eval_expr 同款 + // (key,sig) 匹配;归约形态的 native16 谓词恒 false → 自动落 `#x` const nn::fused::FusedShader* fs = (psig != 0) - ? find_prec_variant_(key, psig, inputs) : nn::fused::find_fused(key); + ? find_prec_variant_(key, psig, spec, /*log_hit=*/true) + : nn::fused::find_fused(key); if (fs && backend_.has_fused_shader(fs->key)) { auto fi_r = fused_buffers_(inputs); if (!fi_r) return std::unexpected(fi_r.error()); std::vector& gpu_inputs = fi_r->bufs; // owners 随 fi_r 存活到本作用域末 const auto vp = nn::expr_spec_runtime_view_params(spec); - const bool rdisp = nn::expr_prec_sig_is_dispatch(fs->prec_sig); - const bool out_f16 = rdisp ? nn::expr_prec_sig_out_f16(psig) - : nn::expr_prec_sig_out_f16(fs->prec_sig); + const bool out_f16 = nn::expr_variant_out_f16(fs->prec_sig, psig); auto out = backend_.run_fused_gpu( fs->key, gpu_inputs, spec.consts, rows, cols, /*vector_out=*/true, vp, spec.rparams, @@ -1660,13 +1659,13 @@ class GpuEngine final : public ComputeEngine return std::unexpected(Error{ "eval_expr_into: 仅支持逐元素表达式(无归约)"}); const std::string key = nn::expr_spec_key(spec); - // 精度变体优先(Phase 2 in-kernel f16):目标精度 = dst.precision() + // 变体优先(`#a` native16 → `#x` 运行期分派):目标精度 = dst.precision() const nn::ExprPrecSig psig = nn::expr_prec_sig_of(inputs, dst.precision()); #ifdef NN_FUSED_REGISTRY_EMBEDDED - // native16 ALU 变体优先,f32 算术变体回退(find_prec_variant_) const nn::fused::FusedShader* fs = (psig != 0) - ? find_prec_variant_(key, psig, inputs) : nn::fused::find_fused(key); + ? find_prec_variant_(key, psig, spec, /*log_hit=*/true) + : nn::fused::find_fused(key); if (fs && backend_.has_fused_shader(fs->key)) { auto fi_r = fused_buffers_(inputs); diff --git a/include/neuralnet.cpp/expr_dsl.hpp b/include/neuralnet.cpp/expr_dsl.hpp index a7c080af..3828e364 100644 --- a/include/neuralnet.cpp/expr_dsl.hpp +++ b/include/neuralnet.cpp/expr_dsl.hpp @@ -140,8 +140,9 @@ struct SpecBuilder return expr::input(static_cast(inputs.size() - 1)); } // 分组归约视图输入:输出 (G,N)、输入 (G*R,N),out[r][c] = Σ/max over - // t(row,col)`(见 + // ExprViewKind::GroupedReduce* 注释)。 ExprOperand add_input_grouped_reduce(const Tensor& t, std::uint32_t R, bool is_max) { ExprView v; @@ -1364,9 +1365,9 @@ template // · 锚点覆盖**编译进来的每个调用点**——调用点在函数体里,函数被编译即实例化, // 与运行期是否走到该分支无关(因此这是"编译期可达"而非"运行期可达")。 // -// ⚠ 只登记**结构**(sig=0):精度签名取决于运行期张量精度(同一表达式里哪些 -// 输入是 f16),类型层面推不出来 → f16 带类型变体仍由 profile_f32/profile_f16 -// 两遍 dry-run 负责(自登记无法取代它)。 +// ⚠ 只登记**结构**:精度签名已不是构建期集合(生成阶段对每个结构发一份 +// 运行期精度分派 shader,键 `key#x`,运行期按真实精度填 PC `prec`)—— +// 故既不存在 `sig` 形参,也不需要 profile_f32/profile_f16 两遍扫描。 // // 结构非法(裸视图作根 → 空指令表等)在构建期即 `_Exit(3)`,与 dry-run 同一闸门。 // @@ -1381,6 +1382,9 @@ template return reg; } +// 锚点:每个被扫描的表达式**类型**实例化一个 `FusedAnchor`,其静态 +// 初始化期把"从类型默认构造的符号实例"折叠出的结构登记进**锚点注册表** +// (scan_exprs 结尾与 dry-run 注册表合并,见下)。 template struct FusedAnchor { @@ -1394,7 +1398,28 @@ struct FusedAnchor return 0; }(); }; + +// ── 构建期扫描的单点登记(三个 dsl 入口共用)──────────────────────────── +// 折叠表达式 → 校验 → 登记进全局注册表,并返回折叠出的 spec(compute_reduce +// 还要用它判定归约轴)。**只登记结构**:精度签名已不是构建期集合 +// (生成阶段对每个结构发运行期精度分派 shader `key#x`)。 +// 锚点(FusedAnchor,按**类型**登记)由本函数统一 odr-use,保证三个入口的 +// 锚点覆盖行为一致(此前三处各写一遍,漏一处 = 该入口的调用点失去编译期可达 +// 覆盖,且只在 GPU 闭合世界报错时才暴露)。 +template +[[nodiscard]] inline ExprSpec scan_register(const Expr& e) +{ +#if !defined(NN_SCAN_NO_ANCHOR) + (void)FusedAnchor::reg; #endif + auto [spec, inputs] = to_expr_spec(e); + if (auto v = validate_expr_spec(spec, inputs.size()); !v) + scan_reject(v.error()); + fused::global_registry().add(spec); + return spec; +} + +#endif // NN_EXPR_SCAN // ══════════════════════════════════════════════════════════════════════════ // 统一入口:engine.compute(expr, rows, cols[, P]) @@ -1417,17 +1442,9 @@ template // 构建期扫描模式:折叠内联表达式的**结构**并登记进全局注册表, // 返回占位张量让 dry-run 流程继续(scan 只关心表达式集合,不真算)。 // 表达式文本仍只出现在 Layer;这里登记的是派生物 ExprSpec。 - // 自登记锚点:按**类型**登记结构(覆盖 dry-run 没跑到的调用点) -#if !defined(NN_SCAN_NO_ANCHOR) - (void)FusedAnchor::reg; -#endif + // 自登记锚点(按类型登记结构)在 scan_register 内统一处理。 (void)eng; - auto [spec, inputs] = to_expr_spec(e); - if (auto v = validate_expr_spec(spec, inputs.size()); !v) - scan_reject(v.error()); - // 构建期只登记**结构**:精度签名已不是构建期集合(生成阶段对每个结构 - // 发运行期精度分派 shader,运行期按真实精度填 PC `prec`)。 - fused::global_registry().add(spec); + (void)scan_register(e); return eng.adopt(P == Precision::F16 ? TensorAccess::cpu(rows, cols) : TensorAccess::cpu(rows, cols)); #else @@ -1522,18 +1539,11 @@ template [[nodiscard]] Result compute_into(ComputeEngine& eng, const E& e, Tensor& dst) { #ifdef NN_EXPR_SCAN - // 构建期扫描:与 compute() 一样只登记结构(不真算、不关心 dst 的值) - // 自登记锚点(按类型登记结构)见 compute() 同名说明 -#if !defined(NN_SCAN_NO_ANCHOR) - (void)FusedAnchor::reg; -#endif + // 构建期扫描:与 compute() 一样只登记结构(不真算、不关心 dst 的值), + // 锚点自登记在 scan_register 内统一处理 (void)eng; - auto [spec, inputs] = to_expr_spec(e); - if (auto v = validate_expr_spec(spec, inputs.size()); !v) - scan_reject(v.error()); - // 只登记结构(精度签名不是构建期集合,见 expr_dsl.hpp 头注释) + (void)scan_register(e); (void)dst; - fused::global_registry().add(spec); return {}; #else if (eng.device() == Device::CPU) @@ -1597,20 +1607,13 @@ template Precision P) // 默认实参在 compute_engine.hpp 前置声明处 { #ifdef NN_EXPR_SCAN - // 构建期扫描:同 compute(),登记结构(归约轴由 gen_fused 判定)。 + // 构建期扫描:同 compute(),登记结构(归约轴供下面取占位张量形状)。 // 占位张量按归约轴取向量形状 (rows,1)/(1,cols),使 Layer 后续 // add_inplace 等形状相关操作在 dry-run 中不因形状失配而中断。 - // 自登记锚点(按类型登记结构)见 compute() 同名说明 -#if !defined(NN_SCAN_NO_ANCHOR) - (void)FusedAnchor::reg; -#endif + // 锚点自登记在 scan_register 内统一处理。 (void)eng; - auto [spec, inputs] = to_expr_spec(e); - if (auto v = validate_expr_spec(spec, inputs.size()); !v) - scan_reject(v.error()); + const ExprSpec spec = scan_register(e); const int raxis = expr_spec_reduce_axis(spec); - // 只登记结构;占位张量仍按目标精度返回,供下游 dry-run 继续(形状相关) - fused::global_registry().add(spec); if (P == Precision::F16) return eng.adopt((raxis == 0) ? TensorAccess::cpu(rows, 1) : (raxis == 1) ? TensorAccess::cpu(1, cols) diff --git a/include/neuralnet.cpp/expr_glsl_gen.hpp b/include/neuralnet.cpp/expr_glsl_gen.hpp index 3128dc46..efb1a36e 100644 --- a/include/neuralnet.cpp/expr_glsl_gen.hpp +++ b/include/neuralnet.cpp/expr_glsl_gen.hpp @@ -172,12 +172,24 @@ inline void glsl_view_read(std::ostringstream& os, // 调用方(emit_scalar_chain / vec4 读取)负责。 // f16_native 时扩展提供了 f16 算术 → 不转(保持 float16_t 类型链)。 const bool f16_buf = expr_prec_sig_in_f16(sig, buf_id) && !f16_native; + const ExprViewKind vkind = static_cast(v.kind); if (!expr_prec_sig_is_dispatch(sig)) { os << glsl_view_read_(v, buf_id, buf, f16_buf, idx_var, row_var, col_var, vp_slot, sig); return; } + if (vkind == ExprViewKind::GroupedReduceSum || vkind == ExprViewKind::GroupedReduceMax) + { + // 分组归约在分派模式下**自带精度选择**:读取点是 `gr_r(row,col)`, + // 该辅助函数内部已按 PC `prec` 选视图并返回 float。再套外层三元只会 + // 产生 `prec ? … : …` 里嵌同样 `prec ? … : …` 的重复表达式(语义相同、 + // 文本翻倍)。RowGather **不能**走这条快路:它自带分派的只有**标签槽**, + // 取值仍需外层三元在 `b` / `bh` 之间选择。 + os << glsl_view_read_(v, buf_id, buf, false, idx_var, row_var, col_var, + vp_slot, sig); + return; + } // ── 运行期精度分派(V1):同一索引同时作两种视图解引用,uniform 选择 ── // f16 分支取 float(...)(行内 rw 已转的表达式再套一层 float 是无害恒等)。 const std::string f32e = glsl_view_read_(v, buf_id, buf, false, idx_var, @@ -189,14 +201,32 @@ inline void glsl_view_read(std::ostringstream& os, << ") : " << f32e << ")"; } +// ── 视图的运行时参数(vp)槽位 ────────────────────────────────────────── +// 槽位 = 排在该视图之前的 runtime-param 视图槽数之和(RowAccess 占 2 槽)。 +// **唯一实现**:六个生成路径此前各写一遍同一段循环,任一处漂移就会读写错位的 +// push constant 字(后端 `pc_base` 的注释记录过这类"静默错值"事故)。 +[[nodiscard]] inline std::uint32_t glsl_vp_slot(const ExprSpec& spec, + std::size_t view_idx) +{ + std::uint32_t vp = 0; + for (std::size_t j = 0; j < view_idx; ++j) + { + const auto k = static_cast(spec.views[j].kind); + if (expr_view_has_runtime_param(k)) + vp += expr_view_runtime_param_slots(k); + } + return vp; +} + // ── 分组归约视图的读取辅助函数发射(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 生成路径;其它生成器不需要调用本函数。 +// 位置:shader 顶部(PC / 缓冲声明之后、main 之前)。 +// **每个可能发射 `gr_r` 的生成器都必须调用本函数**:glsl_view_read 对 +// GroupedReduce 视图无条件发 `gr_r(...)`,目前 elementwise / matmul / +// 归约三处调用它。无该视图时本函数不产出任何文本(调用零成本)。 inline void glsl_emit_grouped_reduce_helpers(std::ostringstream& os, const ExprSpec& spec, ExprPrecSig sig = 0, @@ -209,13 +239,8 @@ inline void glsl_emit_grouped_reduce_helpers(std::ostringstream& os, 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)); + // vp 槽 = 按视图序累计(与 glsl_view_read 调用点同源,见 glsl_vp_slot) + const std::uint32_t vp = glsl_vp_slot(spec, i); const bool is_max = (k == ExprViewKind::GroupedReduceMax); const bool disp = expr_prec_sig_is_dispatch(sig); const bool f16_buf = expr_prec_sig_in_f16(sig, i) && !f16_native; @@ -465,6 +490,10 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) // stage 交替:tile t 用 t&1。 L << "shared vec4 Ash[2][" << BK << "][" << B / 4u << "];\n"; L << "shared vec4 Bsh[2][" << BK << "][" << B / 4u << "];\n\n"; + // 分组归约视图的读取辅助函数(本生成器的尾链也调用 glsl_view_read): + // 与 generate_glsl_reduce 同理——不发射会让含该视图的 spec 生成未声明函数。 + glsl_emit_grouped_reduce_helpers(L, spec, sig, /*f16_native=*/false); + // transA/transB 感知的全局内存加载表达式(batch:A/B 按 batch 垂直 // 切分,batch*m_per 为 A 行偏移 / batch*mm_k 为 B k 偏移;row/col 为 @@ -543,12 +572,7 @@ inline bool glsl_vec4_eligible(const ExprSpec& spec) { if (i == a_slot || i == b_slot) continue; - 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 std::uint32_t vp = glsl_vp_slot(spec, i); L << " const float v" << i << " = "; const bool tail_f16 = sign_f16 && expr_prec_sig_in_f16(sig, i); if (tail_f16) L << "float("; // f16 读 → 显式转 f32(算术仍在 f32) @@ -919,15 +943,6 @@ inline std::string generate_glsl_fold(const std::string& name, const ExprSpec& s << std::hex << bits << std::dec << "u);\n"; } - // 视图运行时参数槽(槽位按视图序数累计:排在前面的 runtime-param 视图槽数之和) - const auto vp_of = [&](std::size_t k) -> std::uint32_t - { - std::uint32_t vp = 0; - for (std::size_t j = 0; j < k; ++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)); - return vp; - }; // 操作数 → GLSL 片段;kbv = 块内下标变量(标量上下文传 "0u"——类别分析 // 保证标量指令无元素源,该分支不可达;归约/元素上下文传 "kb") const auto operand = [&](const ExprOperand& op, const std::string& kbv) -> std::string @@ -953,7 +968,8 @@ inline std::string generate_glsl_fold(const std::string& name, const ExprSpec& s const bool f16_in = sig != 0 && expr_prec_sig_in_f16(sig, op.idx); if (f16_in) os << "float("; glsl_view_read(os, spec.views[op.idx], op.idx, - "(row * cols + " + gk + ")", "row", gk, vp_of(op.idx), sig); + "(row * cols + " + gk + ")", "row", gk, + glsl_vp_slot(spec, op.idx), sig); if (f16_in) os << ")"; return os.str(); } @@ -1115,22 +1131,20 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec { return (sig != 0 && expr_prec_sig_in_f16(sig, i)) ? "float16_t" : "float"; }; - // 直接下标读(mm 段 A/B、vecacc B——非视图,平铺索引):f16 输入包 float() - const auto in_rd = [&](std::size_t i, const std::string& e) -> std::string - { + // 直接下标读(mm 段 A/B、vecacc B——非视图,平铺索引):f16 输入包 float()。 + // 入参是**纯下标表达式**(如 "(batch*mm_k + d)*cols + col"),缓冲名由本 + // 函数按分派位拼出 f32 / f16 两个视图——此前实现是"在下标串里做子串 + // 替换找 `b[` 换成 `bh[`",找不到时静默保留 f32 视图名、把 2 字节 + // 缓冲按 float 读(编译期不报错,运行期错值)。 + const auto in_rd = [&](std::size_t i, const std::string& idx) -> std::string + { + const std::string an = "b" + std::to_string(i); + const std::string f32e = an + "[" + idx + "]"; if (disp) - { - // e 是完整表达式(含 f32 缓冲名),分派分支需换成同名 h 视图 - const std::string fn = "b" + std::to_string(i) + "["; - const std::string hn = "b" + std::to_string(i) + "h["; - std::string e16 = e; - const std::size_t p = e16.find(fn); - if (p != std::string::npos) - e16.replace(p, fn.size(), hn); return "((prec >> " + std::to_string(i) + "u & 1u) != 0u ? float(" - + e16 + ") : " + e + ")"; - } - return (sig != 0 && expr_prec_sig_in_f16(sig, i)) ? ("float(" + e + ")") : e; + + an + "h[" + idx + "]) : " + f32e + ")"; + return (sig != 0 && expr_prec_sig_in_f16(sig, i)) ? ("float(" + f32e + ")") + : f32e; }; // 输出列数 **运行时读 PC 的 vector_out 槽**(形状无关:同 key 服务任意 // d_k——veclen 若进 key 则每个 dk 一个 shader、闭合世界必然缺登记) @@ -1222,12 +1236,10 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec L << " for (uint d = tid; d < mm_k; d += 256u) {\n"; if (f.matmul->transA) L << " Ash[d] = " << in_rd(f.matmul->a_input, - "b" + std::to_string(static_cast(f.matmul->a_input)) + - "[(batch * mm_k + d) * m_per + row_in]") << ";\n"; + "(batch * mm_k + d) * m_per + row_in") << ";\n"; else L << " Ash[d] = " << in_rd(f.matmul->a_input, - "b" + std::to_string(static_cast(f.matmul->a_input)) + - "[(batch * m_per + row_in) * mm_k + d]") << ";\n"; + "(batch * m_per + row_in) * mm_k + d") << ";\n"; L << " }\n"; L << " barrier(); // Ash 就绪(每行预载,块循环前一次)\n"; } @@ -1248,16 +1260,6 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec // ── 操作数 → GLSL 片段。ctx:'E'=链(j 上下文) 'R'=归约源(内层 j) // 'S'=标量行(无 j) 'F'=finalize(dd 上下文)。── - const auto vp_of = [&](std::size_t k) -> std::uint32_t - { - std::uint32_t vp = 0; - for (std::size_t j = 0; j < k; ++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)); - return vp; - }; // (mm 段输出在块首以 smm[] 协作计算——链内不做现场串行 Σ_d, // 现场串行是吞吐回退主因,见块循环头) const auto operand = [&](const ExprOperand& op, char ctx, @@ -1300,7 +1302,8 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec const bool f16_in = sig != 0 && expr_prec_sig_in_f16(sig, op.idx); if (f16_in) os << "float("; glsl_view_read(os, spec.views[op.idx], op.idx, - "(row * cols + " + gk + ")", "row", gk, vp_of(op.idx), sig); + "(row * cols + " + gk + ")", "row", gk, + glsl_vp_slot(spec, op.idx), sig); if (f16_in) os << ")"; return os.str(); } @@ -1384,12 +1387,10 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec L << " for (uint d = grp; d < mm_k; d += " << NG << "u) {\n"; if (m.transB) L << " part += Ash[d] * " << in_rd(m.b_input, - "b" + std::to_string(static_cast(m.b_input)) + - "[(batch * cols + (k0 + jm)) * mm_k + d]") << ";\n"; + "(batch * cols + (k0 + jm)) * mm_k + d") << ";\n"; else L << " part += Ash[d] * " << in_rd(m.b_input, - "b" + std::to_string(static_cast(m.b_input)) + - "[(batch * mm_k + d) * cols + (k0 + jm)]") << ";\n"; + "(batch * mm_k + d) * cols + (k0 + jm)") << ";\n"; L << " }\n"; L << " }\n"; L << " spart[jm][grp] = part;\n"; @@ -1538,8 +1539,7 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec L << " for (; j + 4u <= valid; j += 4u) {\n"; for (int t = 0; t < 4; ++t) L << " const float v" << t << " = " << in_rd(va.b_input, - "b" + std::to_string(static_cast(va.b_input)) + - "[(jr0 + j + " + std::to_string(t) + "u) * vector_out + dd]") << ";\n"; + "(jr0 + j + " + std::to_string(t) + "u) * vector_out + dd") << ";\n"; for (int t = 0; t < 4; ++t) L << " o[(dd - tid) / 256u] += sreg[" << eidx[va.weight_reg] << "][j + " << t << "u] * v" << t << ";\n"; @@ -1547,8 +1547,7 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec L << " for (; j < valid; ++j)\n"; L << " o[(dd - tid) / 256u] += sreg[" << eidx[va.weight_reg] << "][j] * " << in_rd(va.b_input, - "b" + std::to_string(static_cast(va.b_input)) + - "[(jr0 + j) * vector_out + dd]") << ";\n"; + "(jr0 + j) * vector_out + dd") << ";\n"; L << " }\n"; } L << " }\n"; // 块循环尾 @@ -1580,25 +1579,29 @@ inline std::string generate_glsl_fold_v2(const std::string& name, const ExprSpec return L.str(); } -// ── 带类型变体(Phase 2 in-kernel f16)─────────────────────────────────── -// sig 标注每个输入/输出的存储精度(见 expr_spec.hpp 的 ExprPrecSig)。 -// sig == 0 → 全 f32:生成的就是 f32 变体的 GLSL。 -// sig != 0 → 该精度上有 f16 的元素:缓冲区声明为 float16_t(GL_EXT_shader_16bit_storage -// + 显式 float(...)/float16_t(...) 转换),算术仍在 f32(§7.2:f32 参考 + -// 输出舍入)→ 读一次 2B、写一次 2B,无需"边界 cast"为每个算子 -// 物化 f32 副本(边界 cast 路径会使 transient 膨胀 2.4×)。 -// 带类型形态覆盖:纯逐元素、前置 matmul 段、fold 段(v1/v2)、归约 kernel +// ── 每个结构的三份产物(sig 语义见 expr_spec.hpp)──────────────────────── +// sig == 0 → **V0** 全 f32(键 = 结构 key)。 +// sig == EXPR_PREC_SIG_DISPATCH → **V1 运行期精度分派**(键 = key#x):每个 +// 输入/输出声明 f32 + float16_t **双视图**,加载/存储按 PC `prec` 选一。 +// 一份 shader 覆盖任意 (输入位图, 输出) 签名——精度是**参数**不是身份, +// 故构建期无需枚举签名。算术仍在 f32。 +// sig != 0(真实位图)+ native16=false → **V2 存储变体**:该精度上是 f16 的 +// 槽声明为 float16_t(GL_EXT_shader_16bit_storage + 显式 +// float(...)/float16_t(...) 转换),算术仍在 f32(§7.2:f32 参考 + +// 输出舍入)→ 读一次 2B、写一次 2B,无需"边界 cast"为每个算子物化 +// f32 副本(边界 cast 路径会使 transient 膨胀 2.4×)。 +// native16=true → **V2b 原生 f16 算术**(键 = key#a;仅当谓词 +// expr_prec_sig_native16 通过才生成):全链 float16_t 计算 +// (GL_EXT_shader_explicit_arithmetic_types_float16),无 +// load→float()→f32 算术→float16_t() 的逐元素往返。后端按设备 +// shaderFloat16 能力选 pipeline;谓词不通过 → 本函数返回空串,上层不产出 +// `#a` 条目,运行时落 `#x`(或设备的边界 cast)。 +// 形态覆盖:纯逐元素、前置 matmul 段、fold 段(v1/v2)、归约 kernel // (generate_glsl_reduce,读写点按 sig 转换)。仍返回空串的情形:归约混合轴 -// (== -2)、归约槽过多(>8)→ 上层跳过该变体,运行时回退边界 cast, -// 正确性不受影响。 +// (== -2)、归约槽过多(>8)等 → 上层跳过该变体,运行时回退,正确性不受影响。 // -// native16(原生 f16 算术变体):sig 通过 expr_prec_sig_native16 谓词时可生成 -// —— 全链 float16_t 计算(GL_EXT_shader_explicit_arithmetic_types_float16), -// 无 load→float()→f32 算术→float16_t() 的逐元素往返(f32 算术变体才有该 -// 往返)。与 f32 算术变体(native16=false)**并存**:后端按设备 shaderFloat16 能力选 pipeline, -// 运行时优先命中 native16 键。谓词不通过时返回空串(上层跳过 ALU 变体)。 // native16 恒走**标量 kernel**(vec4 的 f16vec4 向量化路径留作后续优化)—— -// 对应变体的 FusedShader.vec_width 必须为 1(gen_fused 同源判定)。 +// 对应变体的 FusedShader.vec_width 必须为 1(fused_generate.hpp 同源判定)。 inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, ExprPrecSig sig = 0, bool native16 = false) { @@ -1761,15 +1764,10 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, const std::string& row_var, const std::string& col_var) { - // 输入读取变量(每输入缓存一次);vp 槽 = 按视图序累计的 runtime-param 槽数 + // 输入读取变量(每输入缓存一次);vp 槽见 glsl_vp_slot for (std::size_t i = 0; i < n_inputs; ++i) { - 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 std::uint32_t vp = glsl_vp_slot(spec, i); // native16:读取直接产出 float16_t(谓词保证全部输入 f16,故 // 无 float() 包装——全链 f16,无逐元素往返);f32 算术变体保持原样。 o << indent << "const " << (native16 ? "float16_t" : "float") @@ -2000,19 +1998,18 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, return std::string{}; // 未知算子:让上层报错 } } - const auto st = [&](const std::string& comp) { - const std::string v = "r" + std::to_string(last_dst) + "." + comp; - return (out_f16 && !disp) ? ("float16_t(" + v + ")") : v; - }; // 运行期精度分派:输出双视图 + uniform 存储分支(每元素只写一次, // 两侧视图按各自的元素宽度寻址,无别名风险)。 + // 输出类型转换由本 lambda 统一负责(c16)——此前另有一个 st() 也包 + // float16_t(...),在"非分派 + f16 输出"上生成 float16_t(float16_t(x))。 const auto emit_store4 = [&](const char* arr, bool c16, const char* ind) { const std::string o = c16 ? "float16_t(" : ""; const std::string c = c16 ? ")" : ""; - L << ind << arr << "[base] = " << o << st("x") << c << ";\n"; - L << ind << arr << "[base+1u] = " << o << st("y") << c << ";\n"; - L << ind << arr << "[base+2u] = " << o << st("z") << c << ";\n"; - L << ind << arr << "[base+3u] = " << o << st("w") << c << ";\n"; + const std::string r = "r" + std::to_string(last_dst); + L << ind << arr << "[base] = " << o << r << ".x" << c << ";\n"; + L << ind << arr << "[base+1u] = " << o << r << ".y" << c << ";\n"; + L << ind << arr << "[base+2u] = " << o << r << ".z" << c << ";\n"; + L << ind << arr << "[base+3u] = " << o << r << ".w" << c << ";\n"; }; if (disp) { @@ -2131,8 +2128,10 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, std::ostringstream L; L << "// ── 自动生成(AOT 算子融合 · 归约 kernel),请勿手动编辑 ──\n"; L << "// 表达式: " << name; - if (sign_f16) + if (sign_f16 && !disp) L << " [精度 " << expr_prec_sig_str(sig, n_inputs) << "]"; + else if (disp) + L << " [运行期精度分派]"; L << "\n"; L << "#version 450\n"; L << "#extension GL_KHR_shader_subgroup_basic : enable\n"; @@ -2189,6 +2188,10 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, L << " float rp" << i << ";\n"; L << "};\n\n"; L << "shared float s_red[" << n_slots << "][256];\n\n"; + // 分组归约视图的读取辅助函数(本生成器也调用 glsl_view_read):不发射的话 + // 含该视图的 spec 会生成未声明函数 `gr_r` —— glslc 报错(响亮,非静默), + // 但报错点离根因很远。无该视图时本调用不产出任何文本。 + glsl_emit_grouped_reduce_helpers(L, spec, sig, /*f16_native=*/false); // 输出写回:非分派 = 单条赋值(wr 包 float16_t);分派 = 输出双视图 // + uniform 分支(每个元素只写一次)。 @@ -2303,16 +2306,11 @@ inline std::string generate_glsl(const std::string& name, const ExprSpec& spec, if (vk == ExprViewKind::ColBroadcast) return rd(op.idx, "col"); // Linear / RotateHalf / RowMod / RowGather / BatchMod:索引映射内联; - // vp 槽 = 按视图序累计的 runtime-param 槽数 + // vp 槽见 glsl_vp_slot std::ostringstream os; - std::uint32_t vp = 0; - for (std::size_t j = 0; j < op.idx; ++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)); glsl_view_read(os, v, static_cast(op.idx), - "(row*cols + col)", "row", "col", vp, sig); + "(row*cols + col)", "row", "col", + glsl_vp_slot(spec, op.idx), sig); const std::string raw = os.str(); // f16 槽:视图读取返回 float16_t 左值 → 显式转 f32(算术仍在 f32) return (sign_f16 && expr_prec_sig_in_f16(sig, op.idx)) diff --git a/include/neuralnet.cpp/expr_registry.hpp b/include/neuralnet.cpp/expr_registry.hpp index 730b90b9..5fd81187 100644 --- a/include/neuralnet.cpp/expr_registry.hpp +++ b/include/neuralnet.cpp/expr_registry.hpp @@ -6,8 +6,9 @@ // AOT 收集架构(表达式只在 Layer 里,别处一律不出现): // ① 构建期 `scan_exprs` 收集结构:`FusedAnchor` 在静态初始化期按 // 表达式**类型**登记(编译期可达,覆盖 dry-run 跑不到的分支),再由 -// dry-run / 模型 pass 补精度签名——每个 dsl::compute* 在记录模式下把 -// 折叠出的 ExprSpec 登记进本注册表(按 expr_spec_key 去重)。 +// dry-run / 模型 pass 补"只有执行 Layer 才拿得到"的结构——每个 +// 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 已并入)。 @@ -43,14 +44,13 @@ struct ExprRegistry void add(const ExprSpec& s) { // 登记 canonical IR:canonicalize 为引擎内部优化(IR-A/IR-B), - // key 建立在 canonical 形态上(scan 与 runtime 两端一致)。 + // key 建立在 canonical 形态上(scan 与 runtime 两端一致;运行时侧 + // 同一规则见 expr_opt.hpp 的 expr_spec_lookup_key)。 const ExprSpec canon = canonicalize_expr_spec(s); const std::string k = expr_spec_key(canon); if (keys.insert(k).second) specs.push_back(canon); } - [[nodiscard]] bool contains(const ExprSpec& s) const - { return keys.count(expr_spec_key(canonicalize_expr_spec(s))) != 0; } }; // 全局注册表(scan_exprs 记录模式写入;普通构建不含 NN_EXPR_SCAN,零开销) diff --git a/include/neuralnet.cpp/expr_spec.hpp b/include/neuralnet.cpp/expr_spec.hpp index 87a44289..91eccbcd 100644 --- a/include/neuralnet.cpp/expr_spec.hpp +++ b/include/neuralnet.cpp/expr_spec.hpp @@ -121,8 +121,10 @@ enum class ExprOperandKind : uint8_t // 修正系数),不是表达式结构 → 不进 expr_spec_key;同结构不同值的 // 表达式(如不同 lr、每步变化的 inv_bc)共享一个融合 shader,运行时 // 按实际 spec 填充 push constant。 - // 与 Const 的区别:Const 值进 key(结构)、编译期不变;RParam 值不进 - // key(运行时)、CPU 求值用 spec.rparams[idx]、GPU 经 PC 传入。 + // 与 Const 的区别:两者**都不进 key**(值都是运行时数据);差别只在 + // 载体——Const 是每结构固定的小常量池(PC `c`)、RParam 是可每步变化的 + // 标量槽(PC `rp`);CPU 求值分别取 spec.consts[idx] / spec.rparams[idx]。 + // (历史:Const 值曾进 key,与 RParam 不对称且导致同结构重复编译,已改。) RParam = 9, // ── fold 行向量态操作数:按当前输出列 d 读行向量态[idx][d]── // 仅允许出现在 fold 的 finalize(向量域逐列求值);idx 恒 0(单槽, @@ -731,9 +733,10 @@ using ExprPrecSig = std::uint32_t; // · 运行期分派:精度是**参数**(PC `prec`),一个结构一份 shader 覆盖全部 // 签名 → 不需要发现 → 构建期只需"结构"这一维(锚点已自动)。 // -// 哨兵取值落在输入位(bit 0..15)与输出位(bit 16)之外 —— 因此既有谓词 -// expr_prec_sig_in_f16 / expr_prec_sig_out_f16 对它一律返回 false, -// 旧路径不会被误触发。 +// 哨兵取值落在输入字段(bit 0..15)与输出位(bit 16)之外 —— 因此谓词 +// expr_prec_sig_in_f16 / expr_prec_sig_out_f16 对它一律返回 false,旧路径不会 +// 被误触发。⚠ 该结论**依赖 in_f16 把入参先与 EXPR_PREC_SIG_INPUT_MASK 相与** +// (否则 bit 17 会被读成"第 17 个输入是 f16")。 inline constexpr ExprPrecSig EXPR_PREC_SIG_DISPATCH = 0x00020000u; [[nodiscard]] inline constexpr bool expr_prec_sig_is_dispatch(ExprPrecSig s) noexcept @@ -742,14 +745,34 @@ inline constexpr ExprPrecSig EXPR_PREC_SIG_DISPATCH = 0x00020000u; // 运行期分派变体的 key 后缀(结构 key + "#x")。#x 不含 [0-9a-f] 之外的 // 字符,且既有变体后缀是 "#%04x" / "#a",不会碰撞。 inline constexpr const char* EXPR_PREC_DISPATCH_SUFFIX = "#x"; +// native16(原生 f16 算术,见 expr_prec_sig_native16)变体的 key 后缀。 +inline constexpr const char* EXPR_PREC_ALU_SUFFIX = "#a"; // 第 i 个输入是否为 f16 / 输出是否为 f16 +// ⚠ in_f16 必须先与 EXPR_PREC_SIG_INPUT_MASK 相与:EXPR_PREC_SIG_DISPATCH +// (0x00020000)是**哨兵**而非位图,不对齐输入字段就会把 bit 17 读成 +// "第 17 个输入是 f16"(EXPR_MAX_INPUTS=16 时侥幸为假,改上限即踩雷)。 [[nodiscard]] inline constexpr bool expr_prec_sig_in_f16(ExprPrecSig s, std::size_t i) noexcept -{ return i < 32u && ((s >> i) & 1u) != 0u; } +{ + return i < EXPR_PREC_SIG_OUT_BIT && + ((s & EXPR_PREC_SIG_INPUT_MASK) >> i & 1u) != 0u; +} [[nodiscard]] inline constexpr bool expr_prec_sig_out_f16(ExprPrecSig s) noexcept { return ((s >> EXPR_PREC_SIG_OUT_BIT) & 1u) != 0u; } +// 变体的**实际输出存储精度**:运行期分派变体(`key#x`)按运行期真实签名 +// (bit16)决定,其余变体(V0 / `key#a`)按变体自身的身份签名决定。 +// 各 eval 入口共用本函数——同一条规则手写四处时写错一处,就会出现 +// "输出缓冲按 4B 分配、shader 却按 2B 写"这类静默错值。 +[[nodiscard]] inline constexpr bool expr_variant_out_f16(ExprPrecSig variant_sig, + ExprPrecSig runtime_sig) noexcept +{ + return expr_prec_sig_is_dispatch(variant_sig) + ? expr_prec_sig_out_f16(runtime_sig) + : expr_prec_sig_out_f16(variant_sig); +} + // 变体索引 key:全 f32 → 结构 key 本身;否则加 "#xxxx" [[nodiscard]] inline std::string expr_prec_sig_key(const std::string& spec_key, ExprPrecSig sig) diff --git a/src/expr_cpu_test.cpp b/src/expr_cpu_test.cpp index 61d80f90..1f594411 100644 --- a/src/expr_cpu_test.cpp +++ b/src/expr_cpu_test.cpp @@ -45,9 +45,9 @@ // ── expr_fused_key_test("结构 = 表达式类型":AOT 自登记的 key 契约)──────── #define main test_fused_key -#define g_fail_fk g_fail_fused_key +#define g_fail g_fail_fused_key #include "expr_fused_key_test.cpp" -#undef g_fail_fk +#undef g_fail #undef main int main() diff --git a/src/expr_fold_test.cpp b/src/expr_fold_test.cpp index 69f57fbd..acbbbcd3 100644 --- a/src/expr_fold_test.cpp +++ b/src/expr_fold_test.cpp @@ -13,8 +13,8 @@ // 参考;{2,133,4} 跨 EXPR_FOLD_BLOCK=128 → 多块流式 + causal 整块跳过 // 6. validate 负例:状态吃元素源 / fold+顶层 instrs / fold+matmul / // finalize 读输入 —— 四类违规必须被拒绝(静态拒而非静默错算) -// 7. registry bin roundtrip:v8 tri_skip 写读对称(key 含该位 → -// 往返 key 全等即证未丢;丢失=静默退化全量算、其余测试仍会绿) +// 7. 注册表去重 + key 契约(原 bin 序列化 roundtrip 的接替者:bin 已删除): +// 同结构重复 add 只留一条;tri_skip(只翻该字段)与掩码 body 差异均进 key // 纯 CPU;输出 (rows,1)(eval_expr 的 cols 参数 = 1 调用约定)。 // ─────────────────────────────────────────────────────────────────────────── @@ -473,9 +473,21 @@ int test_expr_fold() reg.add(sk); // 重复 add → 按 key 去重 check(n1 == 3 && reg.specs.size() == 3, "registry add 去重 n=" + std::to_string(reg.specs.size())); + // tri_skip 进 key 必须**只翻该字段**来验:Causal vs Plain 的 body 本就 + // 不同(has_causal 多 3 条 Gt/Select/Add 指令,指令序列进 key),拿它们 + // 比 key 即使删掉 `feed(&tri_skip, 1)` 也照样"通过" = 假测试。 + { + nn::ExprSpec a = sk; + nn::ExprSpec b = sk; + b.fold->tri_skip = !a.fold->tri_skip; + check(nn::expr_spec_key(nn::canonicalize_expr_spec(a)) != + nn::expr_spec_key(nn::canonicalize_expr_spec(b)), + "fold tri_skip 进 key:同结构仅翻 tri_skip 位后 key 必须不同"); + } + // 顺带锁死"不同掩码 = 不同结构"(body 差异进 key) 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 必须不同"); + "Causal vs Plain 掩码 body 不同 → key 不同"); bool saw_skip = false; for (const auto& s : reg.specs) if (s.fold && s.fold->tri_skip) saw_skip = true; diff --git a/src/expr_fused_key_test.cpp b/src/expr_fused_key_test.cpp index aad787c9..667de5f7 100644 --- a/src/expr_fused_key_test.cpp +++ b/src/expr_fused_key_test.cpp @@ -24,7 +24,7 @@ namespace { -int g_fail_fk = 0; +int g_fail = 0; [[nodiscard]] std::string fk_key(const nn::ExprSpec& s) { return nn::expr_spec_key(nn::canonicalize_expr_spec(s)); } @@ -37,7 +37,7 @@ void fk_eq(const char* name, const std::string& a, const std::string& b, std::printf("[PASS] %s(%s)key=%s\n", name, why, a.c_str()); return; } - ++g_fail_fk; + ++g_fail; std::printf("[FAIL] %s:key 应相同但不同(%s)\n a=%s\n b=%s\n", name, why, a.c_str(), b.c_str()); } @@ -91,7 +91,9 @@ int main() for (const Pair& p : pairs) fk_eq(p.name, p.real, p.sym, "符号实例 key ≡ 真实实例 key"); - // 默认构造可用性(锚点要求整棵表达式树可符号构造) + // 默认构造可用性(锚点要求整棵表达式树可符号构造)。上面 pairs[] 里的 + // `S`(默认构造 Tensor)折叠与真实实例同 key,已经证明"可符号构造"这条 + // 不变量**成立**;这里只再补类型层面的静态断言(编译期,无运行期成本)。 static_assert(std::is_default_constructible_v); static_assert(std::is_default_constructible_v); static_assert(std::is_default_constructible_v); @@ -115,19 +117,8 @@ int main() 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"); } + // (mm_trans 的位编码断言在 expr_matmul_test 的第 4 节——同一聚合目标里 + // 不重复;此处只锁 key 契约。) } // ── ④ 分组归约 R 不进 key(运行期视图参数)──────────────────────────── @@ -144,9 +135,9 @@ int main() 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"); } + else { ++g_fail; 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; + std::printf("\nexpr_fused_key_test: %d failure(s)\n", g_fail); + return g_fail != 0 ? 1 : 0; } diff --git a/src/maxpool_gradcheck.cpp b/src/maxpool_gradcheck.cpp index 0006c713..1f5a76fd 100644 --- a/src/maxpool_gradcheck.cpp +++ b/src/maxpool_gradcheck.cpp @@ -135,8 +135,10 @@ int main(int argc, char* argv[]) {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)会闭合世界未命中硬报错;本用例是该回归的门禁。 + // → 任意池化窗口共享同一个融合 shader。本用例提供**非扫描窗口尺寸** + // 的数值覆盖(库内扫描/模型 pass 只跑 pool∈{2,3};真正锁死"R 不进 key" + // 的契约断言在 expr_fused_key_test / expr_matmul_test 的 key 相等断言, + // 本用例不是那条门禁——R 若被折回 key,扫描仍会注册 pool=3 的结构)。 {2, 7, 7, 3, 3, 2}, }; const Scalar tol = 1e-6f; diff --git a/tools/fused_generate.hpp b/tools/fused_generate.hpp index fe2e4265..d9fa7e68 100644 --- a/tools/fused_generate.hpp +++ b/tools/fused_generate.hpp @@ -64,10 +64,14 @@ namespace if (!f) return {}; f.seekg(0, std::ios::end); const std::streamsize sz = f.tellg(); + // tellg() 失败返回 -1:不判就 static_cast(-1) 请求 ~4GB 分配。 + // SPIR-V 恒为 4 字节字的整数倍——不是即文件损坏(截断的 glslc 产物)。 + if (sz <= 0 || sz % static_cast(sizeof(std::uint32_t)) != 0) + return {}; f.seekg(0, std::ios::beg); std::vector v(static_cast(sz) / sizeof(std::uint32_t)); - if (!v.empty()) - nn::read_pod_span(f, std::span(v)); + if (!nn::read_pod_span(f, std::span(v))) + return {}; // 截断读 → 视为失败(调用方报 [FAIL] 并中止构建) return v; } @@ -178,30 +182,55 @@ namespace namespace nn::tool { +// 构建期工具的环境变量开关(非空且首字符非 '0');与库里的 nn::dsl::env_flag +// 同语义,但工具链不为一个开关拉进整个 DSL 头。两个构建期工具共用本函数。 +[[nodiscard]] inline bool tool_env_flag(const char* name) +{ +#if defined(_MSC_VER) + char* b = nullptr; std::size_t n = 0; + _dupenv_s(&b, &n, name); + const bool v = (b != nullptr && b[0] != '\0' && b[0] != '0'); + std::free(b); + return v; +#else + const char* v = std::getenv(name); + return v != nullptr && v[0] != '\0' && v[0] != '0'; +#endif +} + +// 该结构是否可生成融合 shader(生成循环 / emit_one / 元数据循环三处共用同一 +// 判据——此前三处各写一遍,靠注释"同上"维持同步,漏一处就是"元数据与 shader +// 集合不一致")。raxis == -2(行+列混合归约)无法单 kernel 融合; +// 空指令表且无 matmul/fold 段 = 结构损坏(上游变换丢了 fold 段)。 +[[nodiscard]] inline bool is_fusable(const nn::ExprSpec& spec) +{ + if (nn::expr_spec_reduce_axis(spec) == -2) return false; + if (spec.instrs.empty() && !spec.matmul && !spec.fold) return false; + return true; +} + +// 该结构/变体的每线程元素数(1=标量, 4=vec4):vec4 资格 = 非 fold、非归约、 +// 且逐元素形态可向量化;native16(ALU)变体恒标量(生成器同源判定)。 +[[nodiscard]] inline std::uint32_t fused_vec_width_for(const nn::ExprSpec& spec, + bool alu) +{ + if (alu || spec.fold) return 1u; + if (nn::expr_spec_reduce_axis(spec) >= 0) return 1u; + return nn::glsl_vec4_eligible(spec) ? 4u : 1u; +} + // 生成融合 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 = {}) + const nn::fused::ExprRegistry& reg) { std::error_code ec; std::filesystem::create_directories(out_dir, ec); // A/B 逃生阀:跳过运行期精度分派变体(键 #x)的发射。 - const bool nn_no_dispatch = [] { -#if defined(_MSC_VER) - char* b = nullptr; std::size_t n = 0; - _dupenv_s(&b, &n, "NN_SCAN_NO_DISPATCH"); - const bool v = (b != nullptr && b[0] != '\0' && b[0] != '0'); - std::free(b); - return v; -#else - const char* v = std::getenv("NN_SCAN_NO_DISPATCH"); - return v != nullptr && v[0] != '\0' && v[0] != '0'; -#endif - }(); + const bool nn_no_dispatch = tool_env_flag("NN_SCAN_NO_DISPATCH"); if (reg.specs.empty()) { @@ -211,9 +240,12 @@ namespace nn::tool std::ostringstream H; H << "// ═══════════════════════════════════════════════════════════════\n"; - H << "// fused_registry.hpp — AUTO-GENERATED by gen_fused,请勿手动编辑\n"; - H << "// 构建期 scan_exprs 收集 + gen_fused 合成的融合 shader 注册表\n"; - H << "// 每个条目:key(=expr_spec_key) → {ExprSpec 结构, 内联 SPIR-V}\n"; + H << "// fused_registry.hpp — AUTO-GENERATED by tools/fused_generate.hpp,请勿手动编辑\n"; + H << "// 生成者 = scan_exprs(收集 + 合成一体,见 tools/scan_exprs.cpp)\n"; + H << "// 每个条目:key(=expr_spec_key[#x|#a]) → {ExprSpec 结构, 内联 SPIR-V}\n"; + H << "// · key V0 全 f32\n"; + H << "// · key#x V1 运行期精度分派(PC `prec`,覆盖任意签名)\n"; + H << "// · key#a V2 native16 原生 f16 算术(结构谓词判定)\n"; H << "// 运行时按 key 精确匹配 dispatch(闭合世界)。\n"; H << "// ═══════════════════════════════════════════════════════════════\n"; H << "#ifndef NN_FUSED_REGISTRY_HPP\n#define NN_FUSED_REGISTRY_HPP\n"; @@ -231,34 +263,28 @@ namespace nn::tool H << " std::uint32_t view_param_count; // 运行时视图参数个数(RowMod/RotateHalf 的 push constant vp 槽)\n"; H << " std::uint32_t rparam_count; // 运行时标量参数个数(优化器 lr/eps/β 等的 push constant rp 槽)\n"; H << " std::uint32_t vec_width; // 每线程处理元素数(1=标量, 4=vec4)\n"; - H << " std::uint32_t prec_sig; // 精度签名(Phase 2 in-kernel f16;0 = 全 f32)\n"; - H << " // 位 i = 第 i 个输入 f16,bit16 = 输出 f16\n"; + H << " std::uint32_t prec_sig; // 变体签名:V0=0;V1=EXPR_PREC_SIG_DISPATCH\n"; + H << " // (运行期按真实签名填 PC `prec`);\n"; + H << " // V2=\"全输入 f16 + 输出 f16\"位图\n"; H << "};\n\n"; for (const auto& spec : reg.specs) { - const int raxis = nn::expr_spec_reduce_axis(spec); - if (raxis == -2) + if (!is_fusable(spec)) { - // 混合归约轴(行+列)无法单 kernel 融合:跳过,运行时闭合世界硬报错 - std::fprintf(stderr, "[skip] 混合归约轴结构不支持融合: %s\n", + // 混合归约轴(行+列)无法单 kernel 融合 / 空指令表无段 = 结构损坏 + // (上游变换丢掉 fold 段即此形态)。跳过而非让生成器对空表 back() + // 越界(表现为 0xC00000FD 栈崩溃)。正常运行期到不了这里。 + std::fprintf(stderr, "[skip] 不可融合结构: %s\n", nn::expr_spec_key(spec).c_str()); continue; } // matmul+列归约(列方向,如 col_max(matmul)):生成器按 // 元素分解 batch(batch = row/m_per),归约遍历全部 rows(含所有 // batch),与 CPU 端 matmul_out 语义一致 → 正常生成,不跳过。 - // 防御:空指令表且无 matmul/fold 段 = 结构损坏(上游变换丢掉 fold 段 - // 即此形态)→ 跳过而非让逐元素生成器对空表 back() UB 崩溃 - // (表现为 0xC00000FD 栈崩溃)。正常管线到不了这里。 - if (spec.instrs.empty() && !spec.matmul && !spec.fold) - { - std::fprintf(stderr, "[skip] 空指令表且无 matmul/fold 段(结构损坏): %s\n", - nn::expr_spec_key(spec).c_str()); - continue; - } // matmul+归约(注意力结构):generate_glsl_reduce 支持 Matmul - // 操作数(内联点积,不物化 (batch*M,N) 中间矩阵),该形态正常生成(不跳过)。 + // 操作数(内联点积,不物化 (batch*M,N) 中间矩阵),同样不跳过。 + const int raxis = nn::expr_spec_reduce_axis(spec); const std::string key = nn::expr_spec_key(spec); const std::string comp_path = out_dir + "/fused_" + key + ".comp"; const std::string spv_path = out_dir + "/fused_" + key + ".spv"; @@ -306,56 +332,61 @@ namespace nn::tool H << std::dec << "\n};\n\n"; } - // ── 精度变体(Phase 2 in-kernel f16):同一结构 + 非零精度签名 → 独立 shader - // 注册键 = key#sig(expr_prec_sig_key),文件名/标识符用 key_sighex(标识符 - // 不能含 '#')。生成器不支持该形态(超出当前带类型生成能力) - // 时返回空串 → **跳过而非失败**:运行时不命中即回退边界 cast,正确性不变。 + // ── 结构变体:V1 运行期精度分派(键 = key#x)与 V2 native16(键 = key#a)── + // + // V1:**每个结构无条件发射一份**(除非 A/B 逃生阀关闭)——输入/输出各声明 + // f32 + float16_t 双视图,加载/存储按 push constant `uint prec` 走 uniform + // 分支 → 一个 shader 覆盖全部 (输入精度位图, 输出精度)。这是"签名不再需要 + // 构建期枚举"的载体:构建期只需"结构"这一维(锚点 + dry-run 已自动)。 // - // ALU 变体(native16 原生 f16 算术):谓词 expr_prec_sig_native16 通过时 - // 额外发射一份(键 = key#sig#a,标识符后缀 _a)。与 f32 算术变体并存—— - // 后端按设备 shaderFloat16 能力创建 pipeline,运行时优先命中 #a 键。 + // V2:生成条件 = **结构谓词** expr_prec_sig_native16 代入"全输入 f16 + + // 输出 f16"这一结构性常量签名 → 无 fold / 无 matmul 段 / 无归约 / 无 rparams + // / 无常量微值。**该谓词含常量值判据,而常量值不进 key**:运行期选 + // `#a` 前必须用真实实例的 spec 重跑同一谓词(GpuEngine::find_prec_variant_) + // ——否则"生成时那个实例的常量"会被套用到常量不同的同结构实例上(微值 + // 在 f16 下 flush-to-0 → 静默错值)。 // ALU 变体恒为**标量 kernel**(generate_glsl native16 分支不走 vec4), - // vec_width 必须写 1(与生成器同源,否则 dispatch 宽度失配 → 静默算 1/4)。 + // vec_width 写 1(与生成器同源,否则 dispatch 宽度失配 → 静默算 1/4)。 + // + // A/B 逃生阀:NN_SCAN_NO_DISPATCH=1 时不发射 V1(退回旧集合); + // 只影响登记集合,不影响语义。 struct VariantEmit { const nn::ExprSpec* spec; - nn::ExprPrecSig sig; - std::string vkey; // "key#sig" 或 "key#sig#a"(注册表键) - std::string suffix; // "key_sighex" 或 "key_sighex_a"(标识符/文件名) + nn::ExprPrecSig sig; // 变体签名:分派哨兵 / "全 f16" 位图 + std::string vkey; // "key#x" 或 "key#a"(注册表键) + std::string suffix; // "key_x" / "key_a"(标识符与文件名,不含 '#') bool alu = false; // native16 变体(vec_width 恒 1) }; std::vector emitted_variants; - // 去重:scan 预测变体与 run-only 回填清单可能重叠 —— 重复 - // 发射会生成重复的 kSpirv_ 标识符与注册行(重定义编译错误)。 + // 去重:同一 vkey 只发射一次(重复会生成重复的 kSpirv_ 标识符 → 重定义错误) std::unordered_set emitted_vkeys; - // 单个 (spec, sig) 的发射:kind = "f32" | "a"(ALU) + // 单个 (spec, sig) 的发射:alu = native16(V2);否则分派(V1) const auto emit_one = [&](const nn::ExprSpec& spec, nn::ExprPrecSig sig, bool alu) -> bool { - const int raxis = nn::expr_spec_reduce_axis(spec); - if (raxis == -2) - return false; // 混合归约轴(与基础结构 skip 一致) - if (spec.instrs.empty() && !spec.matmul && !spec.fold) + if (!is_fusable(spec)) return false; if (alu && !nn::expr_prec_sig_native16(spec, sig)) - return false; // 谓词不通过 → 不发 ALU 变体(f32 算术变体仍覆盖) + return false; // 谓词不通过 → 不发 ALU 变体(V1 仍覆盖) const std::string key = nn::expr_spec_key(spec); // 只有两种结构变体(精度不再是身份): // V1 运行期精度分派 → 键 key#x // V2 native16(结构谓词判定)→ 键 key#a const bool dispv = nn::expr_prec_sig_is_dispatch(sig); const std::string vkey = dispv ? (key + nn::EXPR_PREC_DISPATCH_SUFFIX) - : (key + "#a"); + : (key + nn::EXPR_PREC_ALU_SUFFIX); const std::string suffix = dispv ? (key + "_x") : (key + "_a"); if (!emitted_vkeys.insert(vkey).second) - return false; // 已发射(scan 预测 ∩ 回填清单重叠)→ 幂等跳过 + return false; // 已发射 → 幂等跳过 const std::string comp_path = out_dir + "/fused_" + suffix + ".comp"; const std::string spv_path = out_dir + "/fused_" + suffix + ".spv"; auto emitter = nn::emitter_registry::make("glsl"); if (!emitter) { std::fprintf(stderr, "[FAIL] 无法创建 GLSL emitter(IR-D 注册表异常)\n"); - std::exit(1); + return false; } + const int raxis = nn::expr_spec_reduce_axis(spec); const std::string glsl = (raxis >= 0) ? emitter->generate_reduce("fused_" + suffix, spec, sig) : emitter->generate("fused_" + suffix, spec, sig, alu); @@ -371,19 +402,19 @@ namespace nn::tool } { std::ofstream f(comp_path); - if (!f) { std::fprintf(stderr, "[FAIL] 无法写入 %s\n", comp_path.c_str()); std::exit(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", vkey.c_str()); - std::exit(1); + std::fprintf(stderr, "[FAIL] glslc 编译变体 %s 失败\n", vkey.c_str()); + return false; } const auto spv = read_spv(spv_path); if (spv.empty()) { std::fprintf(stderr, "[FAIL] 读取 %s 失败\n", spv_path.c_str()); - std::exit(1); + return false; } H << "inline constexpr std::uint32_t kSpirv_" << suffix << "[] = {"; for (std::size_t i = 0; i < spv.size(); ++i) @@ -396,72 +427,50 @@ namespace nn::tool return true; }; - // ── 运行期精度分派变体(V1,键 = key#x)────────────────────────────── - // **每个结构无条件发射一份**:输入/输出各声明 f32 + float16_t 双视图, - // 加载/存储处按 push constant `uint prec` 走 uniform 分支 → 一个 shader - // 覆盖全部 (输入精度位图, 输出精度)。 - // - // 这是"签名不再需要构建期枚举"的载体:运行时按真实张量精度填 prec, - // 任何签名都有 shader → **永不 miss**,因此 dry-run / 模型 pass / - // tools/prec_backfill.txt 全部不再参与生成(A2 起从 scan 移除)。 - // - // ── native16 变体(V2,键 = key#a)───────────────────────────────── - // 生成条件 = **结构谓词**(expr_prec_sig_native16 代入"全输入 f16 + - // 输出 f16"这一结构性常量签名):无 fold / 无 matmul 段 / 无归约 / - // 无 rparams / 无常量微值。运行时仅在真实签名确为全 f16 时才选它 → - // 生成期不需要知道签名,运行期也不需要发现。 - // - // A/B 逃生阀:NN_SCAN_NO_DISPATCH=1 时不发射 V1(退回旧集合); - // 只影响登记集合,不影响语义。 for (const auto& spec : reg.specs) { - if (!nn_no_dispatch) - emit_one(spec, nn::EXPR_PREC_SIG_DISPATCH, /*alu=*/false); - const std::size_t n_in = spec.views.size(); - const std::uint32_t all_in_bits = - (n_in >= 32) ? 0u : ((1u << n_in) - 1u); - emit_one(spec, nn::expr_prec_sig_make(all_in_bits, /*out_f16=*/true), - /*alu=*/true); // 谓词不通过时内部静默跳过 + if (!is_fusable(spec)) + continue; + if (!nn_no_dispatch && + !emit_one(spec, nn::EXPR_PREC_SIG_DISPATCH, /*alu=*/false)) + { + // V1 发不出来只是少了 f16 路径(见 emit_one),V2/V0 仍可能成功; + // 真正的失败(写文件 / glslc / 读 SPIR-V)已在 emit_one 内报 [FAIL]。 + } + // V2:结构性"全输入 f16 + 输出 f16"签名(输入位掩码取满, + // 谓词内部再按 views 数量与其余条件判定) + emit_one(spec, nn::expr_prec_sig_make(nn::EXPR_PREC_SIG_INPUT_MASK, + /*out_f16=*/true), + /*alu=*/true); } - (void)prec_manifest; // 签名不再回填(V1 覆盖任意签名) H << "inline const FusedShader kFusedShaders[] = {\n"; for (const auto& spec : reg.specs) { - const int raxis = nn::expr_spec_reduce_axis(spec); - if (raxis == -2) - continue; // 与上方跳过保持一致(混合轴) - if (spec.instrs.empty() && !spec.matmul && !spec.fold) - continue; // 结构损坏(同上:生成循环已 skip,元数据保持一致) + if (!is_fusable(spec)) + continue; // 与上方生成循环同一判据(共享 is_fusable) const std::string key = nn::expr_spec_key(spec); - const std::uint32_t vecw = - (!spec.fold && raxis < 0 && nn::glsl_vec4_eligible(spec)) ? 4u : 1u; - // fold 形态恒 vecw=1(每线程一行标量状态机,无 vec4 路径);fold 判定 - // 走 FusedShader.spec.fold(后端同源判断,不加结构字段)。 - H << " { \"" << key << "\",\n " << emit_spec(spec) << ",\n" + const std::string spec_lit = emit_spec(spec); // 三行共用,只生成一次 + H << " { \"" << key << "\",\n " << spec_lit << ",\n" << " kSpirv_" << key << ", sizeof(kSpirv_" << key << ")/sizeof(std::uint32_t), " - << raxis << ", " << (spec.matmul ? 1 : 0) << ", " + << nn::expr_spec_reduce_axis(spec) << ", " << (spec.matmul ? 1 : 0) << ", " << nn::expr_spec_runtime_view_param_count(spec) << ", " - << nn::expr_spec_runtime_param_count(spec) << ", " << vecw + << nn::expr_spec_runtime_param_count(spec) << ", " + << fused_vec_width_for(spec, /*alu=*/false) << ", 0u },\n"; } - // 精度变体行(键 = key#sig 或 key#sig#a;元数据与基础结构同源——精度不进结构) + // 变体行(键 = key#x 或 key#a;元数据与基础结构同源——精度不进结构) for (const auto& ve : emitted_variants) { const nn::ExprSpec& spec = *ve.spec; - const int raxis = nn::expr_spec_reduce_axis(spec); - // ALU(native16)变体恒标量 kernel → vec_width 必须 1(与 generate_glsl - // 的 native16 分支同源);f32 算术变体维持原 vec4 资格判定。 - const std::uint32_t vecw = - (!ve.alu && !spec.fold && raxis < 0 && nn::glsl_vec4_eligible(spec)) - ? 4u : 1u; H << " { \"" << ve.vkey << "\",\n " << emit_spec(spec) << ",\n" << " kSpirv_" << ve.suffix << ", sizeof(kSpirv_" << ve.suffix << ")/sizeof(std::uint32_t), " - << raxis << ", " << (spec.matmul ? 1 : 0) << ", " + << nn::expr_spec_reduce_axis(spec) << ", " << (spec.matmul ? 1 : 0) << ", " << nn::expr_spec_runtime_view_param_count(spec) << ", " - << nn::expr_spec_runtime_param_count(spec) << ", " << vecw + << nn::expr_spec_runtime_param_count(spec) << ", " + << fused_vec_width_for(spec, ve.alu) << ", " << ve.sig << "u },\n"; } H << "};\n"; diff --git a/tools/scan_exprs.cpp b/tools/scan_exprs.cpp index 5fdd94ef..236709fe 100644 --- a/tools/scan_exprs.cpp +++ b/tools/scan_exprs.cpp @@ -1,19 +1,23 @@ // ─────────────────────────────────────────────────────────────────────────── -// scan_exprs.cpp — AOT 算子融合:构建期表达式收集(dry-run) +// scan_exprs.cpp — AOT 算子融合:构建期**单步**工具(收集 + 合成) // -// 用 CPU 引擎 + 假张量跑一遍相关 Layer 的 forward/backward,使每个 +// 收集:CPU 引擎 + 假张量跑一遍相关 Layer 的 forward/backward,使每个 // dsl::compute / compute_reduce / compute_into 在 NN_EXPR_SCAN 记录模式下把 -// 折叠出的 ExprSpec **结构**登记进全局注册表(按 expr_spec_key 去重),跑完 dump 成 bin。 +// 折叠出的 ExprSpec **结构**登记进全局注册表(按 expr_spec_key 去重)。 +// 脚本里还有两条互补来源:`FusedAnchor` 静态初始化期按表达式**类型** +// 自登记(编译期可达),以及 fold / matmul-trans 等显式登记块。 +// 合成:同进程内直接调 `nn::tool::generate_fused_registry` +// (tools/fused_generate.hpp,原独立工具 gen_fused)→ +// /fused_registry.hpp。**不经任何中间序列化**。 // // 表达式**文本只出现在 Layer**;这里只是"执行 Layer 代码路径"以触达它们, -// dump 出来的是派生物(结构),不是手写定义。 +// 产物是派生物(结构 + SPIR-V),不是手写定义。 // -// 用法: scan_exprs -// 之后 gen_fused 读 合成融合 shader(→ fused_registry.hpp)。 +// 用法: scan_exprs [--list-backends] // -// ⚠ 闭合世界安全网:若某条 Layer 路径未被本工具覆盖,其内联表达式在 -// GPU 运行时将硬报错,提醒把该路径补进扫描。新增融合表达式 / 新 d_k -// 时,把对应的 dry-run 调用加到下面。 +// ⚠ 闭合世界安全网:若某条 Layer 路径未被覆盖,其内联表达式在 GPU 运行时 +// 将硬报错(错误信息带 key),提醒把该路径补进扫描。新增融合表达式时优先 +// 依赖锚点自登记;只有"运行期配置决定的结构"才需要在这里补 dry-run 块。 // ─────────────────────────────────────────────────────────────────────────── // NN_EXPR_SCAN 由本文件自行开启;`nn_enable_gpu_fusion` 还会用同名宏编译 @@ -22,8 +26,12 @@ #define NN_EXPR_SCAN #endif +#include #include #include +#include +#include +#include #include "compute_cpu_engine.hpp" #include "compute_layer.hpp" @@ -63,12 +71,11 @@ void on_abort(int) } } // namespace -// ── 扫描期精度配置(Phase 2:in-kernel f16 变体收集)────────────────────── -// dry-run 收集的是**结构**(与形状/精度无关);但精度签名(哪些输入/输出是 -// f16)取决于 profile,故整段 dry-run 跑两遍:profile_f32(sig=0,登记基础结构表)与 -// profile_f16(带类型变体)。输入张量按当前 profile 的 compute 创建,使 f16 -// pass 与运行时(激活即 f16)**同源**——否则会收集到运行时永不使用的签名。 -static nn::PrecisionProfile g_scan_prof{}; +// ── 扫描期精度配置 ──────────────────────────────────────────────────────── +// dry-run 收集的是**结构**(与形状/精度无关),故只跑 profile_f32 一遍。 +// 历史上曾跑第二遍 profile_f16 以枚举"精度签名";签名已降级为运行期参数 +// (生成阶段对每个结构发运行期分派 shader,键 `key#x`),该遍已删除。 +// scan_prof 仍用于:占位张量按当前 pass 的 compute 精度创建(与真实运行同源)。 int main(int argc, char* argv[]) { @@ -77,7 +84,10 @@ int main(int argc, char* argv[]) if (std::string(argv[i]) == "--list-backends") { std::printf("[scan] 可用 emitter 后端(IR-D):\n"); - for (const auto& n : nn::emitter_registry::names()) + // 排序:names() 走 unordered_map,顺序不定;工具输出必须确定性(铁律 8) + std::vector backends = nn::emitter_registry::names(); + std::sort(backends.begin(), backends.end()); + for (const auto& n : backends) std::printf(" - %s\n", n.c_str()); return 0; } @@ -96,20 +106,20 @@ int main(int argc, char* argv[]) std::signal(SIGABRT, &on_abort); // NN_ASSERT → abort 带栈(见文件头) nn::CpuEngine raw_engine; + // 扫描期精度配置(占位张量按当前 pass 的 compute 精度创建,与真实运行同源) + nn::PrecisionProfile scan_prof{}; - // ── dry-run 主体:对给定 profile 跑一遍(f32 pass 收集 sig=0 基础结构;f16 pass 收集带类型变体) - // engine 是**形参**(遮蔽外层 raw_engine):f16 pass 直接传原生引擎即可—— - // P-1 下沉后 f16 边界 cast 由基类 NVI 入口统一处理(原 PrecisionEngine - // 适配层已删除,见 docs/development/15 §4.1);原生 CpuEngine 从未见过 - // f16 张量(历史上直交属 UB,实测 heap corruption 0xC0000374)。 + // ── dry-run 主体:跑一遍 Layer 的 forward/backward(只收集**结构**)── + // engine 是**形参**(遮蔽外层 raw_engine):f16 边界 cast 由基类 NVI 入口 + // 统一处理(原 PrecisionEngine 适配层已删除,见 docs/development/15 §4.1)。 [[maybe_unused]] const auto dry_run = [&](nn::ComputeEngine& engine, const nn::PrecisionProfile& prof) { - g_scan_prof = prof; + scan_prof = prof; // M1(docs/development/17 §4.1):库外不再直构 Tensor——scan 输入经 // 引擎创建(出生绑定与本段 dry-run 使用的 engine 一致)。 const auto scan_tensor = [&](std::size_t rows, std::size_t cols) { - return engine.create_tensor(rows, cols, g_scan_prof.compute); + return engine.create_tensor(rows, cols, scan_prof.compute); }; // ── RoPE:forward + backward ───────────────────────────────────────── // (apply 与 apply_step 折叠出的结构相同,会自动去重) @@ -123,7 +133,7 @@ int main(int argc, char* argv[]) std::size_t{64}, std::size_t{128}}) { nn::RotaryEmbedding rope(dk); - rope.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) + rope.set_precision_profile(scan_prof); // 扫描期精度(f16 pass 收集变体) (void)rope.init(engine); // M6 段 C:层由 init 绑定引擎 nn::Tensor q = scan_tensor(2 * dk, 8); // rows 为 dk 的整数倍 (void)rope.apply(engine, q, /*seq=*/8, /*backward=*/false); @@ -137,7 +147,7 @@ int main(int argc, char* argv[]) { const std::size_t R = 8, C = 5; nn::MSELoss mse; - mse.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) + mse.set_precision_profile(scan_prof); // 扫描期精度(f16 pass 收集变体) nn::Tensor pred = scan_tensor(R, C); nn::Tensor target = scan_tensor(R, C); (void)mse.forward(engine, pred, target); @@ -148,7 +158,7 @@ int main(int argc, char* argv[]) { const std::size_t C = 8, B = 5; nn::CrossEntropyLoss ce; - ce.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) + ce.set_precision_profile(scan_prof); // 扫描期精度(f16 pass 收集变体) nn::Tensor logits = scan_tensor(C, B); nn::Tensor target = scan_tensor(C, B); (void)ce.forward(engine, logits, target); @@ -187,35 +197,33 @@ int main(int argc, char* argv[]) // 前置 matmul 段(MatmulSpec)+ 尾逐元素链(Add + Max)。 // 这里直接构造折叠后的结构并登记(与 dsl::compute 登记 spec 是同一机制), // 保证 GPU 运行时同一结构命中 AOT 融合 shader(闭合世界)。 - // - transA/transB 是结构 → 分别登记(4 种组合各一个 shader) + // - transA/transB 是**运行期 operand layout**(PC `mm_trans`)→ **不进 key**: + // 4 种转置组合折叠成同一个 key,登记一次即可(历史上此处登记 4 份, + // key 去掉转置后它们全被去重合并)。 // - k(求和维度)是形状参数 → 不进 key:任取一个 K 登记,运行时任何 K - // 都命中同一融合 shader(同 RowMod/RotateHalf 的视图参数处理:不进 key,运行时填充) + // 都命中同一融合 shader(同 RowMod/RotateHalf 的视图参数处理:不进 key, + // 运行时填充)。 { - const auto make_spec = [](std::uint8_t trA, std::uint8_t trB) { - nn::ExprSpec s; - s.views = {nn::expr::linear(), nn::expr::linear(), nn::expr::linear()}; - s.num_regs = 2; - s.matmul = nn::MatmulSpec{0, 1, trA, trB, /*k=*/8}; - // Add r0 = matmul(A,B) + bias(Input 2) - s.instrs.push_back({static_cast(nn::ExprOp::Add), 0, - nn::expr::matmul_op(), nn::expr::input(2), {}}); - // Max r1 = max(r0, 0)(relu) - s.consts.push_back(nn::Scalar{0}); - s.instrs.push_back({static_cast(nn::ExprOp::Max), 1, - nn::expr::reg(0), nn::expr::cst(0), {}}); - return s; - }; - nn::fused::global_registry().add(make_spec(0, 0)); - nn::fused::global_registry().add(make_spec(0, 1)); - nn::fused::global_registry().add(make_spec(1, 0)); - nn::fused::global_registry().add(make_spec(1, 1)); + nn::ExprSpec s; + s.views = {nn::expr::linear(), nn::expr::linear(), nn::expr::linear()}; + s.num_regs = 2; + // transA/transB 取 (0,0) 作代表值:它们不进 key + s.matmul = nn::MatmulSpec{0, 1, /*transA=*/0, /*transB=*/0, /*k=*/8}; + // Add r0 = matmul(A,B) + bias(Input 2) + s.instrs.push_back({static_cast(nn::ExprOp::Add), 0, + nn::expr::matmul_op(), nn::expr::input(2), {}}); + // Max r1 = max(r0, 0)(relu) + s.consts.push_back(nn::Scalar{0}); + s.instrs.push_back({static_cast(nn::ExprOp::Max), 1, + nn::expr::reg(0), nn::expr::cst(0), {}}); + nn::fused::global_registry().add(s); // 纯 matmul(无逐元素链):输出 = matmul 结果 { - nn::ExprSpec s; - s.views = {nn::expr::linear(), nn::expr::linear()}; - s.num_regs = 0; - s.matmul = nn::MatmulSpec{0, 1, 0, 0, /*k=*/8}; - nn::fused::global_registry().add(s); + nn::ExprSpec pure; + pure.views = {nn::expr::linear(), nn::expr::linear()}; + pure.num_regs = 0; + pure.matmul = nn::MatmulSpec{0, 1, 0, 0, /*k=*/8}; + nn::fused::global_registry().add(pure); } } @@ -297,7 +305,7 @@ int main(int argc, char* argv[]) { nn::CausalSelfAttention attn(d_model, heads, /*max_len=*/1024, /*seq_len=*/seq, enc); - attn.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) + attn.set_precision_profile(scan_prof); // 扫描期精度(f16 pass 收集变体) (void)attn.init(engine); // M6 段 C:层由 init 绑定引擎 run_csa(attn); } @@ -306,7 +314,7 @@ int main(int argc, char* argv[]) const std::size_t doc_ids[8] = {0, 0, 1, 1, 0, 0, 1, 1}; nn::CausalSelfAttention attn_d(d_model, heads, 1024, seq, nn::PosEncodingType::Learned); - attn_d.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) + attn_d.set_precision_profile(scan_prof); // 扫描期精度(f16 pass 收集变体) (void)attn_d.init(engine); // M6 段 C:层由 init 绑定引擎 attn_d.set_doc_ids(doc_ids); run_csa(attn_d); @@ -315,7 +323,7 @@ int main(int argc, char* argv[]) const std::size_t doc_ids[8] = {0, 0, 1, 1, 0, 0, 1, 1}; nn::CausalSelfAttention attn_ad(d_model, heads, 1024, seq, nn::PosEncodingType::ALiBi); - attn_ad.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) + attn_ad.set_precision_profile(scan_prof); // 扫描期精度(f16 pass 收集变体) (void)attn_ad.init(engine); // M6 段 C:层由 init 绑定引擎 attn_ad.set_doc_ids(doc_ids); run_csa(attn_ad); @@ -330,7 +338,7 @@ int main(int argc, char* argv[]) { const std::size_t d_model = 16, heads = 2, seq = 4, batch = 2; nn::MultiHeadAttention attn(d_model, heads, /*seq_len=*/seq); - attn.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) + attn.set_precision_profile(scan_prof); // 扫描期精度(f16 pass 收集变体) (void)attn.init(engine); nn::Tensor x = scan_tensor(d_model, batch * seq); auto fr = attn.forward(x); @@ -357,7 +365,7 @@ int main(int argc, char* argv[]) { const std::size_t C = 8, B = 5; nn::CrossEntropyLoss ce; - ce.set_precision_profile(g_scan_prof); // 扫描期精度(f16 pass 收集变体) + ce.set_precision_profile(scan_prof); // 扫描期精度(f16 pass 收集变体) nn::Tensor logits = scan_tensor(C, B); std::vector labels(B, 1); std::vector mask(B, 1.0f); @@ -397,45 +405,50 @@ int main(int argc, char* argv[]) const std::size_t n_after_dry = nn::fused::global_registry().specs.size(); // 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])。 + // ── 模型级 pass:用 shipped 工厂建模型跑 fwd/bwd(补"整模型路径"的结构)── + // per-layer dry-run 块只覆盖"层被单独造出来"的路径;有些调用点只在**完整 + // 模型**里才会执行到(GPT/RAPT 的 LM head、PatchEmbedding、模型级位置编码 + // 等)。它们是"运行期配置决定结构"的主要来源——实测 84 条结构里 25 条只由 + // 本 pass 产出,故**失败即构建失败**:静默降级会产出不完整的注册表,只在 + // GPU 运行期以闭合世界硬报错暴露(离根因很远)。与 dry-run 的 fail-fast + // (见 run_csa 的 abort)一致。 const auto model_pass = [&](nn::ComputeEngine& engine, - const nn::PrecisionProfile& prof) + const nn::PrecisionProfile& prof) -> bool { - g_scan_prof = prof; + scan_prof = prof; + std::size_t n_fail = 0; 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); }; + { return engine.create_tensor(rows, cols, 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, + std::fprintf(stderr, "[scan][FAIL] %s forward 失败:%s\n", name, out.error().message.c_str()); + ++n_fail; 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, + { + std::fprintf(stderr, "[scan][FAIL] %s backward 失败:%s\n", name, br.error().message.c_str()); + ++n_fail; + } }; 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, + std::fprintf(stderr, "[scan][FAIL] %s 构建失败:%s\n", name, m.error().message.c_str()); + ++n_fail; return; } run(*m, input, name); @@ -529,9 +542,20 @@ int main(int argc, char* argv[]) try_build(causal ? "rapt_causal" : "rapt_bidir", [&] { return nn::build_rapt_model(engine, cfg); }, ids); } + return n_fail == 0; }; // 模型 pass:**结构**来源(运行期配置相关的路径),与精度无关 → 只跑 f32。 - model_pass(raw_engine, nn::profile_f32()); + // 与 per-layer dry-run 同受 `-DNN_SCAN_NO_DRYRUN` 门控(该宏的文档语义就是 + // "关掉 dry-run/模型 pass",此前只关了前者)。 +#if !defined(NN_SCAN_NO_DRYRUN) + if (!model_pass(raw_engine, nn::profile_f32())) + { + std::fprintf(stderr, + "[scan][FAIL] 模型 pass 失败 → 注册表不完整,中止构建" + "(闭合世界:缺结构只在 GPU 运行期报错)\n"); + return 1; + } +#endif const std::size_t n_after_model = nn::fused::global_registry().specs.size(); // ── fold v1(标量域)分块状态归约(表达式集合登记)────────────────────────── // 三个共享样例(expr_fold.hpp——与 fused_gpu_test 对拍**同源构造** → @@ -598,24 +622,10 @@ int main(int argc, char* argv[]) // 这是"能否进一步删掉 dry-run"的唯一依据——锚点按表达式类型登记,但 // **符号实例(`Expr{}`)与真实实例可能折叠出不同结构**(运行期配置 // 维度:视图种类、输入个数、掩码组合…);这些结构锚点看不到。 - const bool dump_src = [] { -#if defined(_MSC_VER) - char* b = nullptr; std::size_t n = 0; - _dupenv_s(&b, &n, "NN_SCAN_DUMP_SOURCES"); - const bool v = (b != nullptr && b[0] == '1'); - std::free(b); - return v; -#else - const char* v = std::getenv("NN_SCAN_DUMP_SOURCES"); - return v != nullptr && v[0] == '1'; -#endif - }(); - std::unordered_set dry_keys; - for (const auto& s : reg.specs) - dry_keys.insert(nn::expr_spec_key(s)); - std::unordered_set anchor_keys; - for (const auto& s : anchor.specs) - anchor_keys.insert(nn::expr_spec_key(s)); + const bool dump_src = nn::tool::tool_env_flag("NN_SCAN_DUMP_SOURCES"); + // 两侧的 key 集合直接取注册表已有的 keys(add() 已算过 key),不再重建。 + const std::unordered_set& dry_keys = reg.keys; + const std::unordered_set& anchor_keys = anchor.keys; const auto summarize = [](const nn::ExprSpec& s) { const int raxis = nn::expr_spec_reduce_axis(s); @@ -675,12 +685,19 @@ int main(int argc, char* argv[]) nn::expr_spec_key(s).c_str(), summarize(s).c_str()); for (const auto& s : anchor.specs) reg.add(s); - std::printf("[scan] 结构来源:dry-run %zu(含模型 pass +%zu、显式登记 +%zu)" - " + 锚点 %zu → 合并 %zu(锚点独有 %zu、dry-run 独有 %zu)\n", + // 分段口径(**互不重叠**):dry-run 侧 = ①per-layer dry-run + matmul/bmm + // 显式登记(n_after_dry)+ ②模型 pass(+)+ ③fold/attn-fold 显式登记(+); + // 锚点为独立来源,与上面各段取并集去重后的新增量 = 锚点独有。 + std::printf("[scan] 结构来源:dry-run 侧 %zu(per-layer+显式 %zu、模型 pass +%zu、" + "fold 显式 +%zu) + 锚点 %zu(新增 %zu)= 合计 %zu" + "(锚点独有 %zu、dry-run 独有 %zu)\n", + n_before_anchor, n_after_dry, n_after_model - n_after_dry, n_before_anchor - n_after_model, - anchor.specs.size(), reg.specs.size(), + anchor.specs.size(), + reg.specs.size() - n_before_anchor, + reg.specs.size(), reg.specs.size() - n_before_anchor, n_dry_only); // 锚点独有 = 手写 dry-run 覆盖不到的调用点(该数突然变大 → 有新层路径 // 没纳入 dry-run)。 @@ -691,13 +708,15 @@ int main(int argc, char* argv[]) } std::printf("[scan] 收集到 %zu 条融合表达式\n", reg.specs.size()); // ── 签名覆盖:**构建期不再需要任何背书** ────────────────────────────── - // 生成阶段对每个结构无条件发一份运行期精度分派 shader(键 key#x)—— + // 生成阶段对每个结构无条件发一份运行期精度分派 shader(键 `key#x`)—— // 输入/输出双视图 + PC `prec`,一份覆盖任意 (输入精度位图, 输出精度)。 // 因此: - // · 结构 = 唯一需要"收集"的东西(锚点 + dry-run/模型 pass); - // · 签名**不再是构建期集合**,无需 dry-run 的 f16 遍、无需回填清单, - // 运行期也不存在 miss / 边界 cast 回退; + // · 结构 = 唯一需要"收集"的东西(锚点 + dry-run/模型 pass + 显式登记); + // · 签名**不再是构建期集合**,无需 dry-run 的 f16 遍、无需回填清单; // · 注册表里没有"精度变体"这一层(`ExprRegistry::variants` 已删除)。 + // ⚠ 精度路径的落点仍有三档,不是"永不 miss":`#a`(native16)→ `#x` + // (运行期分派)→ 基类边界 cast。`#x` 在生成被跳过([skip])、 + // NN_SCAN_NO_DISPATCH=1、或设备无 16bit 存储时不注册 → 落到边界 cast。 // ── 生成阶段(原 gen_fused):同一进程内直接消费注册表 → fused_registry.hpp // 构建期因此只有一步;不再经 expr_specs.bin 中间序列化。 if (!nn::tool::generate_fused_registry(out_dir, glslc, reg))