首页
/ Transformers ty 类型检查实战:make typing 失败后的定位、修复优先级与仓库验证闭环

Transformers ty 类型检查实战:make typing 失败后的定位、修复优先级与仓库验证闭环

2026-09-05 22:50:59作者:蔡怀权

本文以 Transformers 官方的类型检查修复技能(.ai/skills/add-or-fix-type-checking/SKILL.md)为主体,完整还原从 make typing 或 CI 日志失败,到 ty check 获取聚焦基线、按 12 级优先级修复错误、再到回归验证与 CI 覆盖扩展的全流程。读完本文,你将掌握 Transformers 仓库中类型错误的分类分诊方法、union 收窄 / @overload / 泛型容器 / Protocol self 注解 / TypeGuard 等修复手法的具体用法与禁忌,并能独立处理本地运行、CI 或 PR 日志中出现的 typing 失败。

类型检查在 Transformers 质量体系中的位置

在动手修错之前,先弄清楚 make typing 到底跑的是什么。从 Makefile 可以看到,类型检查被定义为独立的一组 checker:

# Makefile
TYPING_CHECKERS := types, modeling_structure

# Runs ty type checker and model structure rules
typing:
	@python utils/checkers.py $(TYPING_CHECKERS)

make typing 实际上运行两个环节:

  1. types:调用 utils/check_types.py,对一组“已纳入类型检查范围”的模块执行 ty check
  2. modeling_structure:调用 utils/check_modeling_structure.py,基于外部 mlinter 包对 modeling_*.py / modular_*.py / configuration_*.py 的文件结构做规则检查。

types checker 内部真正执行的命令是(见 utils/check_types.py):

ty check --respect-ignore-files --error-on-warning --exclude '**/*_pb*' <check_args...>

其中 --error-on-warning 的意义在源码注释中写得很明确:不加它时,warning 级别的诊断(例如 possibly-missing-attribute)只打印但 ty 以 0 退出,本地和 CI 都会“静默通过”,问题就永远修不到 commit 之前。

当前被检查的根路径清单在 utils/check_types.pycheck_args 中维护,例如 src/transformers/_typing.pysrc/transformers/clisrc/transformers/modeling_utils.pysrc/transformers/utilssrc/transformers/generationsrc/transformers/quantizers 等。也就是说:Transformers 的类型检查是按“已 typed 区域”逐步扩展的,并非全仓检查。

另一份关键配置在 pyproject.toml[tool.ty] / [tool.ty.rules] 段。仓库对一批容易产生误报或需要大改才能通过的规则显式设为 ignore,例如:

  • not-subscriptable = "ignore":张量切片(如 self.position_ids[:, :seq_length])的误报;
  • unresolved-import = "ignore":可选依赖(mlx、torch_npu、habana_frameworks 等)在运行时才检查;
  • invalid-argument-typeinvalid-return-type = "ignore":涉及复杂 union 收窄与大规模重构的返回类型不匹配;
  • call-non-callable = "ignore":Mixin 模式(类同时被当作类型与可调用对象使用)。

理解这份“豁免清单”很重要:它解释了为什么修复指南把大量精力放在 union 收窄、@overload、泛型、Protocol self 注解 上——这些是 ty 实际会报错、且仓库没有整体豁免的规则类别。

第一步:从失败的运行中确定检查范围

技能文档给出的第一个动作是“从失败运行中提取范围”:

  • 如果你手里已经有 make typing 或 CI 的输出,直接从中提取失败的文件/模块路径;
  • 如果没有任何现成输出,先在仓库根目录运行:
make typing
  • 然后选择能覆盖所有失败点的最窄目标 <target>(可以是单个模块、单个文件或目录)。

选择最窄目标的原因是 ty 会跟随 import 追踪到被检查根之外的模块(这也是 utils/check_types.py 中把缓存键设置为整个 src/transformers/**/*.py 的原因)。范围越窄,输出的基线噪声越少,分诊越快。

第二步:用 ty check 获取聚焦基线

