首页
/ Hugging Face Transformers 模块化模型开发指南:用 modular 文件与自动转换器从继承中生成完整模型

Hugging Face Transformers 模块化模型开发指南:用 modular 文件与自动转换器从继承中生成完整模型

2026-09-04 17:59:39作者:庞眉杨Will

本篇技术文章基于 Transformers 仓库文档 modular_transformers.md,系统讲解 Modular Transformers(模块化模型)的工作机制:如何在一个 modular_<name>.py 文件中通过 Python 继承复用已有模型组件,再借助仓库内置的 linter/转换器自动生成符合"单模型、单文件"哲学的 modeling_*.pyconfiguration_*.py 等独立文件。读完本文,你将能够独立编写 modular 文件、掌握属性删除/方法覆盖/依赖推断等进阶模式,并理解底层转换器 modular_model_converter.py 的工作方式。

背景:为什么要 Modular Transformers

Transformers 的库设计遵循"一个模型、一个文件"(single model, single file)哲学。这一哲学的副作用是限制了跨文件的继承与导入,导致大量模型组件在不同模型目录中重复出现——src/transformers/models/ 下有成百上千个模型,注意力层、归一化层等实现高度雷同,却又各自独立维护,随着修复与功能变更逐渐分化。

早期仓库用 # Copied from 注释标记"这段代码是从别处复制来的",由 CI 和本地命令保证副本不漂移。但这种方式简单却繁琐,给贡献者带来额外负担。更现实的问题是:一个新模型的贡献往往需要约 1000 行建模代码、约 500 行处理代码,加上测试与文档,常常达到 3000–5000 行,且大多是样板代码,抬高了贡献门槛。

Modular Transformers 的目标就是把这个门槛降到可接受的水平:用"模态文件"(modular file)+ 自动 linter 替代 # Copied from,让贡献者只写"与父模型之间的差异"。

Modular 文件是什么

在模型目录中添加一个 modular_<name>.py 文件(<name> 与 snake_case 的模型目录名一致,如 src/transformers/models/olmo2/modular_olmo2.py),该文件接受 modeling/processing 文件中通常不被允许的两类写法:

  1. 从相邻模型导入:例如 from ..bert.modeling_bert import BertModel
  2. 类对类的继承:例如 class RobertaModel(BertModel)

modular 文件中定义的模型、处理器和配置类,会被 linter 拆解到各自对应的 modeling_*.pyprocessing_*.pyconfiguration_*.py 模块中。每次运行生成脚本都会自动重写这些文件,贡献者只需维护 modular 文件本身。最终用户仍然导入和使用熟悉的单文件接口(如 from transformers import RobertaModel),对外接口不发生任何变化——即"简单贡献"与"设计哲学"兼得。

