SGLang 为什么给 Qwen3.8-27B 写了三个文件:入口拆分不是功能拆分

2026-08-24

SGLang 支持 Qwen3.8-27B 的代码分在三个文件里:qwen3_5.py 有 2457 行,qwen3_5_mtp.py 有 436 行,qwen3_5_text.py 只有 225 行。

第三个文件的体量很可疑。一个模型的「纯文本版本」为什么只要 225 行就能实现?

答案是它根本没实现什么——这三个文件的拆分依据是模型入口,不是功能模块。 搞清楚这一点,顺带能看到 SGLang 和 vLLM 在组织同一个架构时的一处明显分歧。

这篇的依据

来源是 SGLang 主干仓库 python/sglang/srt/models/ 下的三个文件,采集时间 2026-08-24

我们没有安装 SGLang、没有启动过服务、没有推理过一个 token。 下面说的都是从源码结构里读出来的组织方式,不涉及任何运行表现或性能比较。

入口是怎么声明的

SGLang 用一个约定来声明「这个文件提供哪些模型入口」——文件末尾的 EntryClass 列表。

三个文件各自的声明是这样的:

qwen3_5.py 的末尾(第 2457 行):

EntryClass = [Qwen3_5MoeForConditionalGeneration, Qwen3_5ForConditionalGeneration]

qwen3_5_text.py 的末尾(第 225 行):

EntryClass = [Qwen3_5MoeForCausalLM, Qwen3_5ForCausalLM]

qwen3_5_mtp.py 的末尾(第 436 行):

EntryClass = [Qwen3_5ForCausalLMMTP]

规律一眼可见:ForConditionalGeneration 是多模态入口,ForCausalLM 是纯文本入口,MTP 是第三个。 三个文件对应三类入口,一一对应。

Qwen3.8-27B 的 config.jsonarchitectures 写的是 Qwen3_5ForConditionalGeneration,所以它默认从第一个文件进。

那个 225 行的文件里是什么

打开 qwen3_5_text.py,第 40 行的类定义只有一行正文值得注意:

class Qwen3_5ForCausalLM(nn.Module):
    body_cls = qwen3_5.Qwen3_5ForCausalLM

body_cls 指向的是主文件里的同名类。文件顶部也确实有 from sglang.srt.models import qwen3_5 这行导入。

构造函数里把它实例化成 self.model

self.model = self.body_cls(
    config=config,
    quant_config=quant_config,
    prefix=add_prefix("model", prefix),
)

所以这 225 行是个壳。 真正的模型主体在 qwen3_5.py 里,这个文件只负责在外面套一层:加上 lm_head、加上 logits_processor、处理流水线并行的边界情况、暴露几个属性访问器。

packed_modules_mappingsupported_lora_modules 这两个类属性,都是直接从主文件的类上取过来的:

packed_modules_mapping = qwen3_5.Qwen3_5ForCausalLM.packed_modules_mapping
supported_lora_modules = qwen3_5.Qwen3_5ForCausalLM.supported_lora_modules

这种写法的好处很实在:纯文本和多模态两条路走的是同一份主干实现,主干改一次两边都跟着变,不会出现「多模态修了个 bug、纯文本忘了同步」的情况。

一处有意思的注释:纯文本时 mrope 退化了

这个壳文件里有段注释值得单独拎出来:

Text-only checkpoints retain mrope_section, but identical position rows make its rotary embedding equivalent to 1-D RoPE.

意思是:纯文本的 checkpoint 仍然保留着 mrope_section 这个字段,但因为各个位置行的内容相同,它的旋转位置编码实际上等价于一维 RoPE。

MRoPE 是为多模态设计的——图像和视频有空间和时间维度,需要多个维度的位置信息。纯文本没有这些维度,几个位置行退化成一样的值,多维就变回了一维。

字段还在,作用没了。 这类「配置项残留」在多模态模型剥成纯文本版时很常见。它也和 llama.cpp 那边对 mrope_section 的兼容处理形成呼应(见 llama.cpp 转 GGUF 时改了什么)——同一个字段,三个框架各有各的处理方式。

与 vLLM 的一处明显分歧

现在说那个更有意思的对比。

qwen3_5.py 里的类定义有这么两个:

class Qwen3_5LinearDecoderLayer(nn.Module):     # 第 792 行
class Qwen3_5AttentionDecoderLayer(nn.Module):  # 第 935 行

两个独立的类,一个管线性注意力层,一个管全注意力层。

