transformers 里的 Qwen3.5 建模代码:28KB 的定义展开成 88KB 的实现

2026-08-24

想搞清楚 Qwen3.8-27B 的结构,最权威的参考是 transformers 里的建模代码。但打开那个目录会发现一个问题:同一个模型有两份实现文件。

modeling_qwen3_5.py   88,546 字节
modular_qwen3_5.py    28,781 字节

一份 88KB,一份 28KB。读哪个?

答案是:想快速看懂结构,读 28KB 那份;想查具体某个算子怎么实现,读 88KB 那份。 这篇讲为什么,以及从小的那份里能读出什么。

这篇的依据

来源是 transformers 主干仓库 src/transformers/models/qwen3_5/ 目录下的文件,采集时间 2026-08-24

我们没有运行过这些代码、没有加载过模型。 下面是从源码的导入语句和类定义结构里读出来的组织关系。

目录里有什么

完整的五个文件:

文件字节
modeling_qwen3_5.py88,546
modular_qwen3_5.py28,781
configuration_qwen3_5.py8,266
tokenization_qwen3_5.py3,127
__init__.py1,035

另有一个平行的 qwen3_5_moe 目录,供同架构的稀疏版本使用。

有独立的 tokenization_qwen3_5.py 这件事值得留意——不是所有模型都需要自己的分词器实现,多数会直接复用前代的。结合这个模型 248,320 的词表,说明分词这一层确实有专门处理。

那份 28KB 里几乎全是继承

打开 modular_qwen3_5.py,看类定义会发现一个鲜明的特征——几乎每一个类的括号里都写着别人的名字

class Qwen3_5TextConfig(Qwen3NextConfig):
class Qwen3_5VisionConfig(Qwen3VLVisionConfig):
class Qwen3_5Config(Qwen3VLConfig):
class Qwen3_5VisionRotaryEmbedding(Qwen3VLVisionRotaryEmbedding):
class Qwen3_5TextRotaryEmbedding(Qwen3VLTextRotaryEmbedding):
class Qwen3_5GatedDeltaNet(Qwen3NextGatedDeltaNet):
class Qwen3_5Attention(Qwen3NextAttention):
class Qwen3_5MLP(Qwen3NextMLP):
class Qwen3_5RMSNorm(Qwen3NextRMSNorm):

没有一个是从零写的。 每个类都基于前代的某个类,只写出差异部分。

这就是这个文件为什么只有 28KB——它不是完整实现,是一张「相对于前代改了什么」的差异清单。而那份 88KB 的 modeling_qwen3_5.py,是把这些继承全部展开之后的完整独立实现。

这个组织方式对读代码的人极其友好。 想知道 Qwen3.5 相对 Qwen3-Next 和 Qwen3-VL 做了哪些改动,读 28KB 那份就够了,改动全在那儿;不想被继承链绕晕、要看某个方法的完整逻辑,就去读 88KB 那份,那里一切都是平铺的。

继承关系印证了双血统

把上面那份类清单按父类分组,一个清晰的模式就出来了:

继承自 Qwen3Next 的Qwen3_5TextConfigQwen3_5GatedDeltaNetQwen3_5AttentionQwen3_5MLPQwen3_5RMSNorm——全是语言模型主干相关的部件。

继承自 Qwen3VL 的Qwen3_5ConfigQwen3_5VisionConfigQwen3_5VisionRotaryEmbeddingQwen3_5TextRotaryEmbedding——全是多模态和位置编码相关的部件。

语言主干来自 Qwen3-Next,多模态外壳来自 Qwen3-VL。

这和 vLLM 那边的继承结构完全一致——那边也是解码层与主干继承 Qwen3-Next、顶层条件生成类继承 Qwen3-VL(见 64 层在 vLLM 里怎么分流)。

两个独立的代码库,做出了同样的血统划分。 这不是巧合,而是因为它们都在如实反映这个模型本身的构成方式。这也给 config.json 里全是 qwen3_5 的代际关系 提供了又一份佐证:代际不只体现在命名上,代码是实打实继承下来的。

有意思的是文件里还导入了 Qwen3ForCausalLM——来自更早的 Qwen3。三代的东西同时出现在一个文件里,是这类快速迭代的模型系列的常见景象。