确定 <target> 后,运行:

ty check --respect-ignore-files --exclude '**/*_pb*' <target>

两个参数的含义:

  • --respect-ignore-files:尊重 ignore 文件,与 make typing 的实际行为保持一致;
  • --exclude '**/*_pb*':排除 protobuf 生成物(*_pb*),它们是自动生成的、不受类型检查约束的代码。

注意:本地手动跑这条命令时没有 --error-on-warning,所以 warning 不会导致非零退出码——但修复时应把 warning 也当作需要处理的问题,否则 CI 里开着 --error-on-warningmake typing 仍会挂。

第三步:修复前先按类别分诊

技能文档强调:修复任何代码之前,先把错误分好类。常见类别有六种:

  1. 函数签名上错误或缺失的类型注解;
  2. 对 union 类型(如 X | None)做属性访问;
  3. 函数返回过宽的 union(如 str | list | BatchEncoding);
  4. Mixin / Protocol 的 self 类型问题;
  5. 对象或模块上的动态属性访问;
  6. 第三方 stub 缺陷(缺 kwarg、缺 __version__ 等)。

分类的意义在于:不同类别对应完全不同的修复工具。把“属性访问失败”归入第 2 类就该用收窄,而不是第 5 类就该用 TypeGuard——混用工具是本技能文档“禁忌清单”中大多数反模式的根源。

修复优先级 a:用 isinstance() / if x is None / hasattr() 收窄 union

这是解决 union 类型错误的第一工具ty 能通过所有这类模式做收窄,包括否定形式:

# 收窄 X | None —— 用 `if ...: raise`,绝不用 assert
if x is None:
    raise ValueError("x must not be None")
x.method()  # ty 在此处知道 x 是 X

# 收窄 str | UploadFile
if isinstance(field, str):
    raise TypeError("Expected file upload, got string")
await field.read()  # ty 在此处知道 field 是 UploadFile

# 在函数体开头就收窄宽 union 参数
# (常见于接受 list | dict | BatchEncoding 之类的方法)
if isinstance(encoded_inputs, (list, tuple)):
    raise TypeError("Expected a mapping, got sequence")
encoded_inputs.keys()  # ty 现在只看得到 dict/mapping 类型

要点:先否定不需要的那个分支并直接 raise,让检查器在后续代码中只看到剩余类型。这种写法同时改进了运行时行为(提前暴露坏输入),是“类型修复”与“代码质量”双赢的写法。

修复优先级 b:用局部变量让 ty 追踪跨闭包的收窄

self.x 的类型是 X | None,且需要把它传给嵌套函数或闭包时,ty 无法追踪 self.x 保持非 None 这一事实(因为实例属性随时可能被改写)。正确做法是先拷贝到局部变量,再收窄局部变量:

manager = self.batching_manager
if manager is None:
    raise RuntimeError("Manager not initialized")
# 在嵌套函数中使用 `manager`(而不是 self.batching_manager)

规则很具体:收窄只对被赋值的局部名生效self.attrif self.attr is None: raise 之后,对外部作用域仍是 X | None,闭包内引用它就会继续报错。

修复优先级 c:拆分链式调用(中间类型是宽 union 时)

如果 func().method() 报错,原因是 func() 返回了一个 union——ty 无法穿过链式调用做收窄。拆成三步:先取值,再收窄,再链:

# BAD: ty can't narrow through chained calls
result = func(return_dict=True).to(device)["input_ids"]

# GOOD: split, narrow, then chain
result = func(return_dict=True)
if not hasattr(result, "to"):
    raise TypeError("Expected dict-like result")
inputs = result.to(device)["input_ids"]

这与 Transformers 里 model(**inputs, return_dict=True) 一类返回 ModelOutput | dict 的接口非常契合:先接住返回值,用 hasattr 断言它具备所需方法,再继续操作。

修复优先级 d:在源头修正错误的类型注解

