Slang 自动微分 IR 指令族完全解析:从 `__fwd_diff` 到 `ForwardDifferentiate` 的降级路径与逐指令测试验证
2026/9/19 10:42:11 网站建设 项目流程

Slang 自动微分 IR 指令族完全解析:从__fwd_diffForwardDifferentiate的降级路径与逐指令测试验证

【免费下载链接】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)指令、detachDerivativeno_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 行)运行processPairTypesMakeDiffPair改写成MakeStruct、把两个投影改写成字段访问,随后执行removeDetachInstsremoveTypeAnnotationslowerDiffTypeInfoInsts(source/slang/slang-emit.cpp 第 882 行)会(若存在)把DiffTypeInfo改写成makeTuple。两者都以requiredLoweringPassSet.autodiff为门控,而calcRequiredLoweringPassSet(同文件第 405 行)只要看到任何IRTranslateBaseIRTranslatedTypeBaseIRDifferentialPairTypeBaseIRMakeDifferentialPairBase指令就会置位。唯一的例外是builtinRequirementKey:它可提升(hoistable),可能在特化后作为未被引用的全局存活,而 source/slang/slang-emit-c-like.cpp 第 5338 行的ensureGlobalInst会显式跳过这条纯元数据指令。

二、指令族谱系(Family hierarchy)

整个微分指令族以五个抽象组为骨架(见 docs/generated/design/ir-reference/differentiation.md 的谱系图):

  • MakeDifferentialPairBaseMakeDiffPairMakeDiffRefPair
  • DifferentialPairGetDifferentialBaseGetDifferentialGetDifferentialPtr
  • DifferentialPairGetPrimalBaseGetPrimalGetPrimalRef
  • TranslateBase→ 前向算子、反向算子、旧式桥接算子、合成导数 witness
  • 未分组指令 →checkpointObjloopExitValueReportCheckpointStore、四个占位符(LoadReverseGradientReverseGradientDiffPairRefPrimalParamRefDiffParamRef)、detachDerivativeDiffTypeInfobuiltinRequirementKey

TranslateBase是唯一拥有庞大成员的抽象组;三个 pair 组的存在是为了让消费者可以用一次as<IRMakeDifferentialPairBase>(inst)同时匹配值与指针两种风味——calcRequiredLoweringPassSetprocessPairTypes都依赖这一点。

三、差分对构造:MakeDiffPairMakeDiffRefPair

3.1 指令语义

OpcodeC++ wrapper操作数标志AST 源头语义
MakeDiffPairIRMakeDifferentialPairprimal, differentialDifferentialPair<T>.__initdiffPair(...),均为__intrinsic_op($(kIROp_MakeDifferentialPair))把值类型 primal 与其 differential 打包
MakeDiffRefPairIRMakeDifferentialPtrPairprimal, differentialDifferentialPtrPair<T>.__init__intrinsic_op($(kIROp_MakeDifferentialPtrPair))把指针类型 primal 与其微分指针打包

MakeDiffPair的结果类型是IRDifferentialPairType(类型细节归 types.md 的 differentiation types 一节)。它并非 autodiff 内部指令:用户直接写DifferentialPair<float>(x, dx)diffPair(x, dx)就会得到它——这正是为什么即使模块从未提及fwd_diff/bwd_diffcalcRequiredLoweringPassSet看到任何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

