Prhub

#32401 [Model] Support standalone text-only Qwen3.5 checkpoints

原始 PR 作者 JustinTong0323 合并时间 2026-07-28 14:08 文件变更 4 提交数 2 评论 2 代码增减 +222 / -1

执行摘要

支持独立文本 Qwen3.5 模型部署

独立文本 Qwen3.5 检查点使用 Qwen3_5ForCausalLM 或 Qwen3_5MoeForCausalLM 架构,但现有 qwen3_5.py 中的类是供多模态包装器使用的 transformer body,返回 hidden states,缺少顶层 LM head 和 logits processor。此 PR 完成 #27899 中讨论的运行时路径,支持直接部署文本检查点。

值得精读,特别是如何通过包装现有 body 快速适配新 checkpoint 格式;建议在类似模型支持中采用此模式。

讨论亮点

该 PR 没有收到 reviewer 评论或实质性讨论。仓库 CI 状态显示 PR Test 通过,PR Test Extra 失败,但未提供详细分析。Gemini Code Assist 机器人发表了一条关于服务终止的通知,与代码内容无关。

实现拆解

  1. 新增顶层模型类:在 qwen3_5_text.py 中定义 Qwen3_5ForCausalLM(和 Qwen3_5MoeForCausalLM)类,包装已有的 qwen3_5.Qwen3_5ForCausalLM body,添加 lm_head(Pipeline Parallel 感知)和 LogitsProcessor
  2. 注册新 Config 类型:在 configs/__init__.py 中导出 Qwen3_5TextConfigQwen3_5MoeTextConfig,并在 model_config.py 中将新架构添加到 draft 模型路由列表,使得它们能正确映射到 Qwen3_5ForCausalLMMTP
  3. 注册 Config Registry:在 utils/hf_transformers/common.py 中将 Qwen3_5TextConfigQwen3_5MoeTextConfig 加入 _CONFIG_REGISTRY,确保 AutoConfig 能加载对应的 model_type
  4. 权重加载适配:模型加载时从 model.* 键加载 body 权重,可选的未绑定 lm_head 单独加载,同时保持已有 MTP 加载器不变。
文件 模块 状态 重要度
python/sglang/srt/models/qwen3_5_text.py 模型层 added 9.28
python/sglang/srt/configs/__init__.py 配置 modified 5.31
python/sglang/srt/configs/model_config.py 配置 modified 4.53
python/sglang/srt/utils/hf_transformers/common.py 工具模块 modified 4.5

关键符号

Qwen3_5ForCausalLM.__init__ Qwen3_5ForCausalLM.forward Qwen3_5ForCausalLM.start_layer Qwen3_5ForCausalLM.end_layer Qwen3_5ForCausalLM.get_embed_and_head Qwen3_5ForCausalLM.set_embed_and_head

关键源码片段

python/sglang/srt/models/qwen3_5_text.py core-logic

核心变更文件,包含新顶层模型类 `Qwen3_5ForCausalLM` 和 `Qwen3_5MoeForCausalLM`,复用 body 并添加 lm_head 和 logits processor。

# qwen3_5_text.py —— 顶层文本入口
# 复用 qwen3_5.Qwen3_5ForCausalLM body,添加 LM head 和 logits processorclass Qwen3_5ForCausalLM(nn.Module):
    body_cls = qwen3_5.Qwen3_5ForCausalLM # 复用已有多模态 body
​
    def __init__(self, config, quant_config=None, prefix=""):
        super().__init__()
        self.config = config
        self.quant_config = quant_config
        self.pp_group = get_pp_group()
​
        # 代理 quant config 的 packed_modules_mapping
        if quant_config is not None and hasattr(quant_config, "packed_modules_mapping"):
            quant_config.packed_modules_mapping = self.packed_modules_mapping
​
        # 实例化 body
        self.model = self.body_cls(
            config=config,
            quant_config=quant_config,
            prefix=add_prefix("model", prefix),
        )
​
        # 在 last rank 创建 lm_head
        if self.pp_group.is_last_rank:
            if self.pp_group.world_size == 1 and config.tie_word_embeddings:
                self.lm_head = self.model.embed_tokens
            else:
                self.lm_head = ParallelLMHead(
                    config.vocab_size,
                    config.hidden_size,
                    quant_config=quant_config,
                    org_num_embeddings=config.vocab_size,
                    prefix=add_prefix("lm_head", prefix),
                    use_attn_tp_group=get_server_args().enable_dp_lm_head,
                )
        else:
            self.lm_head = PPMissingLayer()
​
        self.logits_processor = LogitsProcessor(config)
​
        # 文本 checkpoint 保留 mrope_section,但与 1D RoPE 等价
        rope_config = getattr(config, "rope_parameters", None) or getattr(
            config, "rope_scaling", None
        )
        self.is_mrope_enabled = bool(rope_config) and "mrope_section" in rope_config
        self.capture_aux_hidden_states = False
​
    @property
    def start_layer(self):
        return self.model.start_layer
​
    @property
    def end_layer(self):
        return self.model.end_layer
​
    def get_input_embeddings(self):
        return self.model.embed_tokens
​
    def get_embed_and_head(self):
        return self.model.embed_tokens.weight, self.lm_head.weight
​
    def set_embed_and_head(self, embed, head):
        del self.model.embed_tokens.weight
        del self.lm_head.weight
        self.model.embed_tokens.weight = embed
        self.lm_head.weight = head
        torch.cuda.empty_cache()
        torch.cuda.synchronize()
​
    @torch.no_grad()
    def forward(
        self,
        input_ids,
        positions,
        forward_batch,
        input_embeds=None,
        pp_proxy_tensors=None,
        **kwargs,
    ):
        if self.is_mrope_enabled:
            positions = forward_batch.mrope_positions
​
        hidden_states = self.model(
            input_ids,
            positions,
            forward_batch,
            input_embeds,
            pp_proxy_tensors=pp_proxy_tensors,
        )
        # LogitsProcessor 生成最终 logits
        # 返回 logits 或 PPProxyTensors(依赖 pp_group 位置)

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

主要风险包括:

  • 权重加载一致性:新的 Qwen3_5ForCausalLM 类从 model.* 前缀加载 body 权重,但若 Hub 上 checkpoint 结构不符预期可能导致加载失败。
  • MTP 路由漏配:在 model_config.py 中新增的架构条件若字母排序有误或遗漏变体,可能导致 draft 模型无法正确初始化。
  • mrope 等效性:文本 checkpoint 保留 mrope_section,但注释说明其等价于 1D RoPE,若实际效果有偏差可能导致位置编码错误。
  • 没有单元测试:PR 未添加单元测试,潜在回归只能通过集成测试覆盖。

对用户:Qwen3.5 文本模型用户可以无需多模态包装器直接部署,减少配置复杂度。对系统:新增约 208 行核心代码,但仅在加载 Qwen3.5 文本模型时生效,无通用性能影响。对团队:模式可复用为其他多模态模型提供文本入口。

权重加载路径 MTP 路由变更 缺少测试覆盖

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论