而 vLLM 那边是一个类Qwen3_5DecoderLayer 接受一个 layer_type 字符串参数,在构造函数里 if / elif 二选一,把结果挂到不同的属性名上(见 64 层在 vLLM 里怎么分流)。

同一个架构,两种组织方式:

SGLang 的做法是类型分离——两种层是两个类,各自的 __init__forward 完全独立,读某一个类时不会被另一种情况的分支干扰。

vLLM 的做法是参数分派——一个类涵盖两种情况,共用的部分(层归一化、MLP、层缩放)只写一遍,差异部分放在分支里。

两种做法各有各的道理,谈不上谁对谁错。但它提醒了一件事:读框架源码时,别用一家的结构去套另一家。 你在 vLLM 里找 Qwen3_5DecoderLayer 是找得到的,在 SGLang 里按同样的名字找就会扑空——不是不支持,是拆法不同。

还有一处:GatedDeltaNet 是自己实现的

qwen3_5.py 第 266 行开始是 Qwen3_5GatedDeltaNet 这个类,一直写到 792 行,五百多行

vLLM 那边则是从 vllm.model_executor.layers.mamba.gdn.qwen_gdn_linear_attn 导入一个现成的 QwenGatedDeltaNetAttention,模型文件里不写实现。

又是一处组织差异:SGLang 把这个注意力实现放在模型文件里,vLLM 把它抽到了共享的层目录下。

不过两边有个共识:这东西属于 Gated DeltaNet。名字在两个框架里都出现了,加上 llama.cpp 那边的 A_logdt_biasconv1d 参数名,三个独立的实现都指向同一个事实——这 48 层线性注意力走的是状态空间那套机制,不是某一家的理解偏差。

第三个文件:MTP 也是独立入口

qwen3_5_mtp.py 有 436 行,只声明一个入口类 Qwen3_5ForCausalLMMTP

这个安排和 vLLM 是一致的——两家都把 MTP 做成了独立注册的模型,而不是主模型的一个可选组件。这不是巧合,而是由 MTP 本身的性质决定的:它有自己的词嵌入、自己的解码层,输入之一是主模型的输出(结构拆解见 MTP 在推理时干什么)。这样一个东西,本来就更像是「另一个模型」而不是「一个模块」。

有意思的是体量对比:SGLang 的 MTP 实现有 436 行,vLLM 那边是 323 行。两家做的是同一件事,行数差了三分之一——这类差异通常来自各自框架的抽象层次和样板代码量,不代表功能多少。读源码时不要用行数去推断完整度,这是个很容易犯的错。

这种拆分方式对使用者的实际影响

入口式拆分看起来是内部实现细节,但有两处会影响到实际使用。

第一,你没法通过「用哪个文件」来选择模式。 走多模态还是纯文本,取决于 config.jsonarchitectures 写的是什么,框架据此匹配对应的 EntryClass。你不能指着 qwen3_5_text.py 说「我要用这个」——这个选择权在模型配置那边,不在你这边。

想要纯文本行为,正确的做法是用一份 architectures 写着 Qwen3_5ForCausalLM 的配置,也就是社区剥好的纯文本 checkpoint,而不是试图在启动参数里指定文件。

第二,报错信息里的类名可能和你预期的不一样。 如果加载出问题,异常栈里出现的可能是 Qwen3_5ForConditionalGeneration,而你以为自己在用纯文本模式。这时候第一件事是回去确认 config.jsonarchitectures 的实际值——配置说了算,你的预期不算。

小结

  • SGLang 的三个文件按模型入口拆分:多模态、纯文本、MTP 各一个
  • 文件末尾的 EntryClass 列表声明各自提供哪些入口
  • qwen3_5_text.py 只有 225 行是因为它是包装壳,主体通过 body_cls 复用主文件
  • 纯文本 checkpoint 里 mrope_section 字段仍在但退化为一维 RoPE
  • SGLang 把线性层和全注意力层拆成两个类,vLLM 用一个类加参数分支
  • SGLang 在模型文件里自己实现 GatedDeltaNet,vLLM 从共享层目录导入

给读源码的人一条实用建议:先找 EntryClass(SGLang)或 registry 注册项(vLLM),确认入口类;再顺着入口往下读。直接按文件名猜内容,很容易在这种入口式拆分面前判断错——比如看到一个只有两百多行的 qwen3_5_text.py,就以为纯文本支持是残缺的,实际上它背后是完整的两千多行主干。

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

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