SICI · 蕴灵宗 · 第十七章 · 元婴二层

器道融通

深度学习框架与AI编译器 · 元婴二层

你以为框架就是调 API。可在框架之门前,真正的战场在抽象层之间——上层是优雅的 Tensor 运算,底层是裸的 CUDA kernel,中间隔着千万行胶水代码。抽象泄漏在暗渊里立赌:你以为高层抽象能掩盖一切,我赌你看不到底层的真相。

抽象泄漏低语:「你觉得框架就是 model.fit?随便猜。你说深度学习框架最核心的抽象是什么?」
序章

框架之门

核心抽象之赌 · 四选 · 框架层次全景

框架之门的门楣上刻着八个字:器以载道,道以御器。门内是一座巨塔——塔分多层:底层是裸的 CUDA kernel,中层是计算图与 autograd,顶层是 model.fit 的一行代码。高斯老祖站在塔基,手里只有一块黑板,上面写着:抽象是分层的不相欠。

框架之门的巨塔:底层裸 CUDA kernel、中层计算图与 autograd、顶层 model.fit,三层抽象之间横着一道暗渊
高斯老祖 高斯老祖

「你天天写 model(x).backward(),可你真的知道这一行背后发生了什么吗?框架把千万行胶水代码藏在一层抽象之下——告诉我,深度学习框架最核心的抽象是什么?先猜,猜错了也没关系,抽象泄漏会记账。」

抽象泄漏 抽象泄漏

「又来了一个想靠 API 混日子的。打个赌——你觉得框架最核心的抽象是什么?你每猜错一次,暗渊深一分,我都记下来。」

核心抽象之赌 · 框架最核心抽象预测挑战器

反直觉下注(四选一,可连错)→ 挑战展开 → 框架抽象层次全景图

抽象泄漏逼你下注:深度学习框架最核心的抽象是什么?逐个点击。
详解 · K17-02 PyTorch核心架构:Tensor/autograd/Module

PyTorch 的三层结构可以按"数据—结构—组织"拆开读。Tensor 不是裸数组,而是 {storage, shape, stride, dtype, device} 五元组:storage 是一段连续的一维显存,shape 与 stride 共同决定如何把这段线性内存解释成高维张量——S3 里 transpose 只改 stride 不动 storage,view() 却假设二者相容,那句 RuntimeError 就在此埋下。autograd 是跑在 Tensor 之上的引擎:只要 requires_grad=True,每个由它派生的 Tensor 都记下 grad_fn(产生自己的算子及其 vjp),前向结束即得到一张 DAG。Module 是工程层的封装:nn.Module 用 __setattr__ 拦截子模块与 Parameter 的赋值、登记进 _parameters,optimizer 才能遍历 parameters();forward() 定义子图,于是"层"就是可复用的子图。台上四选的谜底正落在第二层——Tensor 装载数据、Module 组织代码,唯有计算图把前向、反向、优化、调度统一成同一个对象;它不是三个抽象之一,而是另外两个得以成立的那个抽象。教材对照:Paszke 2019《PyTorch: An Imperative Style, High-Performance Deep Learning Library》、d2l 第 13 章。

第一幕 · 自信→崩塌 前奏

PyTorch 核心 · Tensor 与 autograd

前向传播 · 反向传播 · 梯度沿计算图流动

塔基之上是一片工作台,台上摆着一组积木——每个积木是一个算子。高斯老祖递给你三个 Tensor:x、w、b。「你来搭一个最简单的计算图:y = relu(x·w + b)。前向走一遍,反向再走一遍,看梯度怎么沿图流动。」

高斯老祖 高斯老祖

「autograd 的本质是链式法则的自动执行。前向传播时框架在背后默默建一张有向无环图(DAG),每个算子记下自己的输入和反向求导规则。反向传播时从 loss 出发,沿图逆拓扑序,每个节点把自己的梯度传给上游。你不需要手写求导——图替你记住了结构。」

抽象泄漏 抽象泄漏

「你以为 backward() 是魔法。它不过是图上的一遍遍历。可你知道图是怎么存的吗?你知道梯度为什么有时是 None 吗?」

自动微分可视化器 · 计算图 + 前向 + 反向

点前向看图构建 → 点反向看梯度流动 → 查看每个节点的局部梯度

计算图 · y = relu(x·w + b)

梯度流动 · 节点局部雅可比

详解 · K17-03 PyTorch autograd引擎:计算图构建与梯度计算

