读者:项目开发者(本仓内部工作文档)。本分册定位见
README.md;历史归档在../history.md。
目标:在不推翻现有 AOT 闭合世界的前提下,把
ExprSpec从"直通轻量 IR"演进为带优化 pass 的规范 IR,分阶段提升开发便利与代码质量。 状态:现行优化链 = IR-A(canonicalize:DCE/常量折叠/代数化简/稳定重编号)+ IR-B(CSE + 寄存器分配 liveness)+ IR-C(图 IR +begin_expr/end_expr融合分析)+ IR-D(后端 emitter 抽象),分别由expr_opt.hpp/expr_graph.hpp/expr_emitter.hpp承载;IR-C 已恢复并通用化(P1 机制 + P2 写穿多输出 + Adam 接线,见 §5.3)。 关联文档:02-operator-fusion.md(算子融合)、10-development-standards.md(分层铁律)、08-pitfalls-and-lessons.md。
当前融合算子生成流水线为四段:
Layer 内联表达式 → to_expr_spec 折叠 → ExprSpec → glsl_gen → GLSL → glslc → SPIR-V → fused_registry.hpp
(前端) (IR) (后端)
运行时:fold → expr_spec_key → find_fused(key) → dispatch(闭合世界)
ExprSpec 是事实上的轻量 IR:SSA 式扁平指令表、内存访问视图、可序列化(NNEXP)、跨后端、确定性结构 key。它自带一组确定性优化 pass(IR-A/IR-B,expr_opt.hpp):折叠产出的指令序列先经 canonicalize_expr_spec 规范化再算 key,glsl_gen 只消费 canonical 形态。
这组 pass 解决的是"直通 IR"(无 pass 时)的五类问题——它们是本文 pass 设计的动机,也是当前由 pass 自动处理的事项:
- 子表达式重复导致超输入/寄存器上限(如
grad*gamma出现 3 次即超输入上限、被迫手工拆表达式)→ 现由 CSE 消除。 - 无死代码消除、无常量折叠、无代数化简,shader 携带冗余计算 → 现由 canonicalize 链处理。
- 无寄存器分配,
num_regs线性增长,受EXPR_MAX_REGS=32约束 → 现由活跃性分配复用。 - 后端耦合:
glsl_gen为 GLSL 专用,无法"一份 IR 多后端"(CUDA 后端已整体移除)→ IR-D emitter 抽象,当前注册的只有 GLSL。 - 跨表达式融合:已由 IR-C 落地(
begin_expr/end_expr+expr_graph.hpp图 IR,P1 链 / P2 分量融合,§5.3);不需要跨表达式写穿的场景仍可直接写成单个dsl::compute(单个 AOT 融合 kernel)。
本文记录 IR 优化 pass 的设计与当前实现,核心约束是不破坏闭合世界的确定性 key 匹配。
与算子融合文档一致:
| 红线 | 说明 |
|---|---|
| 算法文本只在 Layer | 公式只写在 compute_layer.hpp / compute_loss.hpp |
| 引擎只提供 op-level 原语 | 引擎认"结构"(reduce(matmul(A,B))),绝不认"算法名" |
| 融合/优化逻辑归工具/引擎内部 | IR、pass、emitter 都在引擎/工具内部,Layer 无感知 |
| Shader 是内部实现 | 融合 shader 只存在于 shaders/ + 引擎 |
IR 属于"引擎/工具内部",完全落在红线允许区,且强化"引擎认结构不认算法名"的哲学。
- 规范性:把
ExprSpec正式确立为 IR 规范,写清语义、上限、序列化。 - 可优化性:提供确定性优化 pass(DCE、常量折叠、CSE、寄存器分配、代数化简)。
- 闭合世界兼容:key 定义在 canonical(优化后)IR 上,scan 与 runtime 两端一致。
- 跨后端:IR → 多 emitter(GLSL / CPU),可选扩展。
- 可扩展性:单表达式 IR 与图 IR 分层——跨表达式融合(IR-C 图 IR)现行(§5.3),可安全融合的层由
begin_expr/end_expr显式录制。
沿用现有 ExprSpec 数据结构,仅确立为 IR 并加 canonicalization 层(下为核心四字段节选——当前完整结构还含 rparams 运行时标量池、可选 matmul 段(S1)与可选 fold 段(P-C1/C2 FoldSpec,分块状态归约,注意力 forward 即此结构),见 expr_spec.hpp):
struct ExprSpec { // 节选:核心字段
std::vector<ExprInstr> instrs; // SSA 式指令表
std::vector<ExprView> views; // 与 inputs 一一对应(索引映射)
std::vector<Scalar> consts; // 常量池
std::uint32_t num_regs;
// + rparams / std::optional<MatmulSpec> matmul / std::optional<FoldSpec> fold
};- 指令:
ExprInstr{ op, dst, a, b, c },操作数 kind 为Reg/Input/Const/Fanout/Reduce(S7 起增网格索引Row/Col/Batch与Matmul;VecState仅 fold finalize 可用)。 - 视图:
Linear/RotateHalf/RowMod/RowBroadcast/ColBroadcast+ 归约视图(S7 起增RowGather/BatchMod/BatchCol,后又有RowAccess)。 - 上限:
EXPR_MAX_INPUTS=16 / REGS=32 / INSTRS=64 / CONSTS=16;fold 段另有独立上限FOLD_MAX_BODY/FINALIZE/STATE/VEC。
canonical IR =
canonicalize_expr_spec(spec)的输出,是 key 计算、去重、shader 合成的唯一依据。
关键约定:
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<i>(ExprOperandKind::Const 发射成 c<idx>) |
rparams 个数 / 值 |
个数 ✅ / 值 ❌ | push constant rp<i> |
| 视图形状参数(RowMod/RotateHalf/BatchMod/BatchCol/RowAccess 的 param) | ❌ | push constant vp<i> |
matmul.k / matmul.batch |
❌ | push constant mm_k / mm_batch |
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 |
❌ | push constant vp<i>(读取走 gr_r<slot>() 辅助函数内的运行期循环,累加顺序与 CPU 模板路径一致) |
- pass 遍历顺序必须固定(如始终按指令序从前到后、视图按输入序、常量按出现序)。
- CSE 胜利者选择必须确定(如取首次出现、同序最小寄存器号)。
- 寄存器分配算法必须确定(固定贪心顺序,杜绝跨编译器漂移)。
- 参考既有教训:C++ 实参求值顺序未指定会让 key 跨编译器不稳定。任何 pass 不得引入依赖未定义求值顺序的逻辑。
canonicalize_expr_spec(spec) → spec,含:
| pass | 说明 |
|---|---|
| DCE(死代码消除) | 从最后一条指令(输出)反向遍历,剔除不影响输出的指令 |
| 常量折叠 | x+0、x*1、x*0、neg(neg(x))、max(x,x)、比较真值等 |
| 代数化简 | 幂等、结合/交换律的确定性规范化(可选,注意浮点语义) |
| 稳定排序/重编号 | 保证 canonical 形态跨调用一致 |
注意浮点语义:代数化简须保守,避免改变数值结果(如 a+b+c 的合并顺序会影响舍入)。默认只做不改变求值语义的化简。
| pass | 说明 |
|---|---|
| CSE(公共子表达式消除) | 哈希指令 (op,a,b,c) → 若已存在等价指令则复用其 dst,消除重复子表达式 → 降低输入/寄存器压力,缓解超限问题 |
| 寄存器分配 | 活跃性分析(liveness)→ 寄存器复用 → 降低 num_regs,让更多表达式落在 EXPR_MAX_REGS=32 内 |
| 死寄存器回收 | 结合 DCE 回收不再活跃的寄存器 |
CSE 需处理 Input/Const/Reduce 操作数的等价性(视图相同 + 输入相同 + 常量相同才等价)。归约指令的 CSE 需保证归约槽语义一致。
IR-C(图级跨表达式融合)现行:expr_graph.hpp(ExprGraph/ExprGraphNode/fuse_expr_graph/recording_graph_owner、per-graph node_outputs、占位 tag 分配器 next_virtual_tag())+ 引擎虚入口 ComputeEngine::begin_expr()/end_expr(),配套 dsl::start_expr/end_expr(ExprBlock 语法糖,单表达式跨行)与 ExprSegment(RAII 守卫)。GPU 走真录制(GpuEngine::execute_fused_graph),CPU 普通运行 no-op、NN_EXPR_SCAN 下 end_expr 跑同一套 fuse_expr_graph(构建期扫描与运行期一致)。P2 新增 ExprSpec.extras(写穿多输出,≤4)与引擎入口 eval_expr_multi_into(NVI + 虚 eval_expr_multi_into_impl),并落地 P2-12 图级计划缓存(graph_cache_key/FusedKernelPlan/instantiate_plan)。
定位:IR-C(图级跨表达式融合)现行为图 IR + begin_expr/end_expr 融合分析,P1(链融合机制)与 P2(写穿多输出 + Adam/AdamW 接线)已在训练路径落地;历史取舍正文(移除与否证依据、删除项/保留项清单、恢复流水)见 docs/history/history.md。
- 批内上传免 flush(P2):
GpuEngine::from_matrix移除 batch 模式下的强制end_batch → begin_batch。安全依据:from_matrix总是新建 GpuTensor,upload_blocking独立提交 + 等待完成,新 buffer 未被正在录制的 batch 引用,独立上传提交先于 batch(队列 FIFO)。to_matrix/copy_from仍须 flush(前者读 batch 中刚写的 buffer、后者写 batch 已引用的既有 dst)。
把 expr_glsl_gen.hpp 的 GLSL 专用生成抽象为 emitter 接口(expr_emitter.hpp):
IR → GlslEmitter(当前唯一注册后端)
ExprEmitter纯接口(name/generate/generate_reduce)+emitter_registry(按后端名选择工厂)。GlslEmitter(expr_glsl_gen.hpp的 generate_glsl/generate_glsl_reduce 封装)。- 生成阶段 经 emitter 注册表选择后端(默认 glsl);
--list-backends展示可用后端。
当前只有
glsl一个注册后端(GlslEmitter);scan_exprs --list-backends可列出。 接口 + 注册表即 IR-D 的全部交付:新后端实现ExprEmitter并注册即可,运行时无需包含该头(与glsl_gen一致)。
| 阶段 | 内容 | 当前状态 |
|---|---|---|
| IR-A | 确立 ExprSpec 为 IR 规范 + canonicalize_expr_spec(DCE/常量折叠/稳定排序)+ 接入 key |
现行(expr_opt.hpp) |
| IR-B | CSE + 寄存器分配(liveness) | 现行(expr_opt.hpp) |
| IR-C | 图 IR + begin_expr/end_expr 融合分析 |
现行(expr_graph.hpp;P1/P2 已落地,§5.3) |
| IR-D | 后端 emitter 抽象(多后端,一份 canonical IR) | 现行(expr_emitter.hpp,仅 GlslEmitter 注册) |
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、CPUeval_expr_impl、GPUeval_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<last_dst>)。 - 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) 重编号)。 - 回归范围:
expr_cpu_test(expr_opt_test/expr_reduce_test等子测试已并入该聚合目标)+expr_gpu_test+ gradcheck 系列 + MNIST / text_train 端到端训练冒烟(CPU + GPU)。
canonicalization 与优化 pass 的验证重点:
- 确定性:同一 spec 多次 canonicalize 得相同结果;跨编译器(Clang/MSVC)key 稳定。
- 语义等价:canonical 前后 CPU 求值结果一致(容差内);优化不改变浮点语义。
- 现有测试全量回归:
expr_cpu_test/expr_gpu_test/ gradcheck 系列 / GPT、MNIST 训练冒烟(CPU + GPU)。 - 上限压力测试:构造超输入/超寄存器用例,验证 CSE + 寄存器分配后落入限制内。
- 闭合世界覆盖:新增优化后,scan 覆盖的路径仍能命中(key 定义在 canonical IR 上,两端一致)。
| 目标 | 状态 |
|---|---|
| 轻量 IR + 优化(IR-A + IR-B) | 现行 |
| 图 IR 融合分析(IR-C) | 现行(expr_graph.hpp,P1/P2,§5.3) |
| + 后端 emitter(IR-D) | 现行(仅 GlslEmitter 注册) |
最大成本集中在确定性 key 验证。现有 ExprSpec 已是合格轻量 IR,直通、简单、确定性是其最大优点;
IR-C 现以真实调用方(P1 机制 + P2 写穿多输出 + Adam/AdamW 接线)接入训练路径,其约束是有真实调用方、可安全融合。
后续演进以"小步演进、不推翻、有真实调用方"为原则推进;IR-C 的完整历史见 docs/history/history.md。