ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Kornia 迁移片段解读:半精度网格坐标一致性、Boxes 非批量容器与 Adoption 依赖页面

Kornia 迁移片段解读:半精度网格坐标一致性、Boxes 非批量容器与 Adoption 依赖页面

Kornia 迁移片段解读:半精度网格坐标一致性、Boxes 非批量容器与 Adoption 依赖页面

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

本文基于 Kornia 仓库changelog.d/+migration-020.*三份迁移片段,逐条拆解该变更集背后的问题根因、修复实现与配套工具链。读完你将掌握:半精度下create_meshgrid与torch.compile数值分叉的成因与修复思路、Boxes.pad/unpad/clamp对非批量(N, 4, 2)容器的支持细节,以及 Kornia 文档站点依赖生态(Adoption)页面的数据抓取与渲染流程。

文档背景:changelog.d 迁移片段机制

Kornia 使用 Towncrier 下新增一个changelog.d/<PR>.<type>.md文件,发布时由工具合并进CHANGELOG.md,避免多人同时编辑同一段落的合并冲突(详见 changelog.d/README.md)。

文件类型分三类:added(新功能与面向用户的文档)、fixed(行为修正)、breaking(不兼容变更,需同时描述新旧行为)。本批+migration-020三个片段正是这套机制的"存量迁移"部分——+前缀是 Towncrier 的 orphan 前缀,用于避免重复生成 PR 链接,文件名保留到下一个版本发布时被消费。

片段类型变更主题关联 PR
+migration-020.added.mdaddedAdoption 文档页面#4155
+migration-020.breaking.mdbreaking半精度网格坐标一致性#4231
+migration-020.fixed.mdfixedBoxes.pad/unpad/clamp非批量容器#4372

预览与发布命令定义在 pixi.toml 中,工具被隔离在独立的 uv 环境中运行,不污染 Kornia 运行时依赖:

pixi run changelog-preview # 预览待发布条目(不修改文件) pixi run changelog-build --version X.Y.Z # 组装发布说明并消费片段