把台上那张图手算一遍,autograd 的全部秘密就摊开了。前向:mul = x·w = 2×3 = 6,add = 6 + b = 7,relu(7) = 7,y = 7——框架在算值的同时把 mul、add、relu 三个节点及其输入边记进 DAG,每个 op 附带一个 vjp(vector-Jacobian product)闭包。反向:从 ȳ = ∂L/∂y = 1 出发按逆拓扑序回传,relu 的局部导数是 (add>0 ? 1 : 0) = 1;add 对两个输入都是 1(梯度直通并分流给两条边);mul 对 x 的局部导数是 w、对 w 的是 x。于是 ∂y/∂x = 1×1×1×3 = 3,∂y/∂w = 1×1×1×2 = 2,∂y/∂b = 1×1×1 = 1——与台上读数逐位吻合,没有任何魔法。三个必须记住的工程细节:①梯度是累加进 .grad 而非覆盖,故每步前要 optimizer.zero_grad(),否则跨 batch 累加;②只有叶子节点默认保留 .grad,中间节点的梯度用完即释放,要看需先 retain_grad();③原地操作(in-place)会破坏前向记录的值,触发 "a leaf Variable that requires grad is being used in an in-place operation" 一类报错。教材对照:花书第 6 章反向传播、PyTorch autograd 文档。

DataLoader 流水线 · 多进程预取喂 GPU

3 个 worker 异步取批 → 预取队列 → GPU 消费。看 GPU 是否饿着。

队列0
GPU 利用率—
点切换看预取开关对 GPU 利用率的影响。
详解 · K17-04 数据加载与预处理pipeline:Dataset/DataLoader

训练吞吐的上限常常不在 GPU,而在喂数据那条链上。PyTorch 把它拆成四段:Dataset 用 __len__ 与 __getitem__ 把任意存储(磁盘、LMDB、WebDataset)抽象成"按下标取一样本";Sampler 决定下标顺序(RandomSampler 打乱、DistributedSampler 在 DDP 下按 rank 切分,保证各卡不重不漏);collate_fn 把一批样本拼成张量(变长序列在此 padding);DataLoader 用 num_workers 个子进程并行跑前三步,结果经队列送回主进程。台上那个小模型把这条链画活了:3 个 worker 以约 1.2% 的概率各自把一批推进队列(上限 5),GPU 每步忙 0.8 秒;关掉预取后入队概率降到 0.5%、队列上限降到 3,GPU 立刻开始空等,利用率读数断崖下滑。四个关键旋钮:num_workers(CPU 核数与 I/O 并发的折中,常从 4 起调,过多反而因进程切换与共享内存争用变慢)、prefetch_factor(每 worker 预取批数,默认 2)、pin_memory=True(锁页内存,让主机到设备的拷贝走 DMA 并可与计算重叠)、persistent_workers=True(epoch 之间不销毁进程,省掉反复 fork 的开销)。顺序是:先保证 GPU 不空等,再谈算子优化。教材对照:d2l 第 13 章。

第一幕 · 自信→崩塌 前奏

动态图 vs 静态图

PyTorch eager · TensorFlow graph · 优化机会权衡

工作台后是两面镜子。左镜映着 PyTorch 的 eager 模式——每写一行代码,图就即时构建即时执行,像流水;右镜映着 TensorFlow 的 graph 模式——先定义整张图,再编译执行,像图纸。高斯老祖:「灵活性和性能是一对永恒的矛盾。你拖动天平,看两边怎么权衡。」

高斯老祖 高斯老祖

「动态图每次前向都重新建图,方便调试、支持控制流,但牺牲了全局优化机会——框架看不到整张图。静态图先定义后执行,可以做算子融合、常量折叠、内存规划,但失去了运行时灵活性。PyTorch 2.0 的 torch.compile 试图两者兼得——用动态图写,编译成静态图执行。」

抽象泄漏 抽象泄漏

「灵活?性能?你以为能两个都要。可天平一倾斜,抽象就开始漏。」

图模式对比器 · 动态(eager) vs 静态(graph)

切换模式看执行差异 → 拖天平看灵活性-性能权衡 → 展开静态图优化机会

执行模式对比 · 逐行 vs 编译

抽象天平 · 灵活性 ↔ 性能

灵活性 0.50 · 性能 0.50
详解 · K17-01 深度学习框架的设计哲学:动态图vs静态图

两条路线的分歧在"图什么时候被建起来"。define-by-run(动态图):每执行一行 Python 就即时建节点并立刻求值,图随代码流每次再生——PyTorch(Paszke 2019)、NumPy、TF2 默认的 eager 都属此类;好处是 print、断点、Python 原生控制流全部可用,代价是框架只看见当前这一步,拿不到全局。define-and-run(静态图):先用一套描述语言定义完整计算,再交给运行时编译执行——TensorFlow 1.x(Abadi 2016)是代表;图一旦到手就能做算子融合、常量折叠、内存规划与布局选择,代价是调试要靠专门的打印算子、控制流要写成框架提供的条件与循环算子。台上 6 个算子的对比是这个分歧的缩影:eager 侧 6 次 launch、6 次显存落盘;graph 侧先编译、再以 2 个融合大算子执行,launch 从 6 次降到 2 次。真正缝合二者的是 PyTorch 2.0 的 torch.compile(2023):TorchDynamo 从 Python 字节码里抓出可编译子图,AOTAutograd 提前算好反向,Inductor 生成 Triton 或 C++ 代码——写法仍是动态图,执行已是静态图。但缝合不等于弥合:一旦触发 graph break 或遇到动态 shape,编译回退,抽象泄漏立刻现形。选择从来不是二选一,而是"你要在哪一层放弃确定性"。

