ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

kornia 开发者的 compile-first 优化工作流:让算子真正兼容 torch.compile(fullgraph=True) 并测得实打实的加速

kornia 开发者的 compile-first 优化工作流:让算子真正兼容 torch.compile(fullgraph=True) 并测得实打实的加速

kornia 开发者的 compile-first 优化工作流:让算子真正兼容 torch.compile(fullgraph=True) 并测得实打实的加速

【免费下载链接】kornia🐍 Geometric Computer Vision Library for Spatial AI项目地址: https://gitcode.com/GitHub_Trending/ko/kornia

本篇技术指南以 kornia 仓库中面向开发者(contributor)的《kornia-developer:compile-first optimization》技能规范为骨架,完整讲解如何在 kornia 中把一个算子(op)改造成真正torch.compile(fullgraph=True)兼容、且在 eager 与编译两种路径下输出逐字节一致的函数。文章覆盖了图中断(graph break)的六类分类与对应「单路径」修复方案、禁止is_compiling()兜底的根本原则、强制的前后性能基准(benchmark)实验规范、跨库对比矩阵与「10× vs albumentations」目标,以及把成果用test_dynamo与 PR 时间dynamoCI 任务锁死的落地流程。读完你可以直接照此工作流处理 kornia(或同类 PyTorch 库)中的任意编译失败,并产出可进入 PR 的完整证据链。

为什么需要一条「刚性」的 compile-first 工作流

kornia 的数值核心(filters、color、geometry transforms、losses、morphology、metrics)大部分已经可以编译通过,但「能编译」与「编译后真正等价且更快」是两回事。该技能文档把目标定义得很明确:让一个 kornia 函数真正torch.compile(fullgraph=True)兼容,并且可测量地更快。这是一条「刚性(rigid)」流程——每一步都必须执行,因为它建立在一条首要原则上:

先有地基,再有绿灯测试(foundation, not green tests)。一个通过的测试只是证据,永远不是目标。流程中每一步的存在都是为了让你声称的事实为真;如果你发现自己只是在调测试让它变绿,却没有理解代码为什么现在能工作,请停下——你正在沙子上盖楼。

这条原则的由来,是仓库踩过的三个具体陷阱(SKILL.md 中逐一列出):

  • 测试可以通过,却从未真正检验所声称的事实。用预生成的参数调用op(x, params=pregenerated_params)会跳过forward_parameters,于是一个「fullgraph」的 dynamo 测试即使通过,也从未 trace 过参数生成路径。必须测试真实用户路径(op(x)),而不是绕过所声称修复代码的捷径。这一点在kornia/augmentation/base.py中体现得很清楚:forward_parameters(第 293-303 行)内部调用__batch_prob_generator__再调用generate_parameters,参数生成本身就是一个蕴含torch.rand、torch.where的图节点,不编译它等于没有编译完整的 op。
  • 「在我机器上能跑」不等于「能跑」。int(tensor)在某些 torch 版本(如 2.10)下会在 dynamo 中被常量折叠(constant-fold),在另一些版本(如 2.9.1)下则降级为打断图的.item()。只在本地 torch 上验证过的 fullgraph 声明,如果 CI 跑的是另一个版本就毫无价值。结论:以 CI torch 版本(PR 的 dynamo job)为准,并且优先采用构造上与版本无关的修复——用张量构造张量(如torch.stack拼接标量0/1),绝不使用int(tensor)或torch.tensor([[... python int ...]])。
  • 删除死分支前,先理解它为什么是死的。tensor == torch.Size(...)不会广播,而是退化为恒等==,静默地永远是False。代码可能在某个从不运行的分支下「正常工作」。要诊断机制本身,而不是套模板式地修。

铁律:禁止is_compiling()兜底

torch.compiler.is_compiling()守卫被明令禁止作为修复手段。原因在于它让编译路径跳过数据相关代码而不是让它可 trace,于是 eager 与 compile 运行的是不同路径,编译路径下的检查被静默丢弃。这是「靠排除法实现 fullgraph(fullgraph by exclusion)」,不是真正的 fullgraph——而且它让 op 只被部分编译,直接杀死加速效果。

