Slang 自动微分 IR 指令族完全解析:从__fwd_diff到ForwardDifferentiate的降级路径与逐指令测试验证
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
本文是 Slang 编译器自动微分(automatic differentiation)IR 指令族的逐指令参考指南,围绕 docs/generated/design/ir-reference/differentiation.md 与对应的逐指令测试包 docs/generated/tests/design/ir-reference/differentiation/ 展开。文章覆盖差分对(differential pair)的构造与投影指令、前向/反向模式翻译指令、旧式桥接(legacy bridge)指令、detachDerivative、no_diff标记与检查点/重物化指令,并给出从用户源码表面到LOWER-TO-IR转储的可观测映射。读完你将掌握:每个微分指令的用户源码入口是什么、在-dump-ir转储中长什么样、哪些指令只由内部 pass 合成而无法从用户程序观察到,以及如何用 FileCheck 为它们编写回归测试。
一、背景:自动微分在 Slang IR 中的位置
Slang 的自动微分机制并不以"给函数加一个求导标志"的方式进入 IR,而是由一组**专门的 IR 指令(opcode)**承载:构造和投影差分对的指令、请求某个函数值前向/反向导数的指令、把历史遗留的"合并式反向函数"桥接到当前"拆分式"表示的指令,以及支撑检查点(checkpointing)和重物化(rematerialization)的指令。这些指令的消费方是编译器的 autodiff pass 族(slang-ir-autodiff*.cpp)、翻译 pass(source/slang/slang-ir-translate.cpp),以及需要在"部分微分完成"的 IR 模块上工作的 emit 路径。
这些指令被放入模块的方式有三类生产者,理解它们有助于判断某条指令"能否被用户代码直接触发":
- 核心模块(core module):所有差分对指令与
detachDerivative的源头。DifferentialPair<T>在 source/slang/core.meta.slang 中声明,其__init通过__intrinsic_op($(kIROp_MakeDifferentialPair))(第 786 行)绑定构造指令,p/v/d属性分别绑定DifferentialPairGetPrimal/DifferentialPairGetDifferential(第 791、797、803 行);DifferentialPtrPair<T>声明对应的指针风味四件套(第 877-894 行)。自由函数diffPair(primal, diff)(source/slang/diff.meta.slang 第 1329 行)是MakeDiffPair的第二种拼写,detach<T>(T x)(第 2081 行)是detachDerivative的源头。 - 语义检查(semantic checking):大多数
TranslateBase指令的来源。SynthesizedFuncDecl(source/slang/slang-ast-decl.h 第 760 行)携带一个uint32_t irOp字段和List<Val*> operands;检查一个可微调用时,source/slang/slang-check-decl.cpp 会合成这类 decl 并把目标指令码写进irOp。降级时 source/slang/slang-lower-to-ir.cpp 第 13858 行读取该字段、逐一降级每个Val操作数,调用emitIntrinsicInst生成指令。 - autodiff pass 自身:其余指令。核心消费者是
TranslationContext::maybeTranslateInst(source/slang/slang-ir-translate.cpp 第 40 行),它把每条TranslateBase指令解析为具体的函数或 witness table,并把结果记忆化在模块的翻译字典中;逐指令的工作分布在 slang-ir-autodiff-fwd.cpp、slang-ir-autodiff-rev.cpp、slang-ir-autodiff-unzip.cpp、slang-ir-autodiff-transpose.cpp、slang-ir-autodiff-primal-hoist.cpp 中。注意当前提交点没有slang-ir-autodiff-transcribe.cpp,前向模式转录代码位于slang-ir-autodiff-fwd.cpp内。
关键事实:本指令族没有任何一条会生成目标代码。finalizeAutoDiffPass(source/slang/slang-ir-autodiff.cpp 第 1174 行)运行processPairTypes把MakeDiffPair改写成MakeStruct、把两个投影改写成字段访问,随后执行removeDetachInsts与removeTypeAnnotations;lowerDiffTypeInfoInsts(source/slang/slang-emit.cpp 第 882 行)会(若存在)把DiffTypeInfo改写成makeTuple。两者都以requiredLoweringPassSet.autodiff为门控,而calcRequiredLoweringPassSet(同文件第 405 行)只要看到任何IRTranslateBase、IRTranslatedTypeBase、IRDifferentialPairTypeBase或IRMakeDifferentialPairBase指令就会置位。唯一的例外是builtinRequirementKey:它可提升(hoistable),可能在特化后作为未被引用的全局存活,而 source/slang/slang-emit-c-like.cpp 第 5338 行的ensureGlobalInst会显式跳过这条纯元数据指令。
二、指令族谱系(Family hierarchy)
整个微分指令族以五个抽象组为骨架(见 docs/generated/design/ir-reference/differentiation.md 的谱系图):
MakeDifferentialPairBase→MakeDiffPair、MakeDiffRefPairDifferentialPairGetDifferentialBase→GetDifferential、GetDifferentialPtrDifferentialPairGetPrimalBase→GetPrimal、GetPrimalRefTranslateBase→ 前向算子、反向算子、旧式桥接算子、合成导数 witness- 未分组指令 →
checkpointObj、loopExitValue、ReportCheckpointStore、四个占位符(LoadReverseGradient、ReverseGradientDiffPairRef、PrimalParamRef、DiffParamRef)、detachDerivative、DiffTypeInfo、builtinRequirementKey
TranslateBase是唯一拥有庞大成员的抽象组;三个 pair 组的存在是为了让消费者可以用一次as<IRMakeDifferentialPairBase>(inst)同时匹配值与指针两种风味——calcRequiredLoweringPassSet和processPairTypes都依赖这一点。
三、差分对构造:MakeDiffPair与MakeDiffRefPair
3.1 指令语义
| Opcode | C++ wrapper | 操作数 | 标志 | AST 源头 | 语义 |
|---|---|---|---|---|---|
MakeDiffPair | IRMakeDifferentialPair | primal, differential | DifferentialPair<T>.__init与diffPair(...),均为__intrinsic_op($(kIROp_MakeDifferentialPair)) | 把值类型 primal 与其 differential 打包 | |
MakeDiffRefPair | IRMakeDifferentialPtrPair | primal, differential | DifferentialPtrPair<T>.__init,__intrinsic_op($(kIROp_MakeDifferentialPtrPair)) | 把指针类型 primal 与其微分指针打包 |
MakeDiffPair的结果类型是IRDifferentialPairType(类型细节归 types.md 的 differentiation types 一节)。它并非 autodiff 内部指令:用户直接写DifferentialPair<float>(x, dx)或diffPair(x, dx)就会得到它——这正是为什么即使模块从未提及fwd_diff/bwd_diff,calcRequiredLoweringPassSet看到任何IRMakeDifferentialPairBase也会标记模块需要 autodiff 终结 pass。
3.2 源码表面与降级
对应测试 make-diff-pair.slang 验证了操作数顺序(primal, differential):
uniform float a; uniform float d; uniform RWStructuredBuffer<float> buf; [ForwardDifferentiable] float f(float x) { return x * x; } [numthreads(1, 1, 1)] void main() { buf[0] = __fwd_diff(f)(DifferentialPair<float>(a, d)).d; }其 FileCheck 断言在func %main内部出现DiffPair(Float, %witness) = MakeDiffPair(%a, %d),即%a在前、%d在后。两个操作数特意使用uniform全局变量,目的是阻止常量折叠(constant folding)把构造运算塌缩掉——这是本指令族观测测试的关键技巧。另一个测试 make-diff-pair-diff-pair-free-function.slang 验证自由函数diffPair(primal, diff)与DifferentialPair<T>构造函数产生同一条指令。
值得注意的文档缺口:differentiation.md的表格把MakeDiffPair的 AST 源头标为(synthesized),但事实上用户手写的DifferentialPair<T>(...)构造函数在任何前向模式微分函数的调用点都会自然产生它——测试包把这一点当作可观测声明处理,并把这个"AST origin 列与实际不符"记录为文档缺口。
四、差分对投影:GetPrimal/GetDifferential/GetPrimalRef/GetDifferentialPtr
| Opcode | C++ wrapper | 操作数 | 语义 |
|---|---|---|---|
GetDifferential | IRDifferentialPairGetDifferential | pair | 读取DifferentialPair的微分分量(源码为.d) |
GetDifferentialPtr | IRDifferentialPtrPairGetDifferential | †min=1 | 读取DifferentialPtrPair的微分指针 |
GetPrimal | IRDifferentialPairGetPrimal | pair | 读取DifferentialPair的 primal 分量(源码为.p/.v) |
GetPrimalRef | IRDifferentialPtrPairGetPrimal‡ | ptrPair | 读取DifferentialPtrPair的 primal 指针 |
(‡ 表示 C++ wrapper 为手写而非由 Lua 条目生成;† 表示 Lua 条目只声明了min_operands,wrapper 没有具名访问器,消费者直接用getOperand(i)。)
四个投影是MakeDiffPair的逆操作,在 IR 中由前向模式转录器在"只需要某个 pair 的一个分量"时发出。源码层面它们就是DifferentialPair<T>的.d与.p/.v属性 getter。lowerPairAccess(source/slang/slang-ir-autodiff-pairs.cpp 第 480 行)把全部四个投影改写成降级后 pair 结构体上的字段访问,lowerMakePair(第 448 行)把两个构造函数改写成makeStruct,两者都由processPairTypes(第 592 行)驱动。在改写之前没有任何 pass 会把GetPrimal(MakeDiffPair(a, b))折叠回a。
一个有趣的实现细节:四个投影对同一逻辑角色的操作数命名不一致——GetDifferential/GetPrimal命名为pair,GetPrimalRef命名为ptrPair,GetDifferentialPtr没有命名。实际使用中全部通过抽象基类的getBase()访问。
测试 get-primal.slang 用CHECK-COUNT-2断言两个.p/.vgetter 产生两条Float = GetPrimal(...)行,证明.v是.p的别名而非独立指令;get-primal-ref-get-differential-ptr.slang 覆盖指针风味。测试 get-primal-of-make-diff-pair-not-folded.slang 则验证"投影不会被折叠回构造操作数"这一行为。
五、微分算子:请求而非结果
TranslateBase下的所有指令都是请求(request)而非结果。每条请求由TranslationContext::maybeTranslateInst解析,首先查询模块的翻译字典(IRCompilerDictionary,由initializeTranslationDictionary创建)并记忆化产物,因此每条请求每个模块最多翻译一次。去重以请求本身(opcode + 操作数)为键,所以决定身份的是基础函数而不是调用点:
buf[0] = __fwd_diff(f)(DifferentialPair<float>(a, d)).d; buf[1] = __fwd_diff(f)(DifferentialPair<float>(d, a)).p;无论__fwd_diff(f)写多少次、每次传什么参数,都降级为同一条let %fwd = ForwardDifferentiate(%f),两个call指令共享它。
5.1 前向模式
| Opcode | 操作数 | 标志 | AST 源头 | 语义 |
|---|---|---|---|---|
ForwardDifferentiate | baseFn | H | ForwardDifferentiateExpr(__fwd_diff(...))经visitForwardDifferentiateExpr;也有ForwardDifferentiateVal | 请求函数值的前向模式(JVP)导数 |
TrivialForwardDifferentiate | †min=1 | H | SynthesizedFuncDeclfwd_diff,由checkDifferentiableCallableCommon为[TreatAsDifferentiable]/[HasTrivialForwardDerivative]创建 | 请求一个"运行 primal 并返回零输出微分、忽略传入切向量"的导数 |
ForwardDifferentiatePropagate | †min=1 | H | 无 AST 源头——由 slang-ir-autodiff-unzip.cpp 第 419 行发出 | 解压反向模式函数体时使用的前向模式 propagate 函数 |
(H 表示 hoistable,即相同请求会去重为单一 IR 值。)
ForwardDifferentiate(baseFn)是用户唯一能直接从表达式产生的微分算子:__fwd_diff(f)解析为ForwardDifferentiateExpr,visitForwardDifferentiateExpr(source/slang/slang-lower-to-ir.cpp 第 5878 行)发出指令;把ForwardDifferentiateVal存入 witness table 时也会发出(第 2492 行)。测试 forward-differentiate.slang 锚定let %FWD : Func(DiffPair(Float, %w), DiffPair(Float, %w)) = ForwardDifferentiate(%f)——对float -> float单参数函数,结果类型正是"对差分对签名"的 JVP 函数类型——并要求func %main内的call恰好指向该值。测试 forward-differentiate-hoisted-dedupe.slang 用CHECK-NOT: = ForwardDifferentiate(%f)夹在两个call [[FWD]](之间,验证两个语法上独立的__fwd_diff(f)表达式只产生一个 IR 值,并把断言用### LOWER-TO-IR:与###边界限定在 LOWER-TO-IR 转储段内(后面的逐 pass 转储会重复整个模块)。
5.2 反向模式
反向模式的完整指令集包括:根请求BackwardDifferentiate,以及三个函数值相位BackwardDifferentiatePrimal(primal 相位,计算并返回 propagate 相位需要的值)、BackwardDifferentiatePropagate(propagate 相位,消费记录下来的上下文和输出伴随值,产生输入伴随值)、BackwardRemat(重物化相位,从最小上下文重算 primal 值而非读取完整检查点),以及各自的Trivial*平凡版本。这些指令都不会到达目标代码,但从它们构建的函数和类型会——并且以生成名字出现在每个源码级文本目标(HLSL、GLSL、Metal、WGSL、CUDA、C++)中。SPIR-V 是例外:其输出不为导数函数携带OpName,SPIR-V 阅读器必须按结构而非名字匹配。
命名规律(可被文本目标测试观测):source/slang/slang-ir-autodiff-rev.cpp 的generateName给 propagate 函数前缀s_bwdProp_(第 405、726 行)、给完整中间上下文结构体前缀s_bwdCallableCtx_(第 314、727 行),于是f产生s_bwdProp_f与携带被提升 primal 状态的s_bwdCallableCtx_f;前向模式则是 source/slang/slang-ir-autodiff-fwd.cpp 第 2271 行构建s_fwd_<orig>。对应测试 reverse-mode-emitted-name-prefixes.slang 与 forward-derivative-emitted-name-prefix.slang 逐文本目标(hlsl、glsl、spirv-asm、metal、wgsl、cuda、cpp)验证这些前缀。
5.3 旧式桥接(Legacy bridge)与__bwd_diff的真实降级
这是本文最值得注意的"表象与实现不一致"之处。用户写__bwd_diff(f)(或别名bwd_diff(f))时,LOWER-TO-IR 阶段实际发出的是LegacyBackwardDifferentiate,而不是BackwardDifferentiate:
[Differentiable] float f(float x) { return x * x; } [numthreads(1, 1, 1)] void main() { DifferentialPair<float> dp = DifferentialPair<float>(a, 0.0); __bwd_diff(f)(dp, 1.0); buf[0] = dp.d; }降级为let %bwd : Func(Void, BorrowInOutParam(DiffPair(Float, %w)), Float) = LegacyBackwardDifferentiate(%apply_bwd, %remat, %ctx_t)——三个操作数依次是 bwd 可调用函数、remat 函数和中间上下文类型。测试 legacy-backward-differentiate.slang 用 FileCheck 变量依次捕获BackwardDifferentiatePrimal(%f)、BackwardDifferentiatePropagate(%f)、BackwardRemat(%f)三个相位请求,再要求LegacyBackwardDifferentiate按 apply_bwd、remat、propagate 的顺序列出它们——顺序错乱会导致测试失败。测试 bwd-diff-does-not-emit-backward-differentiate.slang 则用CHECK-NOT: = BackwardDifferentiate(证明该 opcode 从未由用户表面产生(=前缀是为了避免被LegacyBackwardDifferentiate、TrivialBackwardDifferentiate等含子串的拼写误触发)。现代的BackwardDifferentiate指令由 unzip pass 在管线后期把旧式形式转换而来。这个"文档把__bwd_diff列为BackwardDifferentiate的 AST 源头,但 LOWER-TO-IR 实际发LegacyBackwardDifferentiate"的差异被测试包如实记录为文档缺口。
桥接族的其余指令(BackwardFromLegacyBwdDiffFunc与三个投影BackwardPrimalFromLegacyBwdDiffFunc、BackwardRematFromLegacyBwdDiffFunc、BackwardPropagateFromLegacyBwdDiffFunc)共享(targetFunc, legacyBwdDiffFunc)操作数对,各自从解析出的五元组中取一个元素,测试 legacy-bridge-projections.slang 覆盖。LegacyBackwardDifferentiate不遵循该形状:它携带三个函数,由 source/slang/slang-ir-autodiff-rev.cpp 第 759-761 行的maybeTranslateLegacyBackwardDerivative作为操作数 0、1、2 读取。
5.4 合成导数 witness 与实验性__func_extension
FunctionCopy、SynthesizedForwardDerivativeWitnessTable、SynthesizedBackwardDerivativeWitnessTable、MakeIDifferentiableWitness、IdentityRemat等指令服务于"为高阶导数合成 witness table"与"把已有函数命名位合成导数成员的函数体"。其中IdentityRemat背后的__func_extension表面(对应_funcExtensionApply/_funcExtensionBackwardDiff源头)是实验特性:除非传入-experimental-feature,visitFuncExtensionDecl(source/slang/slang-check-decl.cpp 第 16269-16275 行)会对任何__func_extension声明给出诊断 W30131(func-extension-requires-experimental-feature)。注意这是一个警告而不是错误——声明会被丢弃,编译不会立即停止,而是在后续引用该声明处才以另一个更不明显的错误失败。核心模块的元代码享有豁免,这正是它能用同一语法附加条件导数 witness 的原因,因此IdentityRemat完全没有非实验性的用户表面。对应测试 func-extension-requires-experimental-feature.slang 以诊断形式观测这一点。
六、detachDerivative与no_diff的两种形态
detachDerivative(value)原样返回操作数,但阻断导数穿过它传播——对 autodiff 系统而言该值表现为常量。它有两条源码拼写:
- 核心模块
detach<T>(T x)内建函数。测试 detach-derivative.slang 在[Differentiable]函数体内写return x * detach(x),断言let %DET : Float = detachDerivative(%x)且随后= mul(%x, [[DET]])——同一个值既有 detached 又有未 detached 的使用。注意 opcode 拼写是小写 d(detachDerivative),不是DetachDerivative。 - 对表达式应用
no_diff/[TreatAsDifferentiable]。visitTreatAsDifferentiableExpr(source/slang/slang-lower-to-ir.cpp 第 5890 行)会把物化后的IRLoad包进detachDerivative,使no_diff在局部数组索引与资源索引上行为一致(见测试 no-diff-expression-detach.slang)。
但no_diff用在参数声明上是完全不同的构造,不会产生任何detachDerivative指令。在那里它是修饰符而非表达式:检查阶段把NoDiffModifierVal从参数类型移到ParamDecl上成为NoDiffModifier(source/slang/slang-check-decl.cpp 第 6766-6781 行,普通 var decl 的同样移动在第 2876-2878 行),可微性查询从任一位置读取它(doesTypeHaveNoDiffModifier,第 5337、5338 行)。测试 no-diff-param-produces-no-detach.slang 验证了边界:函数scaleBy(float x, no_diff int k)被微分时,LOWER-TO-IR 段内CHECK-NOT: detachDerivative,取而代之的是模块作用域let %ND : Void = no_diff与入口块上的param %k : Attributed(Int, %ND)——no_diff作为类型的一部分存活,而不是作为指令。差分完成后removeDetachInsts(由finalizeAutoDiffPass调用)会删除所有detachDerivative。
七、五元组翻译机制:相位由元组下标携带
反向模式的精髓在于"单一根请求":BackwardDifferentiate翻译为makeTuple的五个元素——primal 函数、remat 函数、propagate 函数、完整中间上下文类型、最小上下文类型。而BackwardDifferentiatePrimal、BackwardRemat、BackwardDifferentiatePropagate、BackwardDiffIntermediateContextType、BackwardDiffMinimalContextType各自通过为同一基础函数合成一个单操作数BackwardDifferentiate、翻译之、再返回元组元素 0 到 4 来解析(source/slang/slang-ir-translate.cpp 第 164-198 行)。Trivial*族(第 199-237 行)与*FromLegacyBwdDiffFunc族(第 244-287 行)以TrivialBackwardDifferentiate与BackwardFromLegacyBwdDiffFunc为根遵循同一形状。这就是各相位指令除基础函数外不需要任何操作数的原因:相位由元组下标携带。两个上下文类型属于 Type 族(归 types.md 的BackwardDiffIntermediateContextType一节),尽管解析发生在与函数值相位相同的 switch 中。TrivialBackwardDifferentiate与BackwardFromLegacyBwdDiffFunc永远不来自 AST 声明——它们只是翻译 pass 为自己构建的临时根。
BackwardDifferentiate的操作数形状值得小心:Lua 条目声明min_operands = 3,手写 wrapper 声明getApplyFunc()、getContextType()、getBwdPropFunc()读取操作数 0、1、2——但当前提交点所有生产者都只构建一个操作数。IRBuilder::emitBackwardDifferentiateInst(source/slang/slang-ir.cpp 第 3759 行)只传单个baseFn,翻译 pass 合成的根也只传一个操作数。三个访问器没有任何调用者。min_operands变为IROpInfo::fixedArgCount(source/slang/slang-ir.h 第 101 行),而没有任何代码读取它。此外,用户可见的__bwd_diff形式根本到不了 IR:语义检查更早地解析了BackwardDifferentiateExpr,visitBackwardDifferentiateExpr(source/slang/slang-lower-to-ir.cpp 第 5972 行)直接调用SLANG_UNEXPECTED。[PrimalSubstitute]/[PrimalSubstituteOf]同理——它们是 source/slang/diff.meta.slang 中声明的 AST 级属性,不存在PrimalSubstituteopcode(测试 primal-substitute-has-no-opcode.slang 覆盖)。
八、内置需求键:builtinRequirementKey与BuiltinRequirementDecoration
IDifferentiable/IBackwardDifferentiable/IBwdCallable接口被编译器识别,autodiff pass 需要按角色(BuiltinRequirementKind::DifferentialType、DAddFunc、DifferentialWitness……)而不是按条目位置查找需求。builtinRequirementKey是承载该角色的可提升需求键指令,BuiltinRequirementDecoration是查找辅助扫描的配套装饰。与普通key/StructKey(每个需求声明一个独立全局符号,靠key_<mangled>链接名跨模块统一)不同,内置键可提升且按 kind 操作数去重——同一逻辑需求无论从规范接口约束、构建类型Differential时合成的约束、还是跨预编译核心模块边界引用,都解析为同一个键指令,且不需要任何链接装饰,因为身份来自操作数。
getInterfaceRequirementKey(source/slang/slang-lower-to-ir.cpp 第 1726 行)从需求的BuiltinRequirementModifier计算角色——例如当被建键的是关联一致性而非关联类型时,把DifferentialType提升为DifferentialWitness——然后调用getBuiltinRequirementKey并附加BuiltinRequirementDecoration(第 1814-1820 行)。getInterfaceEntryByBuiltinRequirement(source/slang/slang-ir-autodiff.cpp 第 229 行)扫描该装饰按角色找条目。由于内置需求可能经动态分派到达,GetDispatcher的lookupKey操作数被类型化为普通IRInst而非IRStructKey(见 source/slang/slang-ir-insts.lua 第 3222-3225 行注释)。相关测试包括 builtin-requirement-decoration.slang(键与装饰携带同一 kind)、builtin-requirement-key-deduped.slang(可提升与按构造去重)与 builtin-requirement-key-differential-witness.slang(角色提升产生不同键)。
九、检查点与重物化:checkpointObj、loopExitValue、ReportCheckpointStore
反向模式 autodiff 经常需要在 primal 值不再自然存活的位置使用它,这些指令标记候选值,让 primal-hoisting pass 在"保持值存活"与"重算它"之间决策:
checkpointObj(value)自身不存储任何东西;它制作一个值的独立副本,使循环体内与循环体外的使用可以被独立提升——这正是它包裹的 primal 值在 primal-hoisting pass 决定检查点还是重算之前所需要的。unzip pass 用它包裹 primal 返回值与最小上下文值,source/slang/slang-ir-autodiff-primal-hoist.cpp 第 2778 行消费它。loopExitValue记录 SSA 变量在循环出口的值,供反向模式读取(由 slang-ir-autodiff-primal-hoist.cpp 第 393 行发出)。ReportCheckpointStore(storedType, originalFunc, storeRef)是同一机制的诊断通道,只有传入-report-checkpoint-intermediates(source/slang/slang-options.cpp 第 622 行)才会打印。报告是 id 为-1的standalone_note(source/slang/slang-diagnostics.lua 第 5916 行),首行点名originalFunc与总大小:
note: checkpointing context of 8 bytes associated with: 'f'随后每个被存值一个可变参数 span,渲染为<size> bytes (<typeName>),同一类型多个实例时渲染为<count> instances of <size> bytes (<typeName>)——storedType操作数就在这里显现。其storeRef操作数是弱引用:若 store 后被消除,操作数变为Poison,source/slang/slang-emit.cpp 第 271 行的报告遍历会跳过该条目。DCE 在isWeakReferenceOperand(source/slang/slang-ir-dce.cpp 第 652 行)中赋予该操作数弱引用地位,被删除的 store 以 poison 替换而不是被标记器拖住存活;标记器本身则靠shouldInstBeLiveIfParentIsLive(第 531 行)中保守的mightHaveSideEffects测试存活,最后由报告遍历移除。测试 report-checkpoint-store-diagnostic.slang 观测该报告。
十、无法从用户表面观测的指令(Untested claims)
以下指令在文档中标注为(synthesized),只由内部 autodiff IR pass 产生,在 LOWER-TO-IR 阶段没有任何自然用户表面。观测它们需要检查后期 pass 的 IR 转储,超出了逐指令参考测试包的范围,因此被记录在测试包 README 的## Untested claims一节而非写成测试:
MakeDiffRefPair、GetDifferentialPtr、GetPrimalRef——指针风味的构造与投影,用于 autodiff pass 内部管线。ForwardDifferentiatePropagate、TrivialForwardDifferentiate——由 unzip / 转录管线合成,__fwd_diff在 LOWER-TO-IR 不产生它们。BackwardDifferentiate本身——文档把__bwd_diff列为它的 AST 源头,但 LOWER-TO-IR 实际发LegacyBackwardDifferentiate,现代指令在 LOWER-TO-IR 阶段视为超范围。BackwardDifferentiatePrimal/BackwardDifferentiatePropagate/BackwardRemat/TrivialBackwardDifferentiate*——由 unzip pass 产生。- 四个旧式桥接提取指令
BackwardFromLegacyBwdDiffFunc族——合成。 FunctionCopy、SynthesizedForwardDerivativeWitnessTable、SynthesizedBackwardDerivativeWitnessTable、MakeIDifferentiableWitness等导数 witness 合成指令——内部。LoadReverseGradient、ReverseGradientDiffPairRef、PrimalParamRef、DiffParamRef——autodiff 临时占位符,不会存活过拆分/反向传播 pass。DiffTypeInfo——合成类型信息容器。checkpointObj、loopExitValue、ReportCheckpointStore——由反向模式 pass 插入的检查点标记,LOWER-TO-IR 无表面形态。
测试包 README 的 Untested claims 表格为每条给出了理由分类:implementation-detail(实现细节,任何 Slang 程序都无法区分)、internal-source-fact(关于编译器源码结构的陈述)、link-stage-only(只存在于链接/翻译阶段,晚于 LOWER-TO-IR 观测点)、unsupported-on-target(SPIR-V 汇编无OpName,生成名字不可观测)。例如:四个占位符各有IRBuilder辅助函数(source/slang/slang-ir-insts.h 第 4144-4147 行声明,source/slang/slang-ir.cpp 第 5503-5541 行定义)但在source/中没有任何调用者;LoadReverseGradient与ReverseGradientDiffPairRef仍出现在两个消费者 switch 中(slang-ir-autodiff-transpose.cpp 第 1569 行、IRInst::mightHaveSideEffects第 9456 行),而PrimalParamRef/DiffParamRef既无生产者也无消费者。DiffTypeInfo有消费者无生产者:__hasDiffTypeInfo(T)解析为HasDiffTypeInfoConstraintDecl并求解为HasDiffTypeInfoWitness,但该 witness 降级为 void 值、约束发出 void 参数(slang-lower-to-ir.cpp 第 2670 行),所以DiffTypeInfo指令永远不会被构建(测试 has-diff-type-info-constraint-void-param.slang)。SynthesizedBackwardDerivativeWitnessTableFromLegacyBwdDiffFunc最彻底:其稳定名字字符串在source/中除 Lua 定义与稳定名表外无处出现。
十一、逐指令测试方法论:从声明到可观测断言
测试包的标准观测形式(定义于 docs/generated/tests/design/ir-reference/differentiation/_prompt.md)是一条//TEST指令:
//TEST:SIMPLE(filecheck=CHECK):-target spirv-asm -dump-ir -o - -entry main -stage compute三条通用规则缺一不可(也是测试作者反复踩坑后沉淀的教训):
-dump-ir必须搭配-target <文本目标>和-o -,否则 IR 转储会被目标文本污染,无法干净地送到 stdout。pipeline_stage=lower——这些是 LOWER-TO-IR 观测。- CHECK 模式锚定用户命名符号(
func %main、func %f、let %fwd_diff、let %bwd_diff)。autodiff 转储在用户 IR 之前有一个非常大的前导(每个IDifferentiable/IForwardDifferentiable/IBackwardDifferentiable接口、每个 key、每类型内建 witness),不锚定用户符号的话前导就可能意外满足模式。
另有四条观测纪律:
- 输出必须逃逸 DCE:写入
RWStructuredBuffer<T>,纯内部计算会在转储前被移除(测试 pair-opcodes-produce-no-target-code.slang 同时演示了"pair 在 emit 时被溶解":它对 hlsl、glsl、spirv-asm、metal、wgsl、cuda、cpp 七个目标各断言{{GetPrimal|GetDifferential|DiffPair}}不出现,并用COMPARE_COMPUTE -cpu验证p.p与p.d的值 3.0 / 6.0 在改写后原样存活)。 MakeDiffPair/detach的操作数必须非恒定(用uniform全局),否则常量折叠会塌缩运算。- 不断言 C++ wrapper 结构体身份(
IRMakeDifferentialPair、IRForwardDifferentiate等)——转储显示的是驼峰 opcode 拼写(MakeDiffPair、ForwardDifferentiate、GetPrimal、GetDifferential)。 - 每个测试函数必须标注
[ForwardDifferentiable](前向观测)或[BackwardDifferentiable]/[Differentiable](反向观测),否则对__fwd_diff(f)的调用是检查器错误,根本不发 IR。
测试包的期望结构:1 个README.md(含## Untested claims收纳所有 slangc 不可观测项)+ 10 到 18 个.slang测试文件,每个文件聚焦一条可观测 opcode(或一条可观测的表面到 opcode 降级映射),紧密相关的观测归入同一文件。README 的## Functional coverage表格把每条可观测声明(C1–C115)映射到意图类型(functional / boundary / negative / expansion)、文档锚点与具体测试文件,## Doc gaps observed则如实记录文档与实现的差异。
十二、文档缺口速览(Doc gaps)
测试包 README 记录了五类已确认的文档缺口,阅读differentiation.md时值得留意:
BackwardDifferentiate的 AST 源头误导:反向模式表格给出BackwardDifferentiateVal源头,但正文随即说__bwd_diff根本不到 IR——读者无从得知"__bwd_diff(f)到底变成了什么"(答案是合成成员上的LegacyBackwardDifferentiate值)。no_diff双义未警示:文档列出两条detachDerivative源码拼写,但未警告参数声明上的no_diff是不同构造、不产生任何指令(对应测试 no-diff-param-produces-no-detach.slang 已钉死该边界)。- 合成入口点未映射到用户属性:AST 源头列命名内部检查入口(
checkDifferentiableCallableCommon、trySynthesizeDiffFuncRequirementWitness等)而不命名触发它们的用户级属性([Differentiable]/[BackwardDifferentiable]、[TreatAsDifferentiable]、[BackwardDerivative(f)]、__func_extension __apply)。 - 去重缺少示例:文档说相同翻译请求"去重为单一 IR 值"但无示例;
forward-differentiate-hoisted-dedupe.slang正是为补上这个示例而存在。 processPairTypes改写缺少目标文本说明:改写后的结构体只在"微分调用跨函数边界传 pair"时存活到目标文本,纯局部 pair 会在 emit 前被标量化掉。
十三、结语
Slang 的自动微分不是"黑盒变换",而是一张由指令、生产者、消费者和观测规则构成的精确图谱:用户表面的DifferentialPair<T>(p, d)、.p/.d、__fwd_diff(f)、__bwd_diff(f)、detach(x)、no_diff各自映射到MakeDiffPair、GetPrimal/GetDifferential、ForwardDifferentiate、LegacyBackwardDifferentiate、detachDerivative与Attributed(T, %no_diff);而现代反向模式的五个相位由翻译字典中一次makeTuple的下标携带。理解"哪些指令可从源码观测、哪些只由内部 pass 合成"这一分层,是阅读 autodiff pass 源码(source/slang/slang-ir-autodiff-*.cpp)、调试部分微分 IR 模块或为指令族新增回归测试的起点——逐指令测试包 docs/generated/tests/design/ir-reference/differentiation/ 提供了可直接复用的观测模板与纪律。进一步阅读:指令模式与"新增 opcode"工作流见 ir-instructions.md,差分对与上下文类型见 types.md,autodiff 设计动机见 autodiff.md。
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考