如果一个参数标注为 X | None,但实际调用链中它永远不可能None,正确做法是从注解里删掉 None,而不是在函数体里加防御性的 if x is None。这对应禁忌清单中的一条:不要为调用链保证非 None 的值添加 if x is not None 守卫——应修注解本身。

修复优先级 e:为未标注的属性补注解

两类典型场景:

  • __init__(或其他地方)赋值的实例变量:补上类型注解,例如 self.foo: list[int] = []
  • 稍后动态赋值的类级属性:显式声明,例如 _cache: Cache_token_tensor: torch.Tensor | None

声明类级属性而不立即赋值,是让检查器知道“这个属性将来会以什么类型存在”的最小手段。

修复优先级 f:用 @overload 表达“返回值依赖输入类型”

当方法的返回类型随输入类型变化时(典型如 __getitem__:str key 返回一种类型、int key 返回另一种),用 @overload 把每种签名分别声明:

from typing import overload

@overload
def __getitem__(self, item: str) -> ValueType: ...
@overload
def __getitem__(self, item: int) -> EncodingType: ...
@overload
def __getitem__(self, item: slice) -> dict[str, ValueType]: ...

def __getitem__(self, item: int | str | slice) -> ValueType | EncodingType | dict[str, ValueType]:
    ...  # actual implementation

收益:调用点从此拿到精确的返回类型,消除调用处的 cast()。注意 pyproject.tomlno-matching-overload 被设为 ignore——仓库容忍 overload 匹配上的误报,但 overload 带来的精度提升仍是消除 cast() 的首选路径。

修复优先级 g:让容器类变泛型,传播值类型

当类似 UserDict 的容器类持有的值在某个变换后类型会变化(例如 .to() 之后 list 变成 tensor),把类改成泛型,让方法能返回收窄后的类型:

from typing import Generic, overload
from typing_extensions import TypeVar

_V = TypeVar("_V", default=Any)  # default=Any keeps existing code working

class MyDict(UserDict, Generic[_V]):
    @overload
    def __getitem__(self, item: str) -> _V: ...
    # ...

    def to(self, device) -> MyDict[torch.Tensor]:
        # after .to(), values are tensors
        ...
        return self  # type: ignore[return-value]

设计要点:

  • default=Any(来自 typing_extensions)保证未参数化的用法(MyDict())仍是 MyDict[Any]——存量代码一行不用改
  • 只有真正收窄值类型的方法(如 .to())才声明具体返回类型;
  • 效果是所有调用点都不再需要 cast(),比在调用点逐个 cast 干净得多。

修复优先级 h:Mixin 用 self: "ProtocolType" 标注宿主接口

当 Mixin 访问其宿主类的属性时,为它定义一个 Protocol,并在需要的方法上标注 self;且要对 Mixin 的所有方法一致地应用。type-only 导入放在 TYPE_CHECKING 块下以避免循环依赖。

这不是纸上谈兵——Transformers 仓库中这是 GenerationMixin 的标准做法。共享 Protocol 定义在 src/transformers/_typing.py

class GenerativePreTrainedModel(Protocol):
    """Protocol for the model interface that GenerationMixin expects.

    GenerationMixin is designed to be mixed into PreTrainedModel subclasses. This Protocol documents the
    attributes and methods the mixin relies on from its host class. It is *not* used at runtime — its
    purpose is to help the ``ty`` type checker resolve ``self.<attr>`` accesses inside the mixin.
    """
    config: Any  # PretrainedConfig — kept as Any to avoid circular imports
    device: torch.device
    dtype: torch.dtype
    generation_config: Any
    def can_generate(self) -> bool: ...
    # ...

其 docstring 直接说明了用途:该 Protocol 运行时不参与,唯一目的就是帮助 ty 在 Mixin 内部解析 self.<attr> 访问。而在 src/transformers/generation/utils.py 中,GenerationMixin 的方法大规模、一致地使用 self: "GenerativePreTrainedModel" 标注(如 generate_validate_model_kwargs_supports_logits_to_keep 等十余处方法签名),且 Protocol 的导入位于文件顶部的 if TYPE_CHECKING: 块中(见 generation/utils.py)。这正是技能文档第 h 条在仓库中的完整落地范例。