半精度网格坐标一致性修复(breaking,PR #4231)

这是本批变更中技术含量最高的一条:create_meshgrid与create_meshgrid3d在float16/bfloat16下返回的归一化坐标,会因模型是否被torch.compile包裹而产生不同结果,且最大偏差可达 0.0117~0.0156。它关联 issue #4030,属于数值语义的 breaking 变更。

问题根因:eager 与编译路径的舍入次数不同

旧实现对归一化公式(xs / (size - 1) - 0.5) * 2在 eager 模式下按三个独立的半精度算子依次执行,每步都舍入一次;同时 eager 路径会先把半精度 ramp 完整物化出来,再由 graph-capture 分支将其加宽。而torch.compile(Inductor)把整条链在float32下计算、仅存储时舍入一次,并折叠掉"先收窄再加宽"的往返——于是当尺寸超过2 ** p(p 为尾数位数,即 bfloat16 的 8 位、float16 的 11 位)时,两条路径除法的分子都不一致了。

数值后果示例([-1, 1]归一化区间):

  • bfloat16、宽度 300:偏差最大约0.0117;
  • bfloat16、宽度 6000:偏差最大约0.0156。

也就是说,半精度网格在模型被套上torch.compile的那一刻就会悄然变化,且变化量与图像宽度相关——宽度越大,坐标数值越大,两条路径的舍入分歧越明显。

修复方案:float32 构建、末尾收窄一次

修复思路是让所有执行路径的算术完全一致:半精度归一化网格改为全程在float32下构建与归一化,最后只收窄一次。这样 eager、traced、compiled 三条路径的中间计算完全相同,仅剩最终的舍入差异,eager 结果也移动到"正确舍入"一侧(这正是数值变化本身)。

从源码看,核心实现位于 kornia/geometry/grid.py:

ramp_dtype = dtype if dtype is not None else torch.empty(0, device=device).dtype widened = normalized_coordinates and ramp_dtype in (torch.float16, torch.bfloat16) work_dtype = torch.float32 if widened else ramp_dtype xs = torch.arange(width, device=device, dtype=work_dtype) ys = torch.arange(height, device=device, dtype=work_dtype) # ... 归一化计算(align_corners 分支)... if widened: # The single rounding, after the whole normalization. xs = xs.to(ramp_dtype) ys = ys.to(ramp_dtype)

值得注意的源码细节:

  • 为什么用arange而非linspace:源码注释(kornia/geometry/grid.py)指出linspace先把端点舍入到坐标 dtype 再反向填充上半段,只有当size - 1能被精确表示(size <= 2 ** p + 1)时步长才恰好为 1,否则会偏离正确舍入的列索引约一个 ulp;而 Inductor 对linspace的降级实现按索引逐点计算反而舍入正确,导致 eager 与编译结果不一致。改用arange后两种模式一致,且保留了导出(export)场景下符号长度的动态性。
  • ramp dtype 的读取方式:通过torch.empty(0, device=device).dtype读取而非torch.get_default_dtype(),因为 TorchScript 没有该内置函数;空张量带上device参数使读取依赖实参,避免 no-input 工厂调用被 TorchScript 的常量折叠冻结为首次运行时的默认 dtype。
  • 缩放系数用张量形式:graph-capture 分支使用torch.scalar_tensor保持符号尺寸动态,并保留align_corners两种约定(角点对齐(xs / (w - 1) - 0.5) * 2与半像素映射(2x + 1) / w - 1),单例轴用xs * 0.0归一到中心 0。

影响范围:哪些结果不变,哪些变了

片段明确划定了边界:

  • 完全不变:float32、float64、整数网格以及像素 ramp(它们都不涉及加宽运算),逐位(bit-for-bit)保持一致;
  • 仍可能差 1 ulp:CUDA 上float16网格,因为 ATen 与 triton 的float32除法并非逐位相同;bfloat16则能吸收该差异,CPU 内核完全一致;
  • 基线随之移动:Linux CPU 半精度测试基线(testing/half_precision_xfails/cpu_bfloat16.txt、cpu_float16.txt)随新数值更新。

HomographyWarper 的联动修复

HomographyWarper(kornia/geometry/transform/homography_warper.py)在构造时用create_meshgrid预计算float32网格,而 forward 时按输入 dtype 动态生成网格——两条路径此前不一致,修复后完全对齐。这顺带修复了:

  • test_homography_warper在float16、bfloat16两个 dtype 下的失败;
  • test_translation[shape1]在float16下的失败;
  • test_translation[shape0]、[shape2]在bfloat16下也加入已知失败列表:该测试atol=1e-4在 bfloat16 下对全部 4 个 shape 都不可达,此前两个 shape 能通过,只是因为旧网格更大的误差在grid_sample的 bfloat16 反归一化中恰好round-trip成了精确的像素索引——属于"错错得正",修复后暴露了真实的精度下限。

验证入口:tests/geometry/test_grid.py中的test_create_meshgrid(像素网格形状与角点值逐位校验)、test_normalize_pixel_grid(像素/归一化网格互转一致)、test_normalized_meshgrid_singleton_axis_is_centered(单例轴归一到 0)。

Boxes.pad / unpad / clamp 支持非批量容器(fixed,PR #4372)

Boxes类(kornia/geometry/boxes.py)的文档约定支持两种容器:批量(B, N, 4, 2)与非批量(N, 4, 2)。但pad、unpad、clamp三个方法此前只按批量形式编写索引逻辑,对非批量容器直接崩溃,issue #4244 记录了该问题。

问题表现

  • pad/unpad:填充量按(B, 1, 1)广播,与(N, 4)的坐标视图不匹配,抛出RuntimeError: output with shape [1, 4] doesn't match the broadcast shape [1, 1, 4];
  • clamp:用repeat(1, _data.size(1), 4)物化边界,对二维数据抛IndexError: too many indices for tensor of dimension 2;
  • 同样的 boxes 只要在前面加一个单例 batch 轴,三个方法都能正常工作——可见问题纯粹是"未按实际 rank 分派"。

修复实现:按容器 rank 广播

修复核心是新增_broadcast_over_vertices辅助方法(kornia/geometry/boxes.py),把逐图像(per-image)的(B, k)列向量整形为能对self._data[..., i]广播的形状:

def _broadcast_over_vertices(self, values: torch.Tensor) -> torch.Tensor: if self._is_batched: return values[..., None, :] # (B, k) -> (B, 1, k),适配 (B, N, 4) 索引 if values.size(0) != 1: raise RuntimeError( f"Unbatched (N, 4, 2) boxes carry a single image, so a per-image tensor must have one row. " f"Got {values.size(0)}." ) return values # (1, k) 已可直接广播到 (N, 4)
  • 批量容器索引为(B, N, 4),列向量需要补一个 box 维度;
  • 非批量(N, 4, 2)只承载单张图像,(1, k)列已能广播,不得再加维度;语义上单张图像对应一个逐图像张量,因此多于一行即拒绝(RuntimeError)。

pad(kornia/geometry/boxes.py)与unpad(kornia/geometry/boxes.py)只对(left, top)两个改变坐标原点的分量做加减(left/top影响原点,right/bottom只影响尺寸),clamp(kornia/geometry/boxes.py)则把每个顶点独立夹取到(B, 2)的上下界内。批量结果(含非有限边界)逐位一致。

NaN 边界的比较语义

clamp的实现特意保留"比较式"语义而非maximum/minimum:对 NaN 边界的每次比较都为False,因此NaN 边界不会污染坐标——坐标保持原值而不是变成 NaN(见 kornia/geometry/boxes.py 的注释)。这是与朴素torch.clamp式的maximum/minimum实现的本质区别。

实战示例与回归测试

import torch import kornia # 非批量容器 (N, 4, 2):单张图像,padding 只能有一行 boxes = kornia.geometry.Boxes(torch.rand(2, 4, 2)) boxes.pad(torch.tensor([[2.0, 0.0, 3.0, 0.0]])) # (1, 4) -> 广播 boxes.clamp( topleft=torch.tensor([[0.0, 0.0]]), botright=torch.tensor([[16.0, 16.0]]), ) # 批量容器 (B, N, 4, 2):逐 batch padding boxes_b = kornia.geometry.Boxes(torch.rand(2, 3, 4, 2)) boxes_b.pad(torch.tensor([[1.0, 0.0, 1.0, 0.0], [2.0, 0.0, 2.0, 0.0]]))

该修复的实际触发路径是RandomCrop的 padding 分支(见 kornia/geometry/boxes.py 的文档说明):秩为 3 的(B, N, 4)bbox 构建批量容器,而单张图像的秩为 2 的(N, 4)bbox 构建非批量容器。回归测试test_convention_padded_random_crop_accepts_rank2_bboxes_4244(tests/augmentation/container/test_augmentation_sequential.py)覆盖了两种 box 数量(N=1 时 clamp 抛 IndexError、N=2 时抛 RuntimeError),以及test_random_crop.py中unbatched参数化用例(tests/augmentation/container/test_random_crop.py)。

Adoption 页面:依赖生态快照(added,PR #4155)

本批变更还新增了一个面向生态的文档页面:Adoption页(docs/source/community/adoption),在文档构建时从docs/source/_data/dependents.json渲染,列出依赖 kornia 的 star 数最多的 GitHub 仓库与软件包。该页面文件由脚本生成、不直接提交,编辑时应改fetch_dependents.py与 JSON 数据(见 docs/generate_adoption.py 文件头注释)。

数据抓取:docs/fetch_dependents.py

GitHub 的 REST/GraphQL API 都不暴露依赖列表,只有仓库页的 "Used by" 及network/dependentsHTML 页面可读。因此 docs/fetch_dependents.py 直接爬取该 HTML 列表:每页 30 条、按 cursor 分页,分别抓取repositories与packages两类,记录精确总数与 star 数最高的条目。

python docs/fetch_dependents.py # 全量爬取(约 330 次请求、约 10 分钟) python docs/fetch_dependents.py --max-pages 5 # 快速刷新总数 python docs/fetch_dependents.py --kind packages --out /path/to/dependents.json # 限定类型与输出

关键实现参数与策略(docs/fetch_dependents.py):

  • KEEP_TOP = 300:每类最多存储按 star 排序的前 300 条;
  • DELAY_S = 1.0:请求间隔,规避 GitHub 无鉴权限流;
  • _get带 5 次重试与指数退避,对 429/5xx 响应按Retry-After等待;
  • 部分爬取合并策略(merge函数):--max-pages或网络失败导致的部分结果会合并进已有 JSON——count始终为最新总数,排名只会越来越完整;完全爬取才替换存量排名;
  • 快照写入docs/source/_data/dependents.json,字段含generated_at、packages/repositories(各自含count、complete、listed、items),items元素为{name, stars, forks}。当前仓库中的快照记录于 2026-09-02,packages 总数 321(见 docs/source/_data/dependents.json)。

页面渲染:docs/generate_adoption.py

docs/generate_adoption.py 在文档构建期把 JSON 渲染为 Adoption 页(rST):顶部以卡片形式展示"多少个 packages 构建在 Kornia 之上 / 多少个 repositories 依赖 Kornia",随后是两个表格——TOP_PACKAGES = 30的包列表与TOP_REPOSITORIES = 30的仓库列表(Repository / Stars / Forks 三列)。load_counts()同时把精确数字注入rst_epilog替换符(|count-dependents|等),供其他页面引用而无需重复拷贝。

统计口径与"添加你的项目"

页面明确说明统计口径:只统计 GitHub 能读取到的 manifest 中声明了kornia的项目(requirements.txt、pyproject.toml、setup.py、environment.yml等),因此私有仓库、Docker 镜像、notebook 与拷贝源码不计入。列表完全由依赖图自动生成、按 star 排序——无需任何提交动作,项目声明依赖并在下次刷新快照后自然出现。

如何验证这批变更

  • 半精度网格:直接对比 eager 与torch.compile下create_meshgrid(300, 300, dtype=torch.bfloat16)的输出是否一致;或在HomographyWarper上运行tests/geometry/transform/test_homography_warper.py与tests/geometry/test_grid.py;
  • Boxes 修复:用单张图像的秩 2 bbox 走RandomCrop(padding=...)路径,或直接对(N, 4, 2)容器调用pad/unpad/clamp;
  • 文档页:执行python docs/fetch_dependents.py --max-pages 5刷新快照,构建文档后检查community/adoption页面的卡片与表格;
  • 变更日志:pixi run changelog-preview预览三条迁移片段合并后的渲染效果。

总结

+migration-020三份片段展示了 Kornia 变更日志"迁移期"的三类典型工作:breaking修复了半精度网格在 eager 与torch.compile之间的舍入分叉(float32 构建、末尾一次收窄,HomographyWarper 顺带对齐);fixed让Boxes的pad/unpad/clamp真正支持文档承诺的非批量(N, 4, 2)容器,并保持了 NaN 边界的比较式语义;added则建立了依赖生态快照的抓取-渲染流水线,为文档站点提供可自动刷新的 Adoption 页面。理解这三条变更的根因与边界,有助于在升级时评估对半精度训练、图像几何流水线与文档构建的实际影响。

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

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

RELATED READING

延伸阅读

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