transformers 里的 Qwen3.5 建模代码:28KB 的定义展开成 88KB 的实现
想搞清楚 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.py | 88,546 |
modular_qwen3_5.py | 28,781 |
configuration_qwen3_5.py | 8,266 |
tokenization_qwen3_5.py | 3,127 |
__init__.py | 1,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_5TextConfig、Qwen3_5GatedDeltaNet、Qwen3_5Attention、Qwen3_5MLP、Qwen3_5RMSNorm——全是语言模型主干相关的部件。
继承自 Qwen3VL 的:Qwen3_5Config、Qwen3_5VisionConfig、Qwen3_5VisionRotaryEmbedding、Qwen3_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_log 和 dt_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_mask和create_recurrent_attention_mask两种掩码,对应两类注意力层 - 读代码先读 modular 建立地图,再按需去 modeling 查细节
更多拆解在 Qwen3.8-27B 专题。