编程 PyTorch 判 BC 破坏性:2 个 release + 180 天弃用窗口,和 57 处漏写 stacklevel 的警告

2026-10-14 23:38:24

PyTorch 判 BC 破坏性:2 个 release + 180 天弃用窗口,和 57 处漏写 stacklevel 的警告

PyTorch 的 PR 评审里有一份 BC 判定准则:.claude/skills/pr-review/bc-guidelines.md。顶层原则只有一句:任何改变任何用户可见行为的改动都潜在地破坏 BC(ANYTHING that changes ANY user-visible behavior is potentially BC-breaking)。评审时要把这类改动归到三种之一——实践中未被用到的行为、bug 修复、自愿的破坏性变更。

判定表

改动判定
删除公共函数/类破坏,需弃用期
重命名公共 API破坏,需弃用期
修改函数签名(删除/调换参数顺序)破坏,需弃用期
新增无默认值的必选参数破坏,应改为提供默认值
修改参数默认值潜在破坏,需写入 release notes
修改返回类型破坏,需弃用期
删除/重命名/更新私有 API潜在破坏,需用全局 GitHub 搜索验证 PyTorch Core 之外无使用者

表外还有两条容易漏的:改默认设备(default device)算破坏,且需要显式迁移方案;新增异常抛出或修改异常类型算潜在破坏。

破坏性变更什么时候可以做

四个条件同时满足才放行:

  1. 已添加弃用警告,且至少一个完整 release 携带 deprecation warning;
  2. 迁移路径已文档化,用户知道怎么改自己的代码;
  3. release notes 已更新;
  4. 收益充分,破坏性变更换来的改进足够显著。

FutureWarning 而不是 DeprecationWarning

面向用户可见 API 的弃用必须用 FutureWarning。DeprecationWarning 默认被过滤,用户在默认配置下根本看不到警告。同时必须指定 stacklevel=2,让警告指向用户代码而不是库内部。

import warnings

def old_function(x, old_arg=None, new_arg=None):
    if old_arg is not None:
        warnings.warn(
            "old_arg is deprecated and will be removed in a future release. "
            "Use new_arg instead.",
            FutureWarning,
            stacklevel=2,
        )
        new_arg = old_arg

属性默认值变更同理,弃用期内要保持旧行为:

def function(x, normalize=None):
    if normalize is None:
        warnings.warn(
            "normalize default is changing from False to True in v2.5",
            FutureWarning,
        )
        normalize = False  # Keep old default during deprecation

评审 checklist 基本是判定表的镜像:无删除公共 API(或带正当弃用路径);无修改签名(或新参数带默认值);无修改默认值(或已加警告);无修改返回类型/shape(或有迁移文档);无修改异常类型(或新类型继承自旧类型);弃用用 FutureWarning 而非 DeprecationWarning;弃用设置 stacklevel=2。

弃用窗口:2 个 release + 180 天

PyTorch's Python Frontend Backward and Forward Compatibility Policy 自 PyTorch 1.12 生效,把窗口写成了可查的数字:警告需覆盖 2 个 PyTorch release + 180 天 nightly。

如果切换到新行为会造成 silent breakage(静默破坏),旧行为还要继续抛 runtime error,再持续 2 个 release + 180 天 nightly。所以稳定组件、无警告运行的程序至少还能跑 2 个 release;遇到静默破坏的场景窗口是 4 个 release,nightly 用户是 360 天。

其余几条细节:每个进程只警告一次,用 TORCH_WARN_ONCE;文档用 Sphinx 的 .. deprecated:: 标记弃用、.. versionchanged:: 记录变更;不应把当前稳定行为弃用去换 prototype/beta 功能,要等替代方案稳定后再弃用;任何会影响旧序列化 TorchScript 模型的破坏性变更,必须写 upgrader。

PEP 387 / PEP 702:至少 2 个 minor,偏好 5 年

Python 官方的 PEP 387 给的是同一套逻辑的另一组数字。除非走弃用流程,API 行为不得在任意两个连续 release 之间以不兼容方式变化;Python 的年度发布流程意味着弃用期至少两年。移除前偏好等 5 年——比如 3.10 起警告、3.15 才移除。警告应出现在至少两个同大版本的 minor 版本里,或一个旧大版本的 minor 版本里。

PEP 387 还区分了软硬弃用。Soft Deprecation 指 API 仍可用但不再开发新功能,只在文档里说明、不发运行时警告;Hard Deprecation 才在运行时发 DeprecationWarning。软弃用若转为常规弃用,仍须遵守 Backwards Compatibility Rules。警告信息应包含「预计变成默认的 release」加一个可反馈的 issue 链接。可行时给 typeshed 加 @deprecated 装饰器(PEP 702),让静态类型检查器也能提示。

stacklevel 的真实坑

issue #60548 是 ezyang 在 2021 年提的:大量 deprecation warning 指向库内部文件行号,例如

/data/users/ezyang/.../torch/_tensor.py:557: UserWarning: floor_divide is deprecated...

用户拿到这条警告,定位不到是自己哪行代码触发的。

PR #182208 做了统计:torch/ 下 57 处 warnings.warn() 调用、跨 38 个文件缺 stacklevel。修法是新增 torch/_warn_utils.py 统一封装 warn():Python ≥ 3.12 用 skip_file_prefixes=(_TORCH_PREFIXES) 跳过 torch/ 内所有栈帧,不再依赖脆弱的 stacklevel 计数;3.10/3.11 退回 stacklevel=2。

这里有个不好抄的点:3.12+ 下 skip_file_prefixes 的跳过发生在 stacklevel 应用之后,深层嵌套的内部调用与 3.10/3.11 的行为仍有差异——旧 Python 上只回退两层,仍可能指向内部代码。同一个封装修补在不同解释器版本上并不等价。

回到工程结论:光有 semver 不够。semver 的前提是先声明「什么算公开 API、什么算破坏性变更」;没有这个定义,贴版本号也只是让判断因人而异。PyTorch 用一张判定表加一组 release/天数把这件事钉住,代价是每个 PR 都得先回答「这算不算用户可见」。而 stacklevel、skip_file_prefixes 这些细节说明,弃用窗口写对了只是第一步,警告真正落到用户那一行才是。

推荐文章

程序员茄子在线接单