修复必须让 eager 与 compile 运行同一条代码路径。

六类图中断的分类与「单路径」修复表

遇到 graph break 时,首先torch.compile(fn, fullgraph=True)(x)精确定位断点(必须是kornia/…内的 traceback 帧),然后对照下表分类处理:

断点形态修复方案(单一路径)
if (t < 0).any(): raise——取值校验torch._assert_async((t >= 0).all(), "msg")—— 检查保留,且不产生断点
bits==0/==8风格的逻辑分支(分支条件依赖张量)无分支的torch.where(cond, a, b)—— 两个分支都计算,再选择
if not mask.any(): break—— 定界循环里的提前退出直接删除;尾部迭代必须可证明是 no-op
if mask.all(): return None—— 返回不同类型的提前返回始终返回该值;让调用方对该值的消费变成 no-op
if batch_prob.sum()==1—— 对 {0,1} 张量的门控无分支乘法(gate * value)
Pythonif p == 1,其中p是Pythonfloat/bool保留原样 —— trace 期就已解析,不算断点

如果六类都不适用(无界while、由.item()驱动的 shape、随机置换分发、动态 shape 的nonzero/unique),说明它需要重新设计或维护者决策——不要强行打补丁,记录下来并停下。

对照源码可以看到这些方案在 kornia 中的落地形态:

  • torch._assert_async替代数据相关校验:kornia/enhance/adjust.py、kornia/enhance/equalization.py、kornia/augmentation/_2d/intensity/equalize.py、kornia/augmentation/_2d/intensity/clahe.py均使用它。例如equalize.py的注释明确说明:该函数内部调用kornia.enhance.equalize,期望输入在[0, 1],对无法均衡化的值抛出RuntimeError;该检查正是torch._assert_async,它从 torch 2.13 起在 MPS 上有内核,而在更老的 MPS 版本上条件会被拿到 host 端读取——每次调用多一次设备同步,且在torch.compile下被跳过。这说明选型时不仅要考虑编译,还要考虑设备(CUDA/MPS)与版本差异。
  • 无分支门控gate * value:kornia/augmentation/base.py的__batch_prob_generator__(第 210-244 行)正是文档表格中「对 {0,1} 张量的门控」一行的实例。旧代码中if batch_prob.sum() == 1是数据相关的分支,现在被替换为无分支组合:batch_prob = batch_prob * elem_prob——batch_prob是 shape(1,)、取值{0, 1}的张量:选中批次得到elem_prob,未选中得到全零,与旧分支结果完全一致,却没有图中断。函数内的p == 1、p == 0、same_on_batch分支因为是 Python 值,trace 期即解析,不构成断点,予以保留。这个单点修复解除了约 13 个增强算子的编译阻塞,是「共享修复」杠杆价值的典型代表。
  • 删除数据相关的提前返回:kornia/losses/_utils.py的mask_ignore_pixels(第 25-59 行)是「返回不同类型的提前返回」一行的实例。旧实现可能对target_mask全True(没有像素被忽略)时提前返回;新实现去掉数据相关的 early-out,始终返回 mask——没有像素被忽略时 mask 全True,调用方的 masking 是 no-op,where保持target不变,与旧返回结果一致,但可 fullgraph 编译。ignore_index is None时直接返回(target, None)的检查保留,因为ignore_index是 Python 的Optional[int]。这一处修复解除了 dice/focal/tversky 等损失的编译阻塞。

验证契约:fullgraph == eager,且 eager 逐字节不变

图中断修复后要做两个相互独立的检查(SKILL.md 第 3 步):

  1. 编译与 eager 在同一条路径上一致:torch.allclose(torch.compile(fn, fullgraph=True)(x), fn(x), atol=1e-5)。
  2. 修复不得改变 eager 输出哪怕一个字节:在同一 seed 下分别保存main分支与你的分支上fn(x)的输出,用torch.equal(old, new)断言逐字节相同——不是allclose。这是契约:编译重构只有在现有用户拿到完全相同的数字时才可接受;如果无法做到逐字节一致(例如真正新增的 opt-in 模式),必须把改动门控在新的参数后面,让默认路径保持逐字节一致。