详解 · K17-11 图优化vs算子优化:不同层次的优化策略

优化发生在两个不同层次,混为一谈就会调错旋钮。图级优化看的是整张 DAG 的拓扑:算子融合(把逐元素链并进前后大算子)、常量折叠(编译期算掉常量子树)、死代码消除(剪掉输出无人消费的分支)、内存规划(静态分析张量生命周期后复用缓冲区)、布局与格式选择(NCHW/NHWC、是否转 channel-last)。它改变的是"算多少次、读写几趟",收益与单个算子的实现无关。算子级优化看的是一个 kernel 内部怎么跑:循环分块 tiling、向量化、双缓冲、共享内存与寄存器分配、tensor core 的使用、Winograd 与 implicit GEMM 之类的算法替换。它改变的是"每次算得多快"。台上展开的四条静态图优化机会(融合、常量折叠、内存规划、布局转换)全在第一层;S7 里框架 A 与 B 的差距(1200 vs 800 s/s)全在第二层——同一个卷积,一个走 Winograd + FP16 tensor core + NHWC,一个走隐式 GEMM + NCHW。二者正交:图优化省掉的是"不必要的"工作,算子优化压缩的是"必要工作"的耗时。AI 编译器栈(TVM/XLA/MLIR)的价值正在于把两层的搜索空间都打开,并给出可复现的自动调优结果,而不是只做其中一层、把另一层继续留在黑箱里。

第一幕 · 第一崩塌

第一崩塌 · 抽象泄漏

view/reshape 复现 · 非连续张量 · 底层真相

镜子碎裂,你跌入一道暗渊。渊底是你的训练台——你对一个转置后的张量调用 view() 想重塑形状,框架报错:RuntimeError: view size is not compatible with input tensor's size and stride。你以为是框架的 bug,可高斯老祖沉默了。暗渊里传来一个不带温度的声音:

抽象泄漏 抽象泄漏

「你以为是框架的问题,其实是底层的真相。view() 假设张量在内存里是连续的,可 transpose 只改了 stride 没动数据——你眼里的'形状'和内存里的'布局'根本不是一回事。第一笔账,我记下了。」

高斯老祖 高斯老祖

「抽象泄漏的根源:高层 API 掩盖了底层的物理约束。view 假设连续,transpose 打破连续,contiguous() 强制拷贝修复。你以为 .view() 和 .reshape() 一样——可前者要求连续,后者会自动处理。抽象总有泄漏,泄漏处即真相处。」

抽象泄漏复现 · view vs reshape + 内存布局诊断

拖操作步骤看泄漏出现 → 展开内存布局真相 → 暗渊裂缝可视化

view() 崩塌复现 · 操作步骤

step 1
详解 · K17-14 自定义算子开发:C++扩展/CUDA核函数

view() 那句 RuntimeError 其实是自定义算子的入门考题。自己写算子有三条路,越往下越贴近硬件、也越容易踩坑。第一条是纯 Python 组合:把已有算子拼成新算子,零成本但拿不到融合收益。第二条是 C++/CUDA 扩展:用 torch.utils.cpp_extension.load() 现场编译(或用 setup.py 预编译),C++ 侧做形状检查与 dispatch、CUDA 侧写 __global__ kernel 自己算下标——这一步逼你亲手处理 shape、stride 与连续性:kernel 里通常要用 strides 手算线性偏移,或干脆在入口对非连续输入调 .contiguous() 规整(代价是一次拷贝,S3 里 reshape 替你做的就是这件事)。第三条是接住自动微分:继承 torch.autograd.Function,forward 存好反向需要的张量,backward 手写 vjp——框架不会替自定义算子求导,导数写错一处,训练就会以极隐蔽的方式跑偏,故必须用 torch.autograd.gradcheck 做数值校验(用有限差分比对解析梯度)。更上层的两个选择:直接调 cuBLAS/cuDNN 拿现成的高效实现,或用 Triton 以 Python 语法写 kernel 交给编译器生成。台上的教训在此回环——你之所以要亲手动刀,正因为高层承诺的"形状"与底层现实的"布局"并不等价,而抽象泄漏总在你以为二者等价的那一刻浮出水面。

