Blog Edition 文本附件:保留技术内容,仅适配路径/链接与运行命令。 # Autograd 实现设计问题复盘 以下结论针对 HIPS/autograd `f53a217...`,源码事实与跨框架概念比较分开表述。 ## 1. 为什么使用 operator overloading / tracing,而不是 symbolic tree? `trace` 直接运行 Python 函数,ArrayBox 运算符与 wrapped primitives 只记录实际发生的连续变换。因此普通 `if`、循环、递归、closure 由 Python 决定,不需要先翻译成 symbolic mini-language。代价是图对应某次具体执行,且难做完整静态优化。 ## 2. graph 显式还是隐式? 没有中央 `Graph` 类。reverse graph 由每个 `VJPNode.parents` 的引用隐式形成;从 end node 即可遍历全部相关 ancestors。 ## 3. intermediate value 存在哪里? trace 进行时当前值位于 `Box._value`。VJP maker 在 node construction 阶段收到 `value/args/kwargs`,生成的 closure 只捕获规则实际引用的 forward state。不是所有 intermediate 都被一个统一 tape 数组保存。 ## 4. backward rule 存在哪里? 全局 `primitive_vjps` 将 primitive 映射到 maker;每个 `VJPNode` 在构造时调用 maker,得到本节点的 `vjp` closure。 ## 5. 为什么 closure 表示 VJP 很自然? forward 阶段已知 `x,y,ans,shape`,backward 阶段稍后才知道 upstream `g`。外层 maker 捕获必要 forward state,内层 callable 接收 `g`,恰好分离两个时刻。 ## 6. closure 如何利用 `x` 与 `ans`? `exp` 规则使用 `ans*g`,`multiply` 规则使用另一个 operand,custom logsumexp 示例同时展示 shape/answer。Python lexical closure 只保留表达式真正引用的对象。 ## 7. 为什么不显式构造 Jacobian? 训练和多数 operator 只需要 `Jv` 或 `w^T J`。局部 product 可以沿图组合,避免大矩阵 materialization。完整 `jacobian` API 需要时才遍历 basis 并 stack products。 ## 8. multiple parent gradients 如何累加? `backward_pass` 对 `zip(node.parents,ingrads)` 的每项调用 `add_outgrads`。相同 parent key 会命中之前的 outgrad 并累加;`x*x` 实测为 `3+3=6`。 ## 9. broadcasting 如何处理? NumPy binary VJP rules用 `unbroadcast_f` 捕获目标 metadata,backward 用 `unbroadcast` 沿多余维度和 target size-1 轴求和。通用 reverse engine 不负责 NumPy shape 语义。 ## 10. nested differentiation 为什么可行? VJP/JVP rule 使用可被 Autograd 再次包装的操作;TraceStack 分配递增 ids,primitive 选择 highest-trace boxes。高阶实验实际看到三层 ArrayBox。 ## 11. mutable operations 为什么困难? reverse closure 可能保留 forward 对象引用;若用户之后原地修改它,backward 看到的 state 会变化。官方 tutorial 明确提醒复制所有值成本太高,数组赋值也不是受支持的常规可微写法。 ## 12. `VJP not defined` 通常意味着什么? forward primitive 可执行,但 `VJPNode.__init__` 在 `primitive_vjps` 找不到被追踪 argument positions 的 rule。custom primitive 实验已复现该异常。 ## 13. 为什么要 `import autograd.numpy as np`? 该 namespace 把 NumPy callables 包为 primitives、注册 ArrayBox 运算与 JVP/VJP rules。原生 NumPy 函数通常不了解 Autograd Box 与 registry,可能断 trace 或拒绝输入。 ## 14. 与 PyTorch/JAX 的概念比较 这是概念层比较,不是对它们当前源码的审计。PyTorch eager autograd 同样通过运行时 operation graph、local backward rules 和 accumulation 支持常见 reverse mode;JAX 更强调把可追踪 Python 数值程序变换为中间表示并组合 JVP/VJP、JIT/vmap 等 transformations。HIPS/autograd 更轻量,直接围绕 Python/NumPy wrappers 与 Box tracing,没有现代框架的大规模 device/compiler/runtime 层。 ## 15. 延续到现代 AD 系统的思想 仍然核心的思想包括:primitive derivative registry、JVP/VJP 而非显式 Jacobian、reverse topo traversal、cotangent accumulation、forward state capture、nested transformations、shape-aware rules,以及用 numerical checks 验证局部 derivatives。 ## 最终 mental model ```text user program executes normally -> differentiable inputs carry trace identity -> primitive calls create local AD state -> parents form the execution graph -> a product query seeds output tangent/cotangent -> local rules compose by chain rule -> shared paths accumulate ```