# on main: torch.manual_seed(0); torch.save(fn(x), "old.pt") # on branch: torch.manual_seed(0); assert torch.equal(fn(x), torch.load("old.pt", weights_only=True))

对边缘逻辑的无分支改写,要穷尽边界输入验证(例如bits的每一个取值 0..8),而不是只抽一个样本。凡是碰 RNG 的地方(augmentation base、generator),一次抽样的顺序变化都会破坏逐字节一致性——此时要以整个模块的测试套件原样通过作为佐证(见第 6 步)。

基准测试是实验,不是感觉

每一个被改动的函数都必须做前后基准测试(SKILL.md 第 4 步)。一个没加速任何东西(或拖慢 eager)的编译修复必须给出理由。基准测试要用实验标准来对待:

import torch.utils.benchmark as bench def us(f, *a): return bench.Timer(stmt="f(*a)", globals={"f": f, "a": a}).blocked_autorange(min_run_time=1.0).median * 1e6 eager = us(fn, *args) c = torch.compile(fn, fullgraph=True) c(*args) # warmup —— 首次调用包含编译 + allocator + cudnn autotune comp = us(c, *args)

让数字可信的方法论(偏离即等于在比噪声):

  • 计时前必须 warm up(首次调用付出编译/autotune/懒初始化成本)。绝不给单次调用计时——用blocked_autorange(统计性的多次中位数),你报告的是信号,而不是调度器抖动。
  • 前后对比必须在同一台机器、同一进程、背靠背进行。永远不要拿一台机器的数字去对比另一台。torch.utils.benchmark会处理 CUDA 同步;手写的time.time()包住 CUDA 调用测的是发射延迟,不是计算时间。
  • 使用真实形状与 batch(增强算子建议(32,3,256,256))——(1,3,8,8)的玩具输入会放大 Python 开销、掩盖 kernel 成本。记录数字时要同时记录硬件、torch 版本、commit、日期(Jetson 这类边缘芯片的数字只具方向性;GPU 领先性的论断需要数据中心级 GPU)。
  • 确认改写没有拖慢 eager(无分支「两个分支都算」与删除提前退出都会给 eager 增加工作量——要测出来)。把前后对比表放进 PR。

跨库对比:孤立数字毫无意义

kornia 的数字不能脱离其他库单独解读。使用仓库现成的基准脚本,不要手搓:

  • benchmarks/augmentation/flagship.py—— 旗舰套件:通过每个库的随机变换类 API(包含参数采样)对比 kornia(eager + compiled)vs torchvision v2、albumentations、OpenCV、PIL;支持--json导出。
  • benchmarks/augmentation/cross_library.py—— 聚焦的三方逐算子对比。
  • benchmarks/augmentation/pipeline.py—— 端到端多算子流水线吞吐(训练循环的真实形态);支持--compile与--half(fp16/AMP)。
  • benchmarks/filters/flagship.py、benchmarks/geometry/flagship.py—— 函数式算子的对应套件。

动手前先读benchmarks/augmentation/README.md——它记录了各性能区间的划分与常设的改进清单,也是持久结果与诚实解读的存放处(当你移动一个数字时要更新它)。运行示例(该 README 给出):

python benchmarks/augmentation/flagship.py --batches 1,8,32 --size 256 --threads 4 --device cpu --compile --json aug_cpu.json python benchmarks/augmentation/flagship.py --batches 1,8,32 --size 256 --threads 4 --device cuda --compile --json aug_cuda.json python benchmarks/augmentation/pipeline.py --batch 32 --size 224 --device cuda --compile

每个脚本都会按benchmarks/README.md的方法论契约打印 git commit、平台、(CUDA 下)设备名;未安装的可选库会被报告为 skip 行而不是让运行失败。