第二幕 · 挣扎→再崩塌 前奏

计算图优化 · 算子融合

小算子 → 大算子 · kernel launch 开销消除

暗渊之上是一座熔炉。炉口吐出一串小算子——add、mul、relu、add、mul——每个都是一次 kernel launch。高斯老祖递给你一把融合锤:「每个小算子启动一次 GPU kernel,开销在 launch 而不在计算。你把它们敲成一个大算子,开销就消失了。」

高斯老祖 高斯老祖

「算子融合把多个小算子合成一个大算子,消除中间结果的显存读写和 kernel launch 开销。比如 y=relu(x*w+b) 融合成一个 fused GEMM+ReLU。GPU 上每次 kernel launch 有 5-10μs 开销——台上这串 5 个小算子,融合后从 5 次启动降到 1 次,省 4 次启动;访存也省了,4 个中间张量不再落盘显存。」

算子融合可视化器 · 融合前后 + 开销对比

点未融合看逐算子执行 → 点融合看合成算子 → 开销条对比

算子图 · 融合前 vs 融合后

kernel launch 开销对比

详解 · K17-06 计算图优化:算子融合/常量折叠/死代码消除

把台上那串算子算清楚,图优化就不神秘了。未融合时 y = relu(x·w + b) 被拆成 mul、add、relu、mul、add 五个算子:每个算子启动一次 GPU kernel,按 7μs/次 launch 计是 5×7 = 35μs;相邻算子之间还要把中间张量写回显存再读出来,4 次访存按 3μs/次计是 12μs,合计 47μs。融合成一个 fused GEMM+ReLU 之后只剩 1 次 launch(7μs)、0 次中间访存:省下 40μs,占 85%(40/47)。注意这个高比例是被"小算子"喂出来的——融合消除的是固定开销,算术量一点没少,所以它对逐元素密集的尾部(bias + activation + dropout + residual)最有效,对一个本身已是毫秒级的大 GEMM 几乎无感;这也是融合规则通常写成"大算子吃掉紧邻的逐元素算子"的原因。另外两类图优化同样只删不增:常量折叠在编译期就把 shape 推导、scale/bias 合并算掉;死代码消除剪掉输出无人消费的分支(推理时的 dropout、训练时未被 loss 触达的辅助头)。它们共同的前提是"看得见整张图"——这正是静态图相对 eager 的结构性优势,也是 torch.compile 要先把动态图抓成静态子图的理由。教材对照:Chen 2018 TVM 论文的图级优化一节。

第二幕 · 挣扎→再崩塌 前奏

AI 编译器 · 从前端到后端

前端 → IR 优化 → 代码生成 · TVM 调度原语

熔炉后是一条流水线。流水线分三段:前端把模型(PyTorch/ONNX)翻译成中间表示(IR);中段对 IR 做优化(融合、布局、调度);后段把优化后的 IR 生成目标代码(CUDA/ROCm/LLVM)。高斯老祖:「这就是 AI 编译器——TVM、XLA、Triton 都是这条路。你走一遍。」

高斯老祖 高斯老祖

「TVM 的核心是计算-调度分离:计算用 Relay/Relax 描述'算什么',调度用 TensorIR/tir.schedule 描述'怎么算'。split、reorder、bind、cache_read 这些调度原语控制循环如何拆分、重排、绑定线程、缓存数据。同一个计算,不同调度在不同硬件上性能差 10 倍。」

编译流水线可视化器 · 前端 + IR + 后端 + 调度

点击流水线三段看数据流 → 切换调度原语看代码变化 → 性能对比

编译流水线 · 前端 → IR优化 → 代码生成

TVM 调度原语 · 同一计算不同调度

详解 · K17-07 AI编译器架构:前端→优化→代码生成

AI 编译器是传统编译器三段式在张量世界的重演。前端负责把模型从宿主框架的表示翻译成自己的 IR:PyTorch 走 TorchDynamo/FX 抓图或导出 ONNX,TensorFlow 走 GraphDef/MLIR,TVM 的前端产出 Relay(静态图)或 Relax(支持动态 shape)。中段是优化:先在图级 IR 上跑 pass 管线(算子融合、常量折叠、布局转换、代数化简),再下沉到张量级 IR(TVM 的 TensorIR、MLIR 的 linalg/affine)做循环变换与调度。后端做代码生成:把带调度的张量 IR 逐步 lower 到目标语言与运行时——CUDA C、ROCm、LLVM IR、WASM,并负责 launch 配置、共享内存分配与 kernel 打包。台上那条三段流水线画的正是这条主干,而 ONNX 往往是前端真正的第一入口(详见 K17-12)。与 GCC/LLVM 最大的区别在于:传统编译器优化的目标是"少算指令",AI 编译器还要同时优化"少读显存"与"把计算切给成千上万个线程"——于是多出一个其他编译器没有的环节:调度与自动调优。正因为多了这一环,同一份计算在不同硬件上的性能可以差一个数量级;也正因为多了这一环,编译耗时与动态 shape 重编译成为新的工程成本。教材对照:Chen 2018《TVM: An Automated End-to-End Optimizing Compiler for Deep Learning》。