修复优先级 i:用 TypeGuard 处理“动态模块属性”

对运行时才确定存在的模块属性(torch.nputorch.xputorch.compiler 等),不要写 getattr(torch, "npu")hasattr(torch, "npu") and torch.npu.is_available(),而是定义一个类型守卫函数:

def has_torch_npu(mod: ModuleType) -> TypeGuard[Any]:
    return hasattr(mod, "npu") and mod.npu.is_available()

然后把它直接用作 if 条件:

if has_torch_npu(torch):
    torch.npu.device_count()

守卫成立后,ty 把该模块视为 Any,属性访问无需 getattr()cast()。仓库中 TypeGuard 已有成熟用法可参照:

  • src/transformers/trainer_utils.pyhas_length(dataset: Any) -> TypeGuard[Sized]:try 调用 len(dataset),成功则收窄为 Sized,捕获 TypeError / AttributeError 返回 False——这是“运行时探测 + 类型收窄”的标准组合;
  • src/transformers/distributed/utils.pyis_dtensor(obj: object) -> TypeGuard[DTensor]:在 TYPE_CHECKING 下导入 DTensor,函数体内惰性 importisinstance 判断。

技能文档特别给出三条 TypeGuard 关键规则,必须逐条遵守:

  1. TypeGuard[Any](不要用 Protocol)——这是与 ty 兼容的最简形式,且不会丢失原模块已知的属性;
  2. 守卫函数必须在 if 条件中被直接调用收窄才生效。ty 不会穿过 and 组合条件、也不会穿过 if not guard: return 的否定早退做收窄;
  3. 必须直接导入守卫函数from .._typing import has_torch_xxx),不能以模块属性形式 _typing.has_torch_xxx 调用——ty 只从直接导入中解析 TypeGuard

修复优先级 j:用 getattr()/setattr() 处理动态的 model/config 属性

对运行时注入的字段(config/model 上动态添加的 flag),读用 getattr(obj, "field", default),写用 setattr(obj, "field", value)。对缺类型 stub 的第三方包同样适用,例如 getattr(safetensors, "__version__", "unknown")

边界要说清楚:getattr() 不适用于 torch 的动态设备后端(npuxpuhpumusamluneuroncompiler)——那些场景走上一条的 TypeGuard,两条工具分工明确、互不替代。

修复优先级 k:cast() 是 type: ignore 之前的最后手段

cast() 用于“结构上已经验证过类型、但检查器看不到”的场景,例如:模式匹配后的 AST 节点、已知类型的 dict 值、已验证的 API 响应:

# After structural validation confirms the type:
stmt = cast(cst.Assign, node.body[0])
annotations = cast(list[Annotation], [])

两条禁令:模块属性收窄不要用 cast()(用 TypeGuard);当 @overload 或泛型能在源头解决问题时,不要用 cast() 在调用点打补丁。

修复优先级 l:# type: ignore 只留给第三方 stub 缺陷

# type: ignore 的合法场景只有一个:第三方包的类型 stub 错误或残缺,且无法通过收窄或 cast 绕过。典型例子:

  • 运行时存在、但 stub 里缺失的 kwarg;
  • 运行时存在、但 stub 未声明的方法。

必须带具体错误码# type: ignore[call-arg],而不是裸的 # type: ignore。裸 ignore 会掩盖将来同一行出现的其他错误,也让 make typing 的排查成本陡增。

禁忌清单:Things to Never Do

技能文档把以下做法列为绝对禁止,与上面的修复优先级互为镜像:

  • 绝不用 assert 做类型收窄。assert 会被 python -O 剥离,不能依赖其保证正确性——用 if ...: raise
  • 绝不把 # type: ignore 当第一手段,先用尽 a–k 所有方法;
  • 不用 getattr(torch, "backend") 访问动态设备后端(npuxpuhpumusamluneuroncompiler)——用 TypeGuard;
  • 不用 cast() 做模块属性收窄——用 TypeGuard;
  • @overload 或泛型能在源头消除时,不用 cast()
  • 不要为了糊弄类型检查器而添加辅助方法或抽象(尤其是只为 1–2 处出现而设的);
  • 不要把领域特定字段污染进基类——用 Protocol;
  • 不要为调用链保证非 None 的值加 if x is not None 守卫——修注解;
  • 不要用条件继承(conditional inheritance)模式——标注 self