诚实地阅读列:性能区间不是同一种竞赛

benchmarks/augmentation/README.md中的「regimes」表说明这些库解决的不是同一个问题,单读某一列就断言「谁赢了」是误导:

库数据批量设备可微分
korniafloatBCHW张量✅ 批量CPU或 GPU✅ 是
torchvision v2floatBCHW张量✅ 批量CPU 或 GPU❌ 否
albumentationsuint8HWCnumpy❌ 逐图循环仅 CPU❌ 否
OpenCV(cv2)uint8HWCnumpy❌ 逐图循环仅 CPU❌ 否
PIL(Pillow)uint8HWC图像❌ 逐图循环仅 CPU❌ 否
kornia-rsuint8HWCnumpy❌ 逐图仅 CPU(原生 Rust)❌ 否

由此得到两个泾渭分明的竞赛:

  • CPU /uint8/ 单图——albumentations、OpenCV、PIL 与 kornia-rs 的领地,是经典数据加载器区间,SIMD/原生后端在此取胜。kornia 的float路径不是为此设计的,不应期望领先。
  • GPU /float/ 批量 / 可微分——kornia 的主场。torchvision v2 在原始吞吐上可以竞争,但不可微分;其他库根本跑不了这里。这是 kornia 设计上要领先的区间,也是torch.compile融合能兑现收益的地方。

按此诚实口径陈述所在区间:OpenCV / kornia-rs 赢 CPU/uint8/单图,torchvision v2 赢原始 float 张量吞吐,kornia 的区间是 GPU 批量 + 可微分 + 编译——也是唯一存在可微分、设备内(on-device)增强的区间。

10× vs albumentations 的登月目标

增强包的性能目标是对 albumentations 的 10×——每次性能修复都要以此为目标框架。现状是 kornia 在 CPU 上落后,能缩小乃至反转差距的杠杆有三条:(a)torch.compile(即你刚做的修复,约 2–3×);(b)uint8快速路径(albumentations 的全部优势就是 uint8 + OpenCV;kornia 会升到 float32);(c) 把热点算子下沉到kornia-rs(albumentations 无法匹敌的 Rust 后端)。一个只做到持平的编译修复只是通往 10× 的一步,不是终点——PR 里要说明还有哪条杠杆在桌上。

从benchmarks/augmentation/README.md记录的实测可见这些杠杆的现实佐证:在 2026-09-18 的 Intel i7-14700K + RTX 4090 快照(batch 32,img/s)中,CUDA 编译使 RandomGaussianBlur 提升 4.78×、ColorJiggle 提升 3.15×;但编译也会输——CPU 上 GaussianBlur 为 eager 的 0.93×、水平翻转 0.75×、仿射 0.45×。这正说明「编译不是所有工作负载的最优解」,任何修复都必须在真实区间里衡量。同一文档还记录了 Jetson Orin 上torch.compile的✗是 Jetson wheel 的 CUDA 驱动限制、Resize的nan是 Orin wheel 的cusolver缺陷——这类数字必须附上设备 caveat 再引用。

持久、可引用的数字存放在benchmarks/results/,渲染在docs/get-started/performance;新增结果后用python docs/generate_benchmarks.py --refresh-llms刷新 LLM 摘要。

锁定成果:test_dynamo 与 PR 时间的 dynamo CI

第 5 步是把修复锁进回归防线。为被改动的 op 添加/确认test_dynamo,且必须锻炼真实路径——编译完整前向(torch.compile(op, fullgraph=True)(x),不传预生成的params),而不是只编译传入参数的apply_transform,这样参数生成也被覆盖。这一点在tests/augmentation/test_augmentation.py中有直接的测试形态:torch.compile(RandomHorizontalFlip(p=1.0), fullgraph=True)(input).shape == input.shape的断言之外,还显式torch._dynamo.reset()后再编译完整前向,注释写明「同时编译完整前向(内部生成参数),使参数生成路径也是 fullgraph,而不只是 apply_transform」。