详解 · K17-08 张量编译:TVM的调度原语与自动调优

TVM 最锋利的那一刀叫"计算与调度分离":TE/TensorIR 只声明"算什么"(一个数学表达式),schedule 只声明"怎么算"(循环怎么切、怎么排、绑到哪个线程、缓存放哪)。四类原语就是台上那四个 toggle:split 把长循环切成 (outer, inner) 两块(factor=32),让数据块能落进共享内存或缓存;reorder 重排循环嵌套的次序(把 k 提前),改变访存轨迹以提高局部性;bind 把循环轴绑到 GPU 的执行载体(blockIdx.x / threadIdx.x / vthread),把串行循环变成成千上万路并行;cache_read/cache_write 显式插入共享内存或寄存器级缓存,配合 compute_at 决定缓存的作用域边界。台上的相对性能条给出量级:naive 1× → split 2.5× → reorder 4× → bind 10×——bind 贡献最大,因为它直接换来并行度,与叙事里"同一计算不同调度性能差 10 倍"对齐。手工写调度需要专家经验,于是有自动调优:AutoTVM(Chen 2018)用模板加机器学习在给定硬件上搜索参数;Ansor(Zheng 2020)进一步自动生成调度草图并做进化搜索,把"专家手写"变成"机器搜索若干小时"。代价很清楚:调优时间从分钟到小时不等,换来的是可预测、可复现的 kernel 性能——这正是抽象泄漏的解药配方。

详解 · K17-12 模型格式与交换:ONNX/SafeTensors/GGUF

模型要跨框架流动,就得有中立格式,三种格式各管一段。ONNX(Open Neural Network Exchange,2017 年由微软等发起)是"图交换格式":用 protobuf 描述一张计算图(节点、输入输出、初始器),并用算子集 opset 号锁定语义版本;PyTorch 的 torch.onnx.export、TensorFlow、MXNet 都能导出,TensorRT、ONNX Runtime、OpenVINO 都能导入——它也是多数 AI 编译器前端的第一入口(台上流水线前端那一步,工程里多半就是"读 ONNX")。ONNX 的软肋是算子覆盖与版本漂移:新算子常是框架里有、ONNX 里没有,导出时需自定义 op 或改写子图。SafeTensors(HuggingFace 2023)管的是另一头——只存权重不存图:头部 JSON 描述 dtype 与 shape、后面紧跟裸字节,因此支持 mmap 零拷贝加载、跨语言读取,且不含 pickle,规避了 torch.load 的任意代码执行风险,已成 HuggingFace 生态默认。GGUF(llama.cpp 2023,GGML 的后继)面向本地推理:单文件打包权重、超参、tokenizer 与量化元数据(如 Q4_K_M 的块结构),为 CPU 与小显存设备上的 mmap 直接运行而设计。三者分工:ONNX 换"图",SafeTensors 换"权重",GGUF 换"可跑的成品"。

第二幕 · 挣扎→再崩塌 前奏

MLIR · 多层中间表示

张量方言 → 仿射方言 → LLVM IR · 逐层 lowering

流水线尽头是一座降阶塔。塔分多层:顶层是张量方言(tensor dialect),操作的是高维张量;中层是仿射方言(affine dialect),操作的是循环嵌套;底层是 LLVM IR,操作的是寄存器和指令。高斯老祖:「MLIR 的精髓是多层 IR——每层有自己的优化,逐层 lowering 把高层抽象翻译成底层指令。你点每一层,看它怎么降。」

高斯老祖 高斯老祖

「MLIR 的 dialect 是可组合的抽象层。tensor dialect 做张量级优化(融合、布局),affine dialect 做循环级优化(分块、向量化),llvm dialect 做指令级优化(寄存器分配、指令调度)。lowering 不是一对一翻译,是带优化的降阶——每降一层,抽象少一分,性能机会多一分。」

IR 层次展示器 · 张量→仿射→LLVM 逐层 lowering

点击三层 IR 看代码变化 → 查看每层优化机会 → lowering 流动

MLIR dialect 塔 · 逐层 lowering

IR 代码 · 当前层级

详解 · K17-09 MLIR与AI编译中间表示:张量方言/仿射方言

