Qwen3.8-27B 的 64 层在 vLLM 里怎么分流:一个字符串决定走哪条路
如果你读过 Qwen3.8-27B 的 64 层怎么排,会知道它的 config.json 里有一个长度为 64 的 layer_types 数组,里面装的是 linear_attention 和 full_attention 两种字符串,比例是 48 比 16。
但配置文件只是一张清单。字符串本身不干活——真正决定第 37 层长什么样的,是推理框架读到这个字符串之后做了什么。想知道这一步,光看模型仓库是看不出来的,得去看框架的实现代码。
这篇就读 vLLM 的那份实现。
先说清楚这篇的边界
本文的全部依据是 vLLM 主干仓库里的 vllm/model_executor/models/qwen3_5.py(741 行)及其 registry.py,采集时间是 2026-08-24。
我们没有下载权重、没有安装 vLLM、没有启动过服务、没有推理过一个 token。 下面所有关于「怎么构造」的说法,都是从源码文本里读出来的结构,不是运行观察。凡是涉及速度、显存、吞吐的话题,本文一概不碰——那些需要真跑,我们没跑。
第一步:vLLM 怎么知道该用哪份代码
Qwen3.8-27B 的 config.json 顶层写着:
"architectures": ["Qwen3_5ForConditionalGeneration"]
vLLM 拿这个字符串去 registry.py 里查表。相关的注册行有三组:
| registry.py 行号 | 注册项 |
|---|---|
| 599 | "Qwen3_5ForConditionalGeneration": ("qwen3_5", "Qwen3_5ForConditionalGeneration") |
| 208-209 | Qwen3_5ForCausalLM / Qwen3_5MoeForCausalLM → 同样指向 qwen3_5 模块 |
| 691-692 | Qwen3_5MTP / Qwen3_5MoeMTP → 指向另一个模块 qwen3_5_mtp |
第一行就是 27B 走的那条。值得留意的是第三组:MTP 被注册成了独立的模型,不在 qwen3_5 这份文件里。这一点后面会再碰到。
第二步:一个继承链,两份血统
打开 qwen3_5.py,最先该看的不是函数体,是几个 class 的括号里写了谁:
class Qwen3_5DecoderLayer(Qwen3NextDecoderLayer):
class Qwen3_5Model(Qwen3NextModel):
class Qwen3_5ForConditionalGeneration(Qwen3VLForConditionalGeneration, IsHybrid):
这三行透露的信息量很大。解码层和主干模型继承自 Qwen3-Next,而顶层的条件生成类继承自 Qwen3-VL。
换句话说,在 vLLM 的实现视角里,Qwen3.5 系列同时承接了两条产品线:混合注意力的骨架来自 Qwen3-Next,多模态的外壳来自 Qwen3-VL。文件开头的导入语句也印证了这一点,它从 .qwen3_next 拿了 Qwen3NextAttention、Qwen3NextDecoderLayer、Qwen3NextModel、Qwen3NextSparseMoeBlock,又从 .qwen3_vl 拿了 Qwen3_VisionTransformer、Qwen3VLMultiModalProcessor 等一整套多模态处理件。
这给 config.json 里全是 qwen3_5 的代际关系 那个疑问补上了一块实现侧的旁证:代际不只是命名上的延续,代码是真的直接继承过来的。
第三步:字符串在哪里被取出来
现在到正题。Qwen3_5Model.__init__ 里有这么一段(qwen3_5.py:250-259):
def get_layer(prefix: str):
return Qwen3_5DecoderLayer(
vllm_config,
layer_type=config.layer_types[extract_layer_index(prefix)],
prefix=prefix,
)
self.start_layer, self.end_layer, self.layers = make_layers(
config.num_hidden_layers, get_layer, prefix=f"{prefix}.layers"
)
逻辑非常直白:make_layers 按 num_hidden_layers(对 27B 是 64)逐个造层,每造一个就调用 get_layer。get_layer 从 prefix 里抠出层下标,拿这个下标去 config.layer_types 里索引,把取到的字符串作为 layer_type 参数传给解码层的构造函数。
所以 layer_types 数组的下标顺序是有意义的,它不是一个「统计各类多少个」的汇总字段,而是一张逐层对照表。第 3 项写什么,第 3 层就按什么造。
第四步:二选一的分支
字符串传进 Qwen3_5DecoderLayer.__init__ 之后,命运在这里分叉(qwen3_5.py:144-162):
if self.layer_type == "linear_attention":
self.linear_attn = QwenGatedDeltaNetAttention(
config=config,
vllm_config=vllm_config,
prefix=f"{prefix}.linear_attn",
gqa_interleaved_layout=False,
reduce_results=not self.use_attn_reduce_scatter_for_moe,
)
elif self.layer_type == "full_attention":
self.self_attn = Qwen3NextAttention(
config,
model_config=model_config,
...
)
else:
raise ValueError(f"Invalid layer_type {self.layer_type}")
三点值得说:
其一,属性名都不一样。 线性层挂在 self.linear_attn 上,全注意力层挂在 self.self_attn 上。这意味着这两种层在对象结构上就是两种东西,不是同一个类换参数。权重名称也因此不同——这一点在加载 checkpoint 时会体现出来。
其二,只认这两个字符串。 写错一个字母就走到 else 分支直接抛 ValueError。没有默认值,没有容错。
其三,也是最有意思的一点:线性那一支构造的类叫 QwenGatedDeltaNetAttention。
第五步:线性注意力为什么住在 Mamba 目录里
翻到文件头部的导入(qwen3_5.py:43-45):
from vllm.model_executor.layers.mamba.gdn.qwen_gdn_linear_attn import (
QwenGatedDeltaNetAttention,
)
路径是 layers/mamba/gdn/。Qwen3.8-27B 那 48 层线性注意力,在 vLLM 的代码组织里被归到了 Mamba 底下。
这不是随手放的。往下看会发现,这一支复用了整套 Mamba 的状态管理设施(qwen3_5.py:46-51 导入,:388-424 使用):
from vllm.model_executor.layers.mamba.mamba_utils import (
MambaStateCopyFunc,
MambaStateCopyFuncCalculator,
MambaStateDtypeCalculator,
MambaStateShapeCalculator,
)
三个类方法分别负责算状态的数据类型、算状态的形状、以及提供状态拷贝函数,方法名里都带着 gated_delta_net:
return MambaStateShapeCalculator.gated_delta_net_state_shape(
tp_size,
hf_config.linear_num_key_heads,
hf_config.linear_num_value_heads,
hf_config.linear_key_head_dim,
hf_config.linear_value_head_dim,
hf_config.linear_conv_kernel_dim,
num_spec,
)
括号里那一串正好就是 config.json 里那几个 linear_ 开头的字段:linear_num_key_heads、linear_num_value_heads、linear_key_head_dim、linear_value_head_dim、linear_conv_kernel_dim。它们在配置文件里只是几个数字,到了这里才显出用途——它们是用来算「循环状态」的尺寸的。
这就解释了一个只看 config.json 想不通的问题:为什么这几个字段跟全注意力那套 num_attention_heads / num_key_value_heads / head_dim 完全平行、互不相干?因为它们服务的根本不是同一套机制。全注意力那套算的是 KV cache,线性这套算的是状态空间模型的循环状态。
两者有一个结构性差别:KV cache 随序列变长而变长,而这里的状态形状由 config 字段固定,跟序列长度无关。 这是从函数签名就能读出来的——gated_delta_net_state_shape 的入参里压根没有序列长度这一项。
第六步:MLP 那一支反而是统一的
注意力分了两种,那前馈网络呢?源码里紧接着还有一个分支(qwen3_5.py:166-180),但判断依据换成了 config.model_type:
if config.model_type == "qwen3_5_moe_text":
self.mlp = Qwen3NextSparseMoeBlock(...)
elif config.model_type == "qwen3_5_text":
self.mlp = Qwen3NextMLP(...)
else:
raise ValueError(f"Invalid model_type {config.model_type}")
上面还有一行注释写得很清楚:
Qwen3.5 use all layers for MLP / Qwen3.5-MoE use sparse MoE blocks
Qwen3.8-27B 的 text_config.model_type 是 qwen3_5_text,所以走第二支——64 层全部用稠密 MLP,没有 MoE。稀疏专家那条路是留给同系列 MoE 型号的。
所以完整的图景是:注意力按层分流,MLP 按型号分流。同一个 Qwen3_5DecoderLayer 类,通过两个不同维度的判断,拼出四种可能的组合。
第七步:两个容易被忽略的实现细节
读完主线,还有两处值得单独拎出来。
MTP 的权重在这里被直接丢掉了。 文件里出现了两次这样的映射(qwen3_5.py:322 和 :472):
WeightsMapper(orig_to_new_prefix={"mtp.": None})
映射到 None 就是丢弃。也就是说,主模型加载 checkpoint 时,凡是 mtp. 前缀的权重一律不要。这跟前面 registry 里 Qwen3_5MTP 被单独注册成另一个模型是一回事——MTP 那一层 想用就得走 qwen3_5_mtp.py 那条独立路径,它不是主模型的一部分。
上游权重是分开存的,vLLM 在加载时才融合。 另一处映射(qwen3_5.py:220-227):
hf_to_vllm_mapper = Qwen3NextModel.hf_to_vllm_mapper | WeightsMapper(
orig_to_new_stacked={
".in_proj_qkv": (".in_proj_qkvz", (0, 1, 2)),
".in_proj_z": (".in_proj_qkvz", 3),
".in_proj_b": (".in_proj_ba", 0),
".in_proj_a": (".in_proj_ba", 1),
}
)
代码上方的注释解释了原因:Qwen3.5 把 Gated DeltaNet 的 in_proj 权重分开发布,而 qwen3-next 是预先融合好的,所以要在 qwen3-next 那套映射之上再叠一层,把分开的四份拼成两份。
这是个纯工程性的差异,但它说明了一件事:同一个架构家族,不同代次的 checkpoint 存储格式可以不一样,框架得靠这类映射表来抹平。
一个会直接拦住你的限制
最后说一条实操性的。构造函数开头有这么一段(qwen3_5.py:332-336):
if cache_config.mamba_cache_mode == "all":
raise NotImplementedError(
"Qwen3.5 currently does not support 'all' prefix caching, "
"please use '--mamba-cache-mode=align' instead"
)
提示语是源码原文。这是个硬性拦截——不是警告,是直接抛异常,模型根本构造不起来。原因也不难理解:混合架构里那 48 层的状态不是标准 KV cache,前缀缓存的复用策略跟纯注意力模型不是一回事。
这条限制带着 currently 这个词,说明是当前版本的状态。你读到这篇文章时 vLLM 可能已经改了,以你安装的那个版本的源码为准。
小结
把整条链路串起来:
config.json的architectures字段 → vLLM 的registry.py查表 → 定位到qwen3_5模块Qwen3_5Model按num_hidden_layers循环造层,每层用自己的下标去layer_types取字符串Qwen3_5DecoderLayer按这个字符串二选一:linear_attention造QwenGatedDeltaNetAttention,full_attention造Qwen3NextAttention,其余报错- 线性那一支复用 Mamba 的状态设施,状态尺寸由
linear_*字段决定,与序列长度无关 - MLP 不看层类型,只看
model_type;27B 是qwen3_5_text,64 层全用稠密 MLP mtp.前缀的权重在主模型这里被丢弃,MTP 是另一个注册模型
最值得记住的一点:layer_types 是一张逐层对照表,下标即层号。它在配置文件里只是 64 个字符串,但在框架里,每一个字符串都对应一次实打实的类型分叉。
想继续往下挖,可以看看 混合注意力由 config 里哪几个字段承载,或者回到 Qwen3.8-27B 专题 看这个模型的其他拆解。