modular_model_converter.py 的实现可以看到,生成文件顶部会插入一段强制警示(AUTO_GENERATED_MESSAGE,见 modular_model_converter.py#L44-L50):

This file was automatically generated from {relative_path}. Do NOT edit this file manually...

也就是说生成文件绝不能手改,CI 会强制校验 modular 文件与生成文件的同步性,相关校验逻辑位于 check_modular_conversion.py

依赖自动推断的细节

linter 会"展开"(unpack)继承关系,为 Python 用户保持生成结果的透明。核心规则(当前实现只展开一层继承):

  • 配置类增删参数RobertaConfigBertConfig 继承并新增/删除了参数时,生成的配置类会直接指向父文件(新增情况)或完全移除该参数(删除情况);
  • 模型子模块自动推断:如果 class GemmaModel(LlamaModel),linter 会推断所有子模块依赖并自动从父类所在模型生成对应类;
  • 新函数自动搬运:在 modular 文件中定义新函数并在类体内使用时,linter 会自动将其复制进被使用的生成文件。

理论上你完全可以把分词器、图像处理器、模型、配置都写在一个 modular 文件里,对应文件全部自动生成。

完整示例:从 BERT 生成 RoBERTa

文档给出的经典示例是 BERT 与 RoBERTa:两者紧密相关,官方实现几乎只差一个 embedding 层(填充位置可学习 vs 位置嵌入可学习)。仓库中对应示例见 modular_roberta.py。文档中的示例(此处保留原文档代码,注意 tokenizer 因差异过大暂被忽略):

from torch import nn
from ..bert.configuration_bert import BertConfig
from ..bert.modeling_bert import (
    BertModel,
    BertEmbeddings,
    BertForMaskedLM
)

# RoBERTa 配置与 BERT 相同
class RobertaConfig(BertConfig):
  model_type = 'roberta'

# 重新定义嵌入层以突出填充位置可学习的差异,并重新定义位置嵌入
class RobertaEmbeddings(BertEmbeddings):
    def __init__(self, config):
        super().__init__(config)
        self.padding_idx = config.pad_token_id
        self.position_embeddings = nn.Embedding(
            config.max_position_embeddings, config.hidden_size, padding_idx=self.padding_idx
        )

# RoBERTa 模型与 BERT 相同,除了嵌入层;上面已重定义嵌入层,这里无需额外工作
class RobertaModel(BertModel):
  def __init__(self, config):
    super().__init__(config)
    self.embeddings = RobertaEmbeddings(config)

# 任务头只需要把内部模型重新指向正确的 RobertaModel
class RobertaForMaskedLM(BertForMaskedLM):
  def __init__(self, config):
    super().__init__(config)
    self.model = RobertaModel(config)

一个重要的错误提示机制:如果你在 modular 文件中定义了某个依赖类却没有在父类 __init__ 里实际使用它,转换器会报错:

ValueError: You defined `RobertaEmbeddings` in the modular_roberta.py, it should be used
                                    when you define `BertModel`, as it is one of it's direct dependencies. Make sure
                                    you use it in the `__init__` function.

更多示例可在 examples/modular-transformers 目录中找到,其中同时存放了各 modular 文件与转换后的生成文件(如 modular_my_new_model.pymodeling_my_new_model2.py),便于对照输入/输出。批量运行转换的脚本为 convert_examples.sh

# 对 examples/modular-transformers 下所有 modular_*.py 执行转换器
sh examples/modular-transformers/convert_examples.sh

真实案例剖析:Olmo2 从 Olmo 继承

仓库中 src/transformers/models/olmo2/modular_olmo2.py 是文档所述流程的完整落地,可逐行印证上述规则。

配置类modular_olmo2.py#L48-L85):Olmo2Config 继承 OlmoConfig,新增 rms_norm_eps: float = 1e-5,而不再使用的 clip_qkv 则被赋值为 AttributeError(),linter 会在生成文件中彻底移除该属性:

@auto_docstring(checkpoint="allenai/OLMo-2-1124-7B")
@strict
class Olmo2Config(OlmoConfig):
    model_type = "olmo2"
    base_model_tp_plan = { ... }   # 张量并行切分策略
    base_model_pp_plan = { ... }   # 流水线并行切分策略
    rms_norm_eps: float = 1e-5
    clip_qkv = AttributeError()

无改动复用Olmo2RotaryEmbedding(OlmoRotaryEmbedding): pass(第 99-100 行)——仅继承 pass,linter 就会把父类内容完整复制并重命名引用。

行为差异Olmo2RMSNorm 相对 LlamaRMSNorm 只差一行——乘法发生在转回输入 dtype 之前而非之后(第 90-96 行);Olmo2Attention__init__super().__init__(...) 之后追加了 q_norm/k_norm 两行(第 114-117 行)。

属性删除Olmo2DecoderLayersuper().__init__ 后执行 del self.input_layernorm(第 173 行),因为 Olmo2 把归一化移到了注意力之后——这正是文档"高级用法"一节所讲的 del 语义的实例。

任务头零改动Olmo2ForCausalLM(OlmoForCausalLM): pass(第 222 行)。

文件末尾声明 __all__ 列表(第 230-236 行),转换器与下游导入都依赖它正确导出公开类。最终生成的独立文件见 modeling_olmo2.py,其中还包含 modular 文件从未显式定义的 Olmo2MLPOlmo2PreTrainedModel 等类——它们是 linter 根据 OlmoDecoderLayer 体内 self.mlp = OlmoMLP(config) 这类依赖自动推断生成的,等价于对父类写了 pass 继承。

进阶用法

1. 删除属性和方法

删除 nn.Module 属性:在 super().__init__(...) 之后写 del self.attribute。linter 只会移除父类复制体中 self.attribute = ... 这一赋值行;若父类的 forward 等其他方法还引用该属性,需要同时重写这些方法。文档示例:

class GemmaModel(LlamaModel):
    def __init__(self, config):
        super().__init__(self, eos_token)
        del self.embed_tokens   # 生成文件中仅删除该赋值行

对照 modular_olmo2.py#L167-L173 可以看到:del self.input_layernorm 只删除了赋值行,所以 Olmo2DecoderLayer.forward 必须完整重写(第 175-203 行),把归一化挪到注意力/FFN 之后。

删除方法:把该方法重写成 raise AttributeError("")(或自定义信息),linter 会直接从生成文件中移除该方法:

class GemmaTokenizer(LlamaTokenizer):
    ...
    def get_spm_processor(self):
        raise AttributeError("Not needed for Gemma")

    def unk_token_length(self):
        raise AttributeError("Not needed for Gemma")

2. 定义并使用新函数

在 modular 文件中定义新函数并在类体内调用它,转换器会自动把该函数(以及它递归调用的其他新函数)复制到使用它的生成文件中:

def my_new_function(*args, **kwargs):
  # Do something here
  pass

class GemmaModel(LlamaModel):
    def forward(*args, **kwargs):
      # Call the function
      example = my_new_function(*args, **kwargs)
      # continue here

仓库中的实例:modular_olmo2.py#L103-L107rotate_half 函数被 Olmo2Attention.forward 使用,因此会被自动搬运进生成的 modeling_olmo2.py

3. 显式调用 super() 的两种模式

super().__init__(config) 告诉转换器"把父类函数体复制进子类"。当你不希望展开 super() 调用、需要精确控制生成结果时,有两种模式:

模式一:直接调用祖父类。当生成的类应该直接继承 nn.Module 而非 modular 中的父类时,显式写出目标类:

# 左侧 modular 写法                          |   右侧生成结果
class GemmaTokenizer(LlamaTokenizer, PretrainedTokenizerFast):   |   class GemmaModel(nn.Module):
    def __init__(self, eos_token="</s>"):                         |       def __init__(self):
        eos_token = AddedToken(eos_token)                          |           eos_token = AddedToken(eos_token)
        PretrainedTokenizerFast.__init__(self, eos_token)          |           super().__init__(eos_token)

模式二:**super_kwargs。继承父方法的完整签名,同时可以附加自定义 docstring 或更换装饰器;在重写签名中写 **super_kwargs,linter 会在生成输出中展开父类全部参数。最典型用法是不重写签名就为 forward 补一段模型专属的 docstring:

# modular_gemma.py
class GemmaForCausalLM(LlamaForCausalLM):
    def forward(**super_kwargs):
        r"""
        Example:
        >>> from transformers import AutoTokenizer, GemmaForCausalLM
        ...
        """
        return super().forward(**super_kwargs)

4. 特殊命名与多模态前缀

linter 继承类时会自动重命名一切引用,因此同一文件内所有类必须使用统一前缀。对于 class GemmaVisionModel(CLIPModel) 这种"类名前缀(GemmaVision)≠ 模型前缀(Gemma)"的复合模型场景,转换器支持以 pass 显式重命名某个依赖来消除歧义:

class Emu3TextMLP(LlamaMLP):
    pass

当 linter 检测到多个候选前缀时会发出警告,提示只用最高频前缀推断依赖,并建议使用单一前缀(最佳实践)或显式子类化中间类。仓库的转换器中有对应的名字保护表 NAMES_TO_NEVER_REPLACEmodular_model_converter.py#L80-L91),避免误替换 mamba 等与模型名冲突的第三方标识符。

5. 装饰器继承

重写带装饰器的父方法时,父类装饰器会自动保留;若你加上自己的装饰器,则替换父类的。库中最常见的是中间输出捕获与 docstring 自动生成(@auto_docstring)两类装饰器,Olmo2Config 上的 @auto_docstring@strict(见上文案例)即为实例——注意这两个装饰器不会从父类继承,必须在 modular 配置类上显式声明。

运行转换器与生成文件

确认 modular 文件写好后,执行:

# 按模型目录名生成
python utils/modular_model_converter.py your_model

# 或直接指定文件
python utils/modular_model_converter.py --files_to_parse /path/to/modular_my_new_model2.py

转换器内部基于 libcst 构建 AST(而非原生 ast,因为原生 AST 不保留 docstring、注释和代码格式,参见 examples/modular-transformers/README.md),流程为:解析 modular 文件 → 对被导入的 transformers.models.modeling_xxxx 模块解析源码并构建类依赖映射 → 合并代码与依赖映射 → 用 ruff 清理重复导入。注意它只展开一层继承:若 Olmo2Attention 继承 OlmoAttention,生成的 Olmo2Attention 完全自包含(无跨模型导入、无继承);但若 OlmoAttention 本身还继承了别的类,祖父类不会被继续内联。

两条硬性纪律:

  1. 绝不手改生成文件——下次运行转换器时所有修改都会被覆盖,CI 也会拦截;
  2. 生成后验证真实权重可正确加载(写 convert_<model>_to_hf.py 转换上游 checkpoint,再 from_pretrained 核对 shape 与 key 匹配),仓库中每个模型的 convert_*_to_hf.py(如 convert_olmo2_weights_to_hf.py)都可作为模板。

它不是什么

最后必须明确边界:Modular Transformers 不是手写建模代码的替代品。如果你的模型不基于任何已有模型,或者配置/分词/处理文件无法从相近文件干净地继承,仍然应该按常规方式手写 modeling_*.py 等文件。此外,写 modular 文件还是先手写再重构,两种方式都可行,没有唯一正确的顺序。

小结与延伸阅读

主题 关键文件/命令
modular 示例集合(含生成结果对照) examples/modular-transformers
转换器实现(libcst AST + 依赖推断) utils/modular_model_converter.py
modular 与生成文件一致性校验 utils/check_modular_conversion.py
完整真实案例(Olmo2 ← Olmo ← Llama) src/transformers/models/olmo2/modular_olmo2.py
批量转换示例 sh examples/modular-transformers/convert_examples.sh
单模型转换 python utils/modular_model_converter.py <model_name>

Modular Transformers 本质上把"模型贡献"从"复制 + 改"变成了"继承 + 声明差异",同时用 linter 守护了 Transformers 的核心设计哲学。掌握 super().__init__ 展开、del/AttributeError() 删除、pass 继承、**super_kwargs 展开这四种语义,就能覆盖绝大多数新模型接入场景。

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

项目优选

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