MLIR(Multi-Level IR,Lattner 2021)要解决的正是"一层 IR 走天下"的困境。它的核心概念是 dialect:每个 dialect 是一组自定义的 op、类型与属性,可以有自己的不变量与自己的优化 pass。典型分层:tensor/linalg dialect 描述高维张量运算(matmul、conv、逐元素),适合做融合与布局;affine dialect 用仿射约束描述循环嵌套(affine.for / affine.load / affine.store),适合做分块、向量化与循环展开;scf dialect 描述结构化控制流;再往下是 vector、gpu、llvm dialect。多个 dialect 可以共存于同一 module,通过 conversion pass 逐片 lowering——这就是"渐进式降阶":高层语义在还能用的时候尽量留住(在 linalg 层融合,比在 LLVM IR 层融合容易一万倍),到不能再留时才降下去。台上那座三层塔(tensor → affine → llvm)正是这个过程的剖面:每降一层,抽象少一分、可用的优化手段换一批、性能机会多一分。MLIR 的真正红利是可组合与可复用:新硬件只需定义自己的 dialect 与 lowering 路径,就能复用上游全部优化;XLA、Triton、IREE、torch-mlir 如今都跑在 MLIR 之上。它是把"编译器碎片化"收敛成一个共同中间层的答案。

第二幕 · 第二崩塌

第二崩塌 · 性能之谜

同模型不同框架性能差异 · kernel 选择影响 · 底层真相

降阶塔碎裂,你跌入一面镜子迷宫。镜子里映着同一个 ResNet——左镜用框架 A 跑,每秒 1200 样本;右镜用框架 B 跑,每秒 800 样本。模型一样、硬件一样、batch 一样,A 比 B 快 50%(反过来 B 比 A 慢 33%)。高斯老祖:「这不是框架的 bug,是 kernel 选择的差异。」

抽象泄漏 抽象泄漏

「你以为 model.forward() 在两个框架里干的是同一件事。可底层——一个选了 cuDNN 的 Winograd 卷积,一个选了隐式 GEMM;一个开了 tensor core,一个没开。抽象掩盖了 kernel 选择,性能差异就藏在你看不见的那一层。第二笔账,我记下了。」

高斯老祖 高斯老祖

「同样的高级代码,底层 kernel 选择天差地别。cuDNN 一个卷积有十几种算法,框架根据输入形状自动选——选错了慢 50%。这就是抽象泄漏的代价:你失去了对底层的控制,也就失去了性能的可预测性。AI 编译器要解决的,正是把 kernel 选择从黑箱变成可优化。」

性能之谜诊断器 · 同模型不同框架 + kernel 选择

拖 kernel 选择看性能差异 → 展开底层 kernel 真相 → 诊断抽象泄漏根因

性能差异复现 · kernel 选择滑块

kernel 选择 50
详解 · K17-10 算子库与内核选择:cuDNN/cuBLAS/Triton

高层那行 nn.Conv2d 落到硬件上,实际执行的是某个具体 kernel,而"选哪个"是框架替你做的黑箱决定。主流算子库:cuDNN(NVIDIA 深度学习原语库,覆盖卷积/池化/归一化/循环网络)与 cuBLAS(稠密线性代数,GEMM 主力),是 PyTorch 与 TensorFlow 在 NVIDIA 卡上的默认后端。cuDNN 里一个卷积就有十几种算法——直接卷积、implicit GEMM、Winograd、FFT,各自适用不同的 kernel size、channel 与 batch 组合;选择方式有两种:启发式查表(cudnn.benchmark = False,秒选)与实际测量(cudnn.benchmark = True,首次遇到某输入形状时把候选算法都跑一遍取最快并缓存)。台上 A/B 的差距正是这类选择的后果:A 走 Winograd + FP16 tensor core + NHWC,1200 s/s;B 走隐式 GEMM、未开 tensor core、仍是 NCHW,800 s/s——以 A 为基准 B 慢 33%,以 B 为基准 A 快 50%,两个口径必须说清基准才不自相矛盾。两条工程建议:输入形状固定后开 benchmark 让实测说话;形状频繁变化则别开,否则每个新形状都要重测一遍。Triton(Tilley 2021,OpenAI)给出第三条路:不从候选库里挑,而是用 Python 语法直接写 kernel、由编译器生成——把选择权从黑箱拿回开发者手里,这正是"把 kernel 选择从黑箱变成可优化"的落地方式。

详解 · K17-13 框架选型:PyTorch vs JAX vs TensorFlow