一个导入语句透露的信息

导入区里有这么一行:

from ...masking_utils import create_causal_mask, create_recurrent_attention_mask

两种掩码工具,对应两种注意力。

create_causal_mask 是标准因果掩码,服务于那 16 层全注意力——保证每个位置只能看到自己和之前的内容。

create_recurrent_attention_mask 里的 recurrent(循环)指向的是那 48 层线性注意力。循环这个词本身就说明了机制:它不是靠掩码矩阵限制可见范围,而是靠一个逐步推进的状态来携带历史信息。

这是继 vLLM 把实现放在 layers/mamba/gdn/ 目录下、llama.cpp 里出现 A_logdt_bias 参数名之后,第三个独立来源指向同一个结论:这个模型的线性注意力走的是状态空间那套机制。

三个框架、三种代码组织方式、同一个答案。 到这个份上,这个结论基本可以当作确定的事实了。

为什么要维护两份而不是一份

看到同一个模型有两份实现,第一反应可能是「这不是冗余吗,改一处忘了另一处怎么办」。

这个顾虑是对的,但两份文件的定位决定了它不会成为问题:一份是源,一份是产物。 人写的是 modular_modeling_ 是从它展开出来的。维护只发生在一处。

那为什么产物也要提交进仓库,而不是用的时候现生成?原因是展开后的版本对使用者更友好

设想你在调试一个问题,想看 Qwen3_5Attention 的前向传播怎么走。如果只有继承式的版本,你得先跳到 Qwen3NextAttention,发现它又继承自别的类,再往上跳——继承链可能跨越三四个模型目录。而展开后的版本里,所有代码都平铺在一个文件里,从上往下读就行。

对一个被大量用户直接阅读源码的库来说,这个体验差别很重要。很多人是把建模文件当文档读的——他们想知道这个模型到底怎么算,而不是想知道它继承了谁。

反过来,对维护者来说,继承式的写法能保证不同代次之间的一致性。Qwen3-Next 修了个 bug,继承它的 Qwen3.5 自动就修好了;如果是复制粘贴出来的两份代码,就得记得两边都改。

这个「用继承写、按展开发布」的模式,本质上是在两类读者之间做平衡:维护者要复用,使用者要平铺。

顺带说一下另外两个文件的定位。configuration_qwen3_5.py 只有 8KB,它定义的是配置类——也就是把 config.json 里那些字段映射成 Python 对象的那部分,包括各字段的默认值和校验逻辑。想知道某个配置字段的官方默认值是多少,查这个文件比翻文档快。

tokenization_qwen3_5.py 只有 3KB,是最小的一个。分词器的实现主体通常在通用组件里,这个文件只放这个模型特有的部分。

读代码时该怎么用这两份文件

给一个实用的顺序建议:

第一步,读 modular_qwen3_5.py 的类定义部分。 只看类名和括号里的父类,五分钟就能建立起「这个模型由哪些部件构成、每个部件继承自谁」的整体地图。

第二步,读那些类里实际写了内容的方法。 继承体系下,写出来的都是改动过的部分——这些就是这一代的真正区别所在

第三步,需要细节时再去 modeling_qwen3_5.py 比如你想看 Qwen3_5GatedDeltaNet 的前向传播到底怎么算,展开后的版本里不用跳转继承链就能一路读完。

反过来先读 88KB 那份是低效的——你会在大量与前代相同的样板代码里,找不出哪些是这个模型特有的。

小结

  • transformers 里同一个模型有两份代码:modular_ 是继承式的差异定义(28KB),modeling_ 是展开后的完整实现(88KB)
  • modular_qwen3_5.py几乎所有类都是继承来的,只写差异
  • 继承关系分两支:语言主干继承 Qwen3Next,多模态与位置编码继承 Qwen3VL
  • 这与 vLLM 的继承结构完全一致,两个独立实现印证了同一个双血统
  • 导入了 create_causal_maskcreate_recurrent_attention_mask 两种掩码,对应两类注意力层
  • 读代码先读 modular 建立地图,再按需去 modeling 查细节

更多拆解在 Qwen3.8-27B 专题

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。