PR 时间(PR-time)的dynamoCI 任务(.github/workflows/pr_test_cpu.yml)现在会在CI 的 torch 版本上用inductor后端跑一遍编译干净的核——主测试矩阵默认会跳过这些用例(KORNIA_TEST_OPTIMIZER=""时被 deselected),因此:

  • 本地运行必须显式设置KORNIA_TEST_OPTIMIZER=inductor,否则这些测试不会真正执行;
  • 如果你把算子加进了 scoped-core 目录,CI 任务会自动覆盖它——要在那里确认通过,而不是只在你本地 torch 上通过。该 CI 任务的pytest-extra明确为-k "dynamo or compile",覆盖 tests/augmentation、tests/enhance、tests/feature、tests/filters、tests/color、tests/geometry、tests/losses、tests/morphology、tests/models 等目录。

验证整个套件未变,然后按规范提交 PR

第 6 步:用--dtype=float32跑该 op 的完整测试文件。对于共享 helper 或 augmentation base,要跑整个模块的测试套件——无分支改写可能改变 RNG 的消耗顺序(例如原本条件式的抽样现在总是执行),套件原样通过才是「没有 seed 化测试依赖它」的证明。这一步与第 3 步的逐字节断言互为印证:逐字节保证单个 op 的输出,模块套件保证 RNG 顺序变化没有渗透进任何已 seed 的测试。

第 7 步:一个 PR 只放一个 op(或一个共享 helper)。PR 正文必须包含:断点(the break)、真正的修复(the genuine fix)、fullgraph == eager 的证据、以及基准对比表。最后用.pre-commit-config.yaml中锁定的版本跑ruff check与ruff format --check,保持零告警。

杠杆优先:共享修复胜过逐算子修补

收益最大的永远是共享 helper / 基类——一处真正的修复能解锁许多算子。SKILL.md 给出的两个实例,都在源码中得到了印证:

  • kornia/augmentation/base.py的__batch_prob_generator__门控 → 解锁约 13 个增强算子(该函数由forward_parameters调用,处于每个随机增强算子的必经路径上);
  • kornia/losses/_utils.py的mask_ignore_pixels→ 解锁 dice/focal/tversky 损失。

因此当一个算子编译失败时,先 trace 到最深的kornia/帧——断点往往在某个共享工具里,在那里修杠杆远高于逐算子修。

动手之前:先扫描已有的编译干净核

SKILL.md 最后提醒:kornia 的数值核心大部分已经可编译(filters、color、geometry transforms、losses、morphology、metrics)。在假设存在断点之前,先扫描确认——把精力投在真正需要修复的地方,而不是重复解决已解决的问题。

小结:一份可直接执行的编译修复清单

  1. 复现断点:torch.compile(fn, fullgraph=True)(x)捕获精确的 graph-break 位置(kornia/…内的 traceback 帧),绝不猜测。
  2. 分类并修复:对照六类断点表应用单路径修复;is_compiling()一律禁用;不适用任何一类时重新设计或请示维护者,不要强打补丁。
  3. 双验证:编译与 eager 同路径allclose;eager 输出与main逐字节torch.equal;RNG 相关改动以模块套件通过佐证。
  4. 基准前后对比:warm up +blocked_autorange+ 同机背靠背 + 真实 shape;记录硬件/版本/commit/日期;用benchmarks/现成 harness 做跨库对比;按区间诚实解读;对照 10× 目标说明还有哪条杠杆未动。
  5. 锁定:test_dynamo必须覆盖完整前向(含参数生成);本地以KORNIA_TEST_OPTIMIZER=inductor运行;确认 PR 的dynamoCI 在 CI torch 版本上通过。
  6. 套件回归:op 测试文件以--dtype=float32全绿;共享 helper 跑整个模块套件。
  7. 提交 PR:一 op 一 PR;正文含断点、修复、等价证据、基准表;ruff双检查干净。

【免费下载链接】kornia🐍 Geometric Computer Vision Library for Spatial AI项目地址: https://gitcode.com/GitHub_Trending/ko/kornia

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进