选型不是品味问题,是四条硬约束的加权。PyTorch(2017 至今)以 eager 为默认、API 贴近 NumPy,研究迭代最快,生态(HuggingFace、torchvision、Lightning、torch.compile)与教学资料最厚,是当前论文复现的事实标准;部署侧曾偏弱,如今靠 torch.export + ExecuTorch + torch.compile 补齐。TensorFlow(2015)在 1.x 时代以静态图与部署链路取胜,2.x 转向 eager + tf.function;优势仍在端到端生产流水线:TF Serving 服务化、TFLite 移动与边缘、TF.js 浏览器、TPU 支持与 TFX 全家桶。JAX(2018)走另一条路——函数式加变换组合:grad 求导、jit 编译、vmap 自动向量化、pmap/sharding 并行,全部是作用在纯函数上的可组合变换,底层统一交给 XLA 编译;代价是必须接受"无副作用、显式传 PRNGKey、数组不可变"的函数式纪律,生态(Flax/Haiku/Optax)比 PyTorch 小。台上 1200 vs 800 s/s 的差距提醒我们:三者的高级 API 看着差不多,真正的差异在底层后端——cuDNN 版本、XLA 是否接管、布局约定、是否启用 tensor core。经验法则:研究与原型用 PyTorch,TPU 与可组合变换用 JAX,成熟服务的端到端链路看 TF。

第三幕 · 顿悟→突破 前奏

显存优化 · 梯度检查点

activation checkpoint · 时间换空间 · trade-off

镜子迷宫尽头是一座显存天平。天平左盘是时间,右盘是空间。高斯老祖:「训练大模型时显存装不下所有中间激活——梯度检查点用时间换空间:前向时不存所有激活,反向时重算被丢弃的部分。你拖滑块,看显存怎么省、时间怎么涨。」

高斯老祖 高斯老祖

「梯度检查点(activation checkpoint)把网络分段,只在检查点存激活,中间的激活反向时重算。N 层网络存 k 个检查点,峰值显存是 k + N/k 层激活(k 个检查点 + 段内重算缓冲),在 k = √N 处取最小、为 2√N 即 O(√N);代价是前向多算一遍重算量。这是时间-空间 trade-off 的经典——大模型训练的标配。」

显存优化器 · 梯度检查点 + 时间空间 trade-off

拖检查点数量看显存节省 → 观察时间增长 → trade-off 曲线

梯度检查点 · 检查点数量滑块

检查点 0 / 20 层

时间-空间 trade-off 曲线

详解 · K17-15 内存管理与显存优化:梯度检查点/激活重计算

把台上那把尺子换算成公式。设网络 N 层,存 k 个检查点(k = 0 表示不做检查点、激活全存,峰值 N 层)。反向推进到第 i 段时,需要从该段起点重算整段前向,段长 N/k,故峰值显存 = k(常驻检查点)+ N/k(当前段的重算缓冲),单位统一为"一层激活"。归一化到基线:(k + N/k)/N,对 k 求导得极值在 k = √N,此时峰值 = 2√N,即由 O(N) 降到 O(√N)(Chen 2016)。台上 N = 20:k = 4 时 (4 + 20/4)/20 = 9/20 = 45%,省 55%;k = 20 时 (20 + 1)/20 ≈ 105% ≈ 100%,等于没省——所以"检查点越多越省显存"是错的,显存随 k 呈 U 形。时间账:基线一步 = 前向 1 + 反向 2 = 3;有检查点后多出重算 (N−k)/N 次前向,总计 1 + (N−k)/N + 2 = 4 − k/N,除以 3 得 (4 − k/N)/3。k → 0(只存输入、全量重算)时为 4/3,即 +33%,与教材口径一致;k = N 时为 1,回到基线。工程接口是 torch.utils.checkpoint.checkpoint(或 checkpoint_sequential 按段切),常与混合精度、ZeRO、offload 叠加。代价也很实在:重算是拿算力换显存,算力已饱和时会直接拉低吞吐——这正是"时间-空间 trade-off"的字面含义。

第三幕 · 最高潮

Boss 战 · 抽象泄漏

框架三问 · 漏账清算 · 排除刻痕收网

显存天平后浓雾重新合拢,温度骤降——抽象泄漏凝聚成形。它不是人形,是一道横亘高层 API 与底层 kernel 之间的暗渊,渊里映着无数错位的形状与布局。它翻开一本账册:「你来了。带着你的两道刻痕,和你的漏账。三问。答对,我弥合。答错,你的元婴修为归零。」

抽象泄漏 抽象泄漏

「你说抽象能掩盖一切——它不能。我住在你'以为高层 API 能掩盖底层'的地方。你每以为抽象无泄漏,我就深了一丈。证明给我看:你的两道刻痕不是耻辱,是你的排除法——每一条'此路不通',都在告诉你正确的路在哪里。」

漏账累计 0 笔 · 三问全对后清零

第三幕 · 收束 · 元婴二层

顿悟 · 器道融通

知识图谱点亮 · 框架编译器方法论 · 境界突破 元婴二层→三层

