
Kornia 修复 pinned-torch CI 中 triton 孤儿依赖导致的 Dynamo/ONNX 导出崩溃【免费下载链接】kornia Geometric Computer Vision Library for Spatial AI项目地址: https://gitcode.com/gh_mirrors/ko/kornia导读本文剖析 Kornia 仓库中一次典型的构建环境漂移问题修复对应 changelog.d/migration-096.fixed.md在 pinned-torch CI 矩阵中uv sync依据 lockfile 安装的是 PyPI 版 torch而矩阵各腿随后将 torch 单独替换为cpu构建——该构建的 wheel 完全不声明triton导致 lockfile 里的 triton 变成孤儿最终 torch 2.6.0 腿在 32 个 Dynamo / ONNX 导出测试的导入阶段集体崩溃。文章将完整还原问题机理、安装步骤的修复方案、环境校验脚本check_torch_env.py的实现细节以及测试侧external_dataFalse的兼容性处理读者读完可以掌握这类按依赖元数据精确换装 torch的 CI 加固套路并可直接复用其中的环境一致性校验思路。一、问题现场2.6.0 腿在导入阶段集体阵亡1.1 两个安装阶段的叠加效应Kornia 的 tests.yml 构建 CI 环境分两步uv sync安装 lockfile此时安装的 torch 来自 PyPI其Requires-Dist中在 Linux x86_64 Python 3.13 条件下声明了triton (3.2.0)这类 sidecar 依赖参见 test_check_torch_env.py 中的PYPI_WHEEL_REQUIRES矩阵腿单独换装 torchpinned通道执行uv pip install --reinstall-package torch --index-url https://download.pytorch.org/whl/cpu torch$PYTORCH_VERSIONtests.yml仅替换 torch 本身。问题在于CPU wheel 索引的cpu构建声明了比 PyPI wheel 更窄的依赖集——完全不声明triton。changelog 明确注明这一点在 2.5.1、2.6.0、2.9.1 和 2.14.0 上均已核实见 check_torch_env.py 的模块 docstring。测试代码中CPU_WHEEL_REQUIRES变量给出了 torch-2.6.0cpu wheel 的完整Requires-Dist列表确实只有 filelock、typing-extensions、networkx、jinja2、fsspec、setuptools、sympy、opt-einsum、optree 等没有 tritontest_check_torch_env.py。1.2 孤儿 triton 为什么会被 torch 捡起来uv sync为 lockfile 的 torch例如 2.14.0装的 triton3.8.0在换装后仍然残留在虚拟环境中。更关键的是 torch 的分发机制torch 对 inductor triton 路径的判断依据是导入是否可用而非版本是否匹配_is_triton_available()只测试import triton是否成功。于是torch 2.6.0 换上后发现环境中存在可导入的 triton便走 inductor 的 triton 路径但该 triton 是 3.8.0早已移除了 torch 2.6.0 需要的from triton.backends.compiler import AttrsDescriptor这个名称结果 32 个 dynamo 和 export 测试在导入阶段直接抛 ImportError——且这个腿从未绿过。这正是一个典型的孤儿依赖被静默拾取问题安装元数据wheel 声明与运行时分发逻辑按 import 可用性产生了错位。二、修复方案安装步骤同步瘦身验证步骤整体断言2.1 安装步骤丢弃未声明的 sidecar 包修复后torch 换装步骤在完成--reinstall-package torch之后立即追加一次对账tests.ymlreport$(pixi run -e ... uv run --no-sync \ python .github/scripts/check_torch_env.py --list-undeclared-sidecars | tr -d \r) # 结果行必须存在否则视为解析失败而非环境干净 if ! printf %s\n $report | grep -q ^undeclared-sidecars:; then echo the sidecar check reported no result; refusing to assume the venv is clean 2 exit 1 fi stale$(printf %s\n $report | sed -n s/^undeclared-sidecars:[[:space:]]*//p | tail -n 1) if [[ -n $stale ]]; then echo Removing packages the installed torch does not declare: $stale pixi run -e ... uv run --no-sync uv pip uninstall $stale fi这段逻辑的关键点有三对账发生在换装步骤内部而不是单独一个步骤。工作流注释明确解释如果把对账放在独立的步骤里它可能被重排序到换装之前——那时环境里还是 lockfile 的 torch它声明triton检查结果为空问题就会在全绿的运行中幸存下来tests.yml。把对账放进换装自己的步骤里顺序就不可重排了。空捕获等于失败。脚本总是会打印undeclared-sidecars:前缀若 grep 不到该前缀说明解析坏了工作流会拒绝假设环境是干净的。这个约定同时被 test_check_torch_env.py 的TestWorkflowWiring测试用grep -q ^undeclared-sidecars:断言钉死。允许名单是精心收缩的SIDECAR_PACKAGES (triton, pytorch-triton)check_torch_env.py。脚本刻意不做已安装但不在已装 torch 闭包内的通用 diff——因为虚拟环境里绝大部分包kornia 自身的依赖、测试工具链本来就合法地不在 torch 的闭包内且安装元数据不记录来源diff 无法区分孤儿与根依赖。换装后幸存的其他包lockfile 的nvidia-*、cuda-*wheel只占磁盘、在cputorch 上不会被加载因此无害。未来若出现新的按可用性分发的 sidecar再往名单里加。2.2 验证步骤对整个环境断言而非只查torch.__version__原先只读torch.__version__的验证方式无法发现其余包仍属于另一个 torch的 venv。修复后的 Verify PyTorch version 步骤调用resolved$(pixi run -e ... uv run --no-sync \ python .github/scripts/check_torch_env.py \ --channel $TORCH_CHANNEL --expected $EXPECTED_PYTORCH_VERSION \ | tr -d \r | sed -n s/^torch-version: //p | tail -n 1) if [[ -z $resolved ]]; then echo the environment check did not report a torch version 2 exit 1 fitests.yml。其中有两个被测试钉死的契约只有通过的检查才打印torch-version:行。若在失败路径也打印误配的 venv 仍会给工作流一个看起来有效的答案shell 的-z守卫就会变成死代码test_check_torch_env.py通道语义pinned要求版本精确等于矩阵值忽略cpu本地段stable把矩阵值当作地板因为上游探测装的是 stable 索引当天解析出的版本check_torch_env.py。验证步骤同时把解析出的实际版本写入GITHUB_OUTPUT供后续按版本命名的产物工件使用。三、check_torch_env.py一个可复用的环境一致性校验器check_torch_env.py 是本修复的唯一事实来源工作流的注释都指向它。其核心函数如下。3.1runtime_requirements按 marker 求值当前依赖读取已装 torch 的Requires-Dist用packaging.requirements.Requirement解析并把extramarker 置空后求值——只保留无 extra、纯运行时条件下成立的依赖check_torch_env.py。测试test_drops_extras_and_false_markers验证了 setuptoolspython_version 3.12和 opt-einsum / optree仅 extra都会被正确过滤test_check_torch_env.py。3.2scan_installed按首个 sys.path 条目优先 同位置冲突检测distributions()按sys.path顺序遍历、首个匹配胜出——这与importlib.metadata.version和普通import的解析结果一致所以first-wins是诚实答案。但同一分发位置内出现两个name-version.dist-info目录则没有先后可言那正是半成品 venv例如被中断的uv pip install --reinstall-package torch的残留会被单独报告出来而不是由文件系统顺序随机裁决check_torch_env.py。测试用临时目录构造冲突 dist-info 验证该行为test_check_torch_env.py。3.3undeclared_sidecarscheck_environment四类问题的完整诊断for name in undeclared_sidecars(requires, installed, environment): problems.append( f{name} {installed[name]} is installed, but torch {torch_version} declares no dependency on it: torch dispatches onto an importable sidecar without checking its version (#4199) )check_environment汇总四类问题check_torch_env.py版本不符不满足通道约束缺失依赖torch 声明了但环境里没有版本滞留依赖还停在 lockfile 版本未随换装移动——测试test_flags_a_requirement_left_at_the_locked_version用sympy 1.14.0复现了这一点孤儿 sidecar装了但当前 torch 未声明#4199 主故障同包多版本一个名字出现多个冲突版本即半成品 venv。失败时诊断信息会附带一张name version需求表缩进两空格打印到 stderr且只在确实有错时才打印——健康腿不会被约 15 行的噪声刷屏成功时打印torch-version: x.y.z并返回 0check_torch_env.py。四、测试侧修复ONNX 导出改为external_dataFalse4.1 被 import 错误掩盖的五个用例triton 崩溃发生在 32 个 dynamo / export 测试的导入阶段把tests/core/test_small_linalg.py里 5 个 ONNX 用例的真实问题也一并掩盖了。环境修好后这些用例暴露出 torch 2.6 dynamo 导出器的一个兼容性差异。4.2 BytesIO 目标下的导出器差异test_small_linalg.py 的_export_kwargs展示了完整的分层兼容逻辑kwargs {opset_version: 18} if torch_version_ge(2, 5, 0): kwargs[dynamo] use_dynamo_exporter if use_dynamo_exporter and torch_version_ge(2, 6, 0): kwargs[external_data] False return kwargs原因链非常清晰源码注释逐条给出这些用例把图导出到BytesIOdynamo 导出器默认external_dataTrue会把手柄直接交给 onnxscript 的save_model_with_external_data——它需要的是文件系统路径于是在 torch 2.6 上翻译成功后才抛出TypeError: expected str, bytes or os.PathLike object, not BytesIO2.9.1 和 2.14.0 通过更新的 onnxscript framework API 能接受 buffer这些图只有少量节点、没有 initializer任何版本下都没有外部数据要写所以external_dataFalse无副作用该参数必须用torch_version_ge(2, 6, 0)门控因为external_data在 2.6 之前不存在——而_export_kwargs承诺对 kornia 支持的所有 torch 版本都返回合法 kwargs。4.3 导出成功本身就是门槛的测试哲学该文件的另一个设计要点test_small_linalg.py缺失 lowering 会直接抛异常在 2.9.1 上实测legacy 导出器抛UnsupportedOperatorErrordynamo 导出器抛ConversionError所以导出调用成功本身就是最硬的门槛。_assert_basic_arithmetic_only正则_LINALG_OP_RE则补上另一种情况防止未来某个分解把 linalg 运算成功导出成重操作——它特意用子串匹配aten_linalg_inv这种 dynamo 导出的 fallback 节点命名并用锚定规则避免qr误吞Sqrt、lu误吞Relu/Selu/Elu/Celu/PRelu/LeakyRelu/ThresholdedRelu全家test_small_linalg.py。五、测试对脚本与工作流契约的双向锁定本修复的可维护性很大程度上来自 test_check_torch_env.py 对脚本与工作流两端契约的联合断言TestWorkflowWiring直接读取 tests.yml 文本断言换装步骤内先--reinstall-package torch后--list-undeclared-sidecars顺序是修复的全部意义、uv pip uninstall $stale存在、验证步骤含--channel $TORCH_CHANNEL --expected $EXPECTED_PYTORCH_VERSION与-z $resolved守卫、以及工作流解析的前缀与脚本打印的前缀一致test_check_torch_env.py。注释特别说明任一侧改名前缀另一侧会静默退化为 no-op。TestMain强调main是工作流 grep 并据此门控的表面stdout 前缀是tests.yml解析的对象且只有通过的检查才打印版本——破坏任一约定工作流步骤会退化成保持绿色但什么都不做的 no-optest_check_torch_env.py。测试代码里还保留着MIS_PROVISIONED这个罪证2.6.0cpu torch 3.8.0 triton 2.14.0 闭包——正是 #4199 描述的误配环境的字面快照test_check_torch_env.py。六、经验总结可迁移的三条 CI 加固原则依赖换装必须连带对账当你把某个包从 lockfile 的版本换成另一个通道/构建如 PyPI →cpuwheel时不要只换包本身还要按已装包声明的依赖闭包清理幸存者——尤其警惕按 import 可用性分发的 sidecar。验证要断言整个环境而不是单点版本只查torch.__version__发现不了其余包仍属于另一个 torch的 venv。把所有运行时依赖齐备、无未声明 sidecar、无重复版本作为验证契约让误配在 setup 阶段就失败而不是变成一堵看起来毫不相关的测试失败墙。用测试锁定工作流与脚本的双向契约把脚本打印什么前缀、工作流 grep 什么前缀、步骤顺序如何写成读取工作流文件本身的测试任何一侧被破坏都会在 PR 阶段暴露而不是在 CI 上静默退化。【免费下载链接】kornia Geometric Computer Vision Library for Spatial AI项目地址: https://gitcode.com/gh_mirrors/ko/kornia创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考