组织规范:共享类型放在哪里

  • 共享的 Protocol 与类型别名统一放在 src/transformers/_typing.py。当前该文件已包含 TransformersLoggerGenerativePreTrainedModelPeftConfigLikeWhisperGenerationConfigLike 等 Protocol,以及 LevelExcInfoDeviceMeshLike 等类型别名,新协议应追加在此而非散落各处;
  • type-only 符号一律放在 if TYPE_CHECKING: 下导入,避免循环依赖(_typing.py 自身的 import torch 即为该模式,见 src/transformers/_typing.py);
  • 使用 from __future__ import annotations 以获得 PEP 604 联合语法(X | Y)——_typing.pygeneration/utils.py 均以此开头。

验证:闭环 PR 的验证顺序

修复完成后按此顺序回归:

  1. 同一个 <target> 重新运行 ty check --respect-ignore-files --exclude '**/*_pb*' <target>,确认基线归零;
  2. 重新运行 make typing,确认 typesmodeling_structure 两个步骤都通过(前者是 ty check --error-on-warning,后者是 mlinter 结构规则,两者独立,缺一不可);
  3. 如果目标是合入就绪(merge readiness),运行 make check-repo——从 Makefile 看,它执行 types + 风格检查 + 仓库一致性检查(--keep-going),是合入前的全量自检;
  4. 确认运行时行为没有变化,并跑相关测试。类型修复的正确姿势是“检查器可见的世界变了,运行时字节码没变”——if ...: raise、局部变量拷贝、注解、@overload 声明都是纯静态改动,如果出现行为差异说明改错了地方。

新增已 typed 区域时:更新 CI 覆盖范围

当你把新的目录纳入类型检查(例如新增一个模块并为其补齐注解),还必须让 CI 覆盖它。技能文档表述为更新 Makefile 中的 ty_check_dirs;从当前仓库源码结构看,实际的检查根清单维护在 utils/check_types.pyCHECKER_CONFIG["check_args"] 列表中(Makefiletyping 目标经由 utils/checkers.py 调度该 checker,清单本身不在 Makefile 内)。操作要点:

  • 把新目录/文件加入 check_args 列表(可参考现有条目,粒度到目录,如 src/transformers/cli,也可到单文件,如 src/transformers/modeling_utils.py);
  • 注意 cache_globs 机制(utils/check_types.py):缓存键覆盖了整个 src/transformers/**/*.py,任何源码变更都会使 types 检查的缓存失效并强制重跑——你不需要为新目录改缓存配置,但新目录在 CI 里将每次全量检查,选根路径时考虑耗时。

小结

Transformers 的类型检查体系可以浓缩为一句话:make typing = ty check(带 --error-on-warning)+ mlinter 结构规则,检查范围由 utils/check_types.pycheck_args 白名单界定,规则豁免由 pyproject.toml[tool.ty.rules] 界定。修复工作时,按“先分诊、后按 a→l 优先级修复”的路径走:收窄与 raise 优先于 cast@overload/泛型优先于调用点打补丁,Protocol 与 TypeGuard 分别解决 Mixin self 与动态模块属性,# type: ignore 只留给第三方 stub 缺陷且必须带错误码。修完后以 ty check <target>make typingmake check-repo → 相关测试的顺序闭环,新增 typed 区域则同步更新 check_args 让 CI 接住它。这套方法论完全基于仓库中 SKILL.mdMakefileutils/check_types.pysrc/transformers/_typing.pypyproject.toml 的实证配置,可直接在本地、CI 或 PR 修复场景中复用。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
924
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
599
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
394