暗渊弥合,你站在山顶。山脚是你走过的路——框架之门、Tensor与autograd、动态图静态图、抽象泄漏、算子融合、AI编译器、MLIR、性能之谜、梯度检查点。山顶放着一面器道镜,镜中映着所有抽象层交织成的知识图谱。高斯老祖:「你想驾驭器物。面前有三道认知:抽象是分层的不相欠、优化是看得见整张图、融通是承认泄漏永恒。」

高斯老祖 高斯老祖

「框架与编译器的全部,是让软件栈各层抽象既独立又融通。计算图是中枢抽象,autograd 是图上的求导规则,算子融合是图级优化,AI 编译器是计算-调度分离,MLIR 是多层渐进 lowering,梯度检查点是时间-空间 trade-off。没有无泄漏的抽象,只有承认泄漏后的融通。元婴二层·器道融通——你已圆满。但元婴之路的下一层,不在融通,在分布式。」

器道融通 · 三道认知 + 知识图谱 + 境界突破

点亮三道认知 → 点亮框架编译器知识图谱 → 境界突破动画(元婴二层→三层)

三道认知 · 各三选一

详解 · K17-05 分布式训练框架:DDP/FSDP/DeepSpeed/Megatron

单卡放不下大模型时,框架把并行训练切成四种范式。DDP(DistributedDataParallel)每卡保一份完整副本,反向结束后对梯度做一次 AllReduce;Ring AllReduce 每卡收发量 = 2(N−1)/N·M(M 为参数量),N 很大时趋近 2M,扩展近乎线性但受通信墙限制。FSDP(Fully Sharded Data Parallel)把参数、梯度、优化器状态都按层分片跨卡,仅在本层前向/反向时才 all-gather 拼回,显存从 DDP 的 O(1) 降到 O(1/N)。DeepSpeed 的 ZeRO 把这套分级:ZeRO-1 只分片优化器状态、ZeRO-2 加梯度、ZeRO-3 再加参数,ZeRO-Offload 把状态卸到 CPU/NVMe 以换更大模型。Megatron-LM 走另一条路——张量并行(层内权重矩阵按行/列切,需两次 all-reduce 拼回)叠流水线并行(层间切,留 1F1B 气泡)再叠数据并行,三层组合榨干集群。取舍一句话:通信敏感选 FSDP/ZeRO,超大规模稠密选 Megatron 3D 并行。教材对照:PyTorch DDP 文档、Rajbhandari 2020《ZeRO》、Shoeybi 2020《Megatron-LM》。

详解 · K17-16 框架发展趋势:编译优化/可组合性/可复现性

PyTorch 2.0 之后主线收敛到三条。①编译优化:TorchDynamo 用 PEP 523 的 frame evaluation 钩子捕获 Python 字节码→FX Graph,绕过 Python 解释器;TorchInductor 再把图 lowering 成 OpenMP(CPU)或 Triton(GPU)融合 kernel,把 eager 模式下一百次算子分发换成几个融合 kernel,典型提速 2×。②可组合性:functorch 的 vmap/grad/forward-mode AD 并入 torch.func,以无状态函数式变换组合 Jacobian、per-sample 梯度、批处理,告别全局 autograd 状态。③可复现性:torch.use_deterministic_algorithms(True) + 固定种子 + cudnn.deterministic,但浮点归约顺序与 atomicAdd 使 bit-exact 难以保证,只能做到 seed-exact(同硬件同版本可复现),跨硬件则只能近似。底层编译器前端也收敛到 MLIR 多层 lowering(TVM/Torch-MLIR/IREE 共享)。教材对照:PyTorch 2.0 技术博客、Lattner 2021《MLIR》。

复盘 · 器道融通主线索

本章七层抽象串成一条主线:Tensor 装数据、计算图立结构、autograd 跑规则、动态/静态图定执行、算子融合做图级优化、AI 编译器行计算-调度分离、MLIR 做多层 lowering、kernel 选择落到底层、梯度检查点以时间换显存。每往下一层,抽象都在"泄漏"——view/reshape 的内存真相、框架差异、性能之谜都提醒你:没有无泄漏的抽象,只有承认泄漏后的融通。这正是元婴二层·器道融通的境界。下一层不在融通,在分布式——ch18 万核归一将把单机抽象扩展到千卡集群。

高斯老祖高斯老祖

「训练要喂得快,靠数据加载 pipeline:Dataset 封装样本,DataLoader 多进程预取、按 batch 拼批、打乱顺序,再叠 transform 做预处理。GPU 算得快、数据供不上,吞吐就卡在 I/O——所以 pipeline 得与训练异步并行,这是框架的隐功臣。」

交互实验

算子融合收益

拖动算子数与融合分组,观察 kernel 启动与中间内存的节省。