OpcodeC++ wrapper操作数语义
GetDifferentialIRDifferentialPairGetDifferentialpair读取DifferentialPair的微分分量(源码为.d
GetDifferentialPtrIRDifferentialPtrPairGetDifferentialmin=1读取DifferentialPtrPair的微分指针
GetPrimalIRDifferentialPairGetPrimalpair读取DifferentialPair的 primal 分量(源码为.p/.v
GetPrimalRefIRDifferentialPtrPairGetPrimalptrPair读取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命名为pairGetPrimalRef命名为ptrPairGetDifferentialPtr没有命名。实际使用中全部通过抽象基类的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 源头语义
ForwardDifferentiatebaseFnHForwardDifferentiateExpr__fwd_diff(...))经visitForwardDifferentiateExpr;也有ForwardDifferentiateVal请求函数值的前向模式(JVP)导数
TrivialForwardDifferentiatemin=1HSynthesizedFuncDeclfwd_diff,由checkDifferentiableCallableCommon[TreatAsDifferentiable]/[HasTrivialForwardDerivative]创建请求一个"运行 primal 并返回零输出微分、忽略传入切向量"的导数
ForwardDifferentiatePropagatemin=1H无 AST 源头——由 slang-ir-autodiff-unzip.cpp 第 419 行发出解压反向模式函数体时使用的前向模式 propagate 函数

(H 表示 hoistable,即相同请求会去重为单一 IR 值。)

ForwardDifferentiate(baseFn)是用户唯一能直接从表达式产生的微分算子:__fwd_diff(f)解析为ForwardDifferentiateExprvisitForwardDifferentiateExpr(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 从未由用户表面产生(=前缀是为了避免被LegacyBackwardDifferentiateTrivialBackwardDifferentiate等含子串的拼写误触发)。现代的BackwardDifferentiate指令由 unzip pass 在管线后期把旧式形式转换而来。这个"文档把__bwd_diff列为BackwardDifferentiate的 AST 源头,但 LOWER-TO-IR 实际发LegacyBackwardDifferentiate"的差异被测试包如实记录为文档缺口。

桥接族的其余指令(BackwardFromLegacyBwdDiffFunc与三个投影BackwardPrimalFromLegacyBwdDiffFuncBackwardRematFromLegacyBwdDiffFuncBackwardPropagateFromLegacyBwdDiffFunc)共享(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

FunctionCopySynthesizedForwardDerivativeWitnessTableSynthesizedBackwardDerivativeWitnessTableMakeIDifferentiableWitnessIdentityRemat等指令服务于"为高阶导数合成 witness table"与"把已有函数命名位合成导数成员的函数体"。其中IdentityRemat背后的__func_extension表面(对应_funcExtensionApply/_funcExtensionBackwardDiff源头)是实验特性:除非传入-experimental-featurevisitFuncExtensionDecl(source/slang/slang-check-decl.cpp 第 16269-16275 行)会对任何__func_extension声明给出诊断 W30131(func-extension-requires-experimental-feature)。注意这是一个警告而不是错误——声明会被丢弃,编译不会立即停止,而是在后续引用该声明处才以另一个更不明显的错误失败。核心模块的元代码享有豁免,这正是它能用同一语法附加条件导数 witness 的原因,因此IdentityRemat完全没有非实验性的用户表面。对应测试 func-extension-requires-experimental-feature.slang 以诊断形式观测这一点。

六、detachDerivativeno_diff的两种形态

detachDerivative(value)原样返回操作数,但阻断导数穿过它传播——对 autodiff 系统而言该值表现为常量。它有两条源码拼写:

  1. 核心模块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
  2. 对表达式应用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 函数、完整中间上下文类型、最小上下文类型。而BackwardDifferentiatePrimalBackwardRematBackwardDifferentiatePropagateBackwardDiffIntermediateContextTypeBackwardDiffMinimalContextType各自通过为同一基础函数合成一个单操作数BackwardDifferentiate、翻译之、再返回元组元素 0 到 4 来解析(source/slang/slang-ir-translate.cpp 第 164-198 行)。Trivial*族(第 199-237 行)与*FromLegacyBwdDiffFunc族(第 244-287 行)以TrivialBackwardDifferentiateBackwardFromLegacyBwdDiffFunc为根遵循同一形状。这就是各相位指令除基础函数外不需要任何操作数的原因:相位由元组下标携带。两个上下文类型属于 Type 族(归 types.md 的BackwardDiffIntermediateContextType一节),尽管解析发生在与函数值相位相同的 switch 中。TrivialBackwardDifferentiateBackwardFromLegacyBwdDiffFunc永远不来自 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:语义检查更早地解析了BackwardDifferentiateExprvisitBackwardDifferentiateExpr(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 覆盖)。

八、内置需求键:builtinRequirementKeyBuiltinRequirementDecoration

IDifferentiable/IBackwardDifferentiable/IBwdCallable接口被编译器识别,autodiff pass 需要按角色BuiltinRequirementKind::DifferentialTypeDAddFuncDifferentialWitness……)而不是按条目位置查找需求。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 行)扫描该装饰按角色找条目。由于内置需求可能经动态分派到达,GetDispatcherlookupKey操作数被类型化为普通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(角色提升产生不同键)。

九、检查点与重物化:checkpointObjloopExitValueReportCheckpointStore

反向模式 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 为-1standalone_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一节而非写成测试:

  • MakeDiffRefPairGetDifferentialPtrGetPrimalRef——指针风味的构造与投影,用于 autodiff pass 内部管线。
  • ForwardDifferentiatePropagateTrivialForwardDifferentiate——由 unzip / 转录管线合成,__fwd_diff在 LOWER-TO-IR 不产生它们。
  • BackwardDifferentiate本身——文档把__bwd_diff列为它的 AST 源头,但 LOWER-TO-IR 实际发LegacyBackwardDifferentiate,现代指令在 LOWER-TO-IR 阶段视为超范围。
  • BackwardDifferentiatePrimal/BackwardDifferentiatePropagate/BackwardRemat/TrivialBackwardDifferentiate*——由 unzip pass 产生。
  • 四个旧式桥接提取指令BackwardFromLegacyBwdDiffFunc族——合成。
  • FunctionCopySynthesizedForwardDerivativeWitnessTableSynthesizedBackwardDerivativeWitnessTableMakeIDifferentiableWitness等导数 witness 合成指令——内部。
  • LoadReverseGradientReverseGradientDiffPairRefPrimalParamRefDiffParamRef——autodiff 临时占位符,不会存活过拆分/反向传播 pass。
  • DiffTypeInfo——合成类型信息容器。
  • checkpointObjloopExitValueReportCheckpointStore——由反向模式 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/中没有任何调用者;LoadReverseGradientReverseGradientDiffPairRef仍出现在两个消费者 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

三条通用规则缺一不可(也是测试作者反复踩坑后沉淀的教训):

  1. -dump-ir必须搭配-target <文本目标>-o -,否则 IR 转储会被目标文本污染,无法干净地送到 stdout。
  2. pipeline_stage=lower——这些是 LOWER-TO-IR 观测。
  3. CHECK 模式锚定用户命名符号func %mainfunc %flet %fwd_difflet %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.pp.d的值 3.0 / 6.0 在改写后原样存活)。
  • MakeDiffPair/detach的操作数必须非恒定(用uniform全局),否则常量折叠会塌缩运算。
  • 不断言 C++ wrapper 结构体身份IRMakeDifferentialPairIRForwardDifferentiate等)——转储显示的是驼峰 opcode 拼写(MakeDiffPairForwardDifferentiateGetPrimalGetDifferential)。
  • 每个测试函数必须标注[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 源头列命名内部检查入口(checkDifferentiableCallableCommontrySynthesizeDiffFuncRequirementWitness等)而不命名触发它们的用户级属性([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各自映射到MakeDiffPairGetPrimal/GetDifferentialForwardDifferentiateLegacyBackwardDifferentiatedetachDerivativeAttributed(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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询