Prhub

#35006 [Diffusion] Reuse SRT Qwen vision and text modules

原始 PR 作者 mickqian 合并时间 2026-08-19 10:12 文件变更 26 提交数 18 评论 3 代码增减 +1348 / -713

执行摘要

Qwen 视觉与文本模块复用 SRT 原语,去重千行

PR body 明确指出:The multimodal generation Qwen2.5-VL, Qwen3, and Qwen3-VL encoders duplicated pure modules already implemented in SRT. Maintaining both copies also left the MMGen paths without reusable SRT tensor-parallel projections, embeddings, activation kernels, and checkpoint loaders. 核心问题是 MMGen 与 SRT 各维护一份 Qwen 系列模块,既增加双份维护成本,又让 diffusion 路径无法使用 SRT 成熟的 TP 线性层、嵌入、激活 kernel 与 checkpoint loader。本 PR 的目标是在不破坏 diffusion 特有 attention/量化契约的前提下,把通用原语下沉到 SRT。

值得精读。该 PR 展示了跨 runtime 复用的边界设计:通用原语下沉到 SRT,而 attention 与量化这类与 runtime 契约强耦合的模块留在 MMGen;同时以数值保真测试和两卡折叠校验保障重构安全。建议重点关注三处:Qwen2_5_VLMLP 的 TP 三段式选型、RMSNorm 高阶 residual 的 flatten/restore 逻辑、以及 TP group 绑定/恢复的生命周期管理。

讨论亮点

本 PR 没有任何 reviewer 评论(review_comments_count = 0),核心权衡集中在 PR body 与 commit 序列中:

  • 作者明确强调 Keep the Qwen2.5 split gate/up execution order as act(gate) * up; this avoids a concatenation and preserves HF numerical behavior.,即在复用 SRT MLP 时坚持保留 split 执行顺序以保证数值一致。
  • TP 选型上采用 Use replicated SRT linears at TP=1 and explicit TP rank/size at TP>1, while retaining the existing replicated fallback when a dimension is not TP-divisible. 的三段式策略,对应 commit Avoid TP wrappers in single-rank Qwen MLPsUnify single-rank Qwen MLP projections 的反复打磨。
  • 对 attention 边界,作者说明共享的 packed-sequence 适配器 remains MMGen-owned because diffusion backend selection and segmented SDPA fallback differ from SRT serving's attention contract.,明确区分了“可复用原语”与“runtime 契约绑定模块”。

实现拆解

  1. 建立共享视觉注意力中枢:新增 qwen_vl_vision.py,定义 PackedSequenceMetadata 打包 cu_seqlenscu_seqlens_hostmax_seqlen,新增 QwenVLVisionAttention 把原来的 nn.Linear 换成 SRT 的 QKVParallelLinear / RowParallelLinear,并按 get_parallel().tp_size 切分头数;backend 支持 packed varlen 时走自定义 kernel,否则逐段回退 torch SDPA
  2. 视觉塔与文本塔去重qwen2_5vl_vision.pyqwen3vl_vision.py 删除本地实现的 Attention、MLP、RMSNorm、PatchEmbed,改为从 SRT 的 qwen2_5_vl.py / qwen3_vl.py 导入;qwen2_5vl.py 删除自实现 Qwen2_5_VLTextMLP,改用 fuse_gate_up=False 的 SRT Qwen2_5_VLMLP 保持 act(gate) * up 顺序,qwen3vl.pyq_norm / k_normnorm 换成 SRT RMSNorm;保留 LocalAttention、量化线性层与 model.visual.blocks 层归属。
  3. 扩展 SRT 公共组件srt/models/qwen2_5_vl.py 新增本地 Qwen2_5_VisionPatchEmbedConv3dLayer + disable_linear)与 Qwen2_5_VisionRotaryEmbeddingQwen2_5_VLMLP 增加 fuse_gate_uptp_sizetp_rank 参数且默认值保持旧行为;srt/models/qwen3_vl.py 新增 _resolve_vision_tpsrt/layers/layernorm.py 支持高阶 residual 输入;srt/layers/activation.pySiluAndMul 可在无 server context 时工作。
  4. TP 组绑定与 checkpoint 名称映射:encoder root 通过 EncoderTensorParallelMixin 绑定到构造/加载/forward 所用的 TP group,嵌套 context 同时切换 MMGen TP、SRT TP 与 SRT attention TP 并在结束后独立恢复;qwen3vl.pyload_weightsvisual. 前缀做 .attn.qkv..attn.qkv_proj. 映射,兼容 MiniMax-H3 checkpoint。
  5. 测试与验证配套:新增 test_qwen_vl_rope.pytest_encoder_fold_srt_linear_2_gpu.pytest_component_accuracy_weight_transfer.py,扩展 test_layernorm_fusion.pytest_qwen3vl_text.pytest_qwen2_5vl_generation.py 等,覆盖 Transformers v5 配置、GQA 布局、TP=1/TP=2、fold/replicate 数值与 E2E 图像质量指标。
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/models/encoders/qwen_vl_vision.py 视觉注意力 added 9.13
python/sglang/multimodal_gen/runtime/models/encoders/qwen2_5vl_vision.py 视觉编码器 modified 8.86
python/sglang/multimodal_gen/runtime/models/encoders/qwen3vl_vision.py 视觉编码器 modified 8.86
python/sglang/multimodal_gen/runtime/models/encoders/qwen2_5vl.py 文本编码器 modified 8.79
python/sglang/srt/models/qwen2_5_vl.py 模型实现 modified 8.76
python/sglang/multimodal_gen/runtime/models/encoders/qwen_vl_rope.py 旋转编码 added 7.93
python/sglang/srt/models/qwen3_vl.py 模型实现 modified 7.2
python/sglang/srt/layers/layernorm.py 归一化层 modified 6.72
python/sglang/multimodal_gen/test/unit/test_qwen_vl_rope.py 单元测试 added 7.28
python/sglang/multimodal_gen/test/single_test_file/test_encoder_fold_srt_linear_2_gpu.py 集成测试 added 7.19

关键符号

PackedSequenceMetadata.from_cu_seqlens QwenVLVisionAttention.__init__ QwenVLVisionAttention.forward QwenVLVisionAttention._packed_attention build_qwen_vl_text_rope apply_qwen_vl_text_rope Qwen2_5_VLMLP.__init__ Qwen2_5_VLMLP.forward Qwen2_5_VisionPatchEmbed.forward _resolve_vision_tp _make_text_rms_norm Qwen2_5VLVisionBlock.forward Qwen3VLVisionBlock.forward

关键源码片段

python/sglang/multimodal_gen/runtime/models/encoders/qwen2_5vl_vision.py data-contract

从 285 行本地实现缩减到 75 行左右,删除 Qwen2_5VLVisionAttention、Qwen2_5VLVisionMLP、Qwen2_5VLVisionRMSNorm 等重复代码,改为复用 SRT 组件,是本次去重收益最大的文件之一。

class Qwen2_5VLVisionBlock(nn.Module):
    def __init__(self, config: Any, layer_idx: int) -> None:
        super().__init__()
        # 直接复用 SRT 的 RMSNorm 与共享注意力,视觉塔由此获得 SRT 的 TP 线性层与 checkpoint loader
        self.norm1 = RMSNorm(
            config.hidden_size, eps=1e-6,
            cast_x_before_out_mul=True, force_native=True,
        )
        self.norm2 = RMSNorm(
            config.hidden_size, eps=1e-6,
            cast_x_before_out_mul=True, force_native=True,
        )
        self.attn = QwenVLVisionAttention(
            config, prefix=f"visual.blocks.{layer_idx}.attn", model_name="Qwen2.5-VL",
        )
        self.mlp = Qwen2_5_VLMLP(
            config.hidden_size, config.intermediate_size, bias=True,
            hidden_act=config.hidden_act,
            prefix=f"visual.blocks.{layer_idx}.mlp", fuse_gate_up=False,
        )
​
    def forward(
        self, hidden_states: torch.Tensor, *, metadata: PackedSequenceMetadata,
        position_embeddings: tuple[torch.Tensor, torch.Tensor],
    ) -> torch.Tensor:
        hidden_states = hidden_states + self.attn(
            self.norm1(hidden_states), metadata=metadata,
            position_embeddings=position_embeddings,
        )
        return hidden_states + self.mlp(self.norm2(hidden_states))
​
​
class Qwen2_5VLVisionTransformer(nn.Module):
    # forward 中构造 full 与 window 两套 packed 元数据并逐层选择
    def forward(self, hidden_states: torch.Tensor, grid_thw: torch.Tensor) -> torch.Tensor:
        # ... 前置 patch embed、窗口重排与位置编码构造省略,核心是两套 metadata 的生成
        cu_seqlens = torch.repeat_interleave(
            grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]
        ).cumsum(dim=0, dtype=torch.int32)
        cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0)
        full_metadata = PackedSequenceMetadata.from_cu_seqlens(cu_seqlens)
        window_metadata = PackedSequenceMetadata.from_cu_seqlens(cu_window_seqlens)
​
        for layer_idx, block in enumerate(self.blocks):
            # 整图注意力层用 full 序列,其余层用窗口序列,契约由 MMGen 持有
            metadata = (
                full_metadata if layer_idx in self.full_attention_layers
                else window_metadata
            )
            hidden_states = block(
                hidden_states, metadata=metadata,
                position_embeddings=position_embeddings,
            )
        merged = self.merger(hidden_states)
        return merged[torch.argsort(window_index)]
python/sglang/multimodal_gen/runtime/models/encoders/qwen_vl_rope.py data-contract

新增的 MRoPE 适配器,把 SRT get_rope 封装成 MMGen 可独立构造的 Qwen-VL 文本 RoPE,并强制走 forward_native 以保持 HF 的 bf16 算术顺序。

def build_qwen_vl_text_rope(
    config: Any, *, mrope_interleaved: bool = False
) -> RotaryEmbedding:
    # 统一走 SRT 的 get_rope,但允许在无 SRT runtime context 的 MMGen 环境独立构造
    head_dim = getattr(config, "head_dim", None) or (
        config.hidden_size // config.num_attention_heads
    )
    rope_theta, rope_scaling = get_rope_config(config)
    rope_scaling = dict(rope_scaling or {})
    rope_scaling["mrope_interleaved"] = mrope_interleaved
    return get_rope(
        head_size=head_dim,
        rotary_dim=head_dim,
        max_position=config.max_position_embeddings,
        base=rope_theta,
        is_neox_style=True,
        rope_scaling=rope_scaling,
    )
​
​
def apply_qwen_vl_text_rope(
    rotary_emb: RotaryEmbedding, position_ids: torch.Tensor,
    query: torch.Tensor, key: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
    """对 4D 的 [batch, heads, sequence, head_dim] 张量应用三轴 MRoPE。"""
    # ... 形状校验省略:query/key 必须为 4D,position_ids 必须为 [3, batch, sequence]
    query = query.transpose(1, 2).reshape(-1, num_query_heads * head_dim)
    key = key.transpose(1, 2).reshape(-1, num_key_value_heads * head_dim)
    # 保持 HF 的 bf16 算术顺序,融合 MRoPE 会改变生成图像
    query, key = rotary_emb.forward_native(position_ids.reshape(3, -1), query, key)
    query = query.view(batch_size, sequence_length, num_query_heads, head_dim)
    key = key.view(batch_size, sequence_length, num_key_value_heads, head_dim)
    return query.transpose(1, 2), key.transpose(1, 2)

评论区精华

SRT 复用边界:attention 与量化线性层为何保留 MMGen 实现 设计

PR 无 reviewer 评论(review_comments_count = 0),此条来自 PR body 的设计说明。作者明确写到 'Share one MMGen packed-sequence attention adapter... It remains MMGen-owned because diffusion backend selection and segmented SDPA fallback differ from SRT serving's attention contract';Qwen3-VL quantized linears 也因 diffusion quantization 契约不同而保留在 MMGen。

结论:通用原语(PatchEmbed、MLP、RMSNorm、MRoPE、TP 线性层)统一到 SRT;attention 与量化这类依赖 runtime 契约的模块继续留在 MMGen,避免破坏 diffusion 的 backend 选择与量化语义。 · 已解决

TP=1 与 TP>1 的线性层选型:ReplicatedLinear vs 显式 TP + 非整除 fallback 设计

commit 'Avoid TP wrappers in single-rank Qwen MLPs' 与 'Unify single-rank Qwen MLP projections' 显示作者多次调整选型;body 说明 'Use replicated SRT linears at TP=1 and explicit TP rank/size at TP>1, while retaining the existing replicated fallback when a dimension is not TP-divisible'。

结论:最终采用 TP=1 时 ReplicatedLinear、TP>1 且可整除时显式 TP、不可整除时回退 replicated 的三段式策略,并新增 SRT Qwen2_5_VLMLP 的 fuse_gate_up 分支兼容两种路径。 · 已解决

MRoPE 数值保真:为什么用 forward_native 而不用融合实现 正确性

qwen_vl_rope.py 与 commit 'Preserve Qwen-VL MRoPE parity'、'Preserve Qwen3 QK norm parity' 体现的决策:SRT 融合 MRoPE 会改变生成图像,因此保留 HF 的 bf16 算术顺序,调用 forward_native。

结论:对 Qwen-VL 文本 attention 复用 SRT rotary embedding 但强制走 native 路径,避免数值漂移;单元测试验证最大误差 2.38e-7(FP32)与 1-2 ULP(BF16)。 · 已解决

风险与影响

  • 跨模块耦合加深:MMGen 与 SRT 从此共享核心层实现,未来任何对 RMSNormQwen2_5_VLMLP、SRT RoPE 的改动都会同时影响 serving 与 diffusion 两条路径,回归面扩大。
  • SRT 基础模块行为变化srt/layers/layernorm.py 的高阶 residual 支持改动影响所有使用 residual 的模型;Qwen2_5_VLMLP 在 TP=1 时将 down_projRowParallelLinear 换成 ReplicatedLinear,需关注量化装载与自定义 weight loader 的兼容性。
  • 数值保真依赖apply_qwen_vl_text_rope 刻意走 forward_native 以保持 HF 的 bf16 算术顺序,如果未来 SRT 融合 MRoPE 被默认启用,会再次出现图像漂移。
  • TP 覆盖不足:两卡折叠测试只覆盖 TP=2 单实例;更高 rank(TP=4/8)与多实例并发下的 group 恢复未验证。
  • 工程流程风险:0 条 review 评论、作者自行合并,26 个文件 1300+ 行改动的评审深度有限。
  • 用户侧:无公开 API 与 CLI 变化,默认行为保持兼容;但任何数值偏差都会直接影响生成图像质量,因此本 PR 用 SSIM、PSNR、CLIP、余弦相似度等多维指标做了严格回归对比。
  • 系统侧:MMGen 的 Qwen2.5-VL、Qwen3-VL、Qwen3、MiniMax-H3 路径全部切换到 SRT 原语,视觉塔与文本塔获得 SRT 的 TP 线性层、嵌入、激活 kernel 与 checkpoint loader;SRT 侧新增本地 Qwen2.5 视觉模块并扩展 MLP/Layernorm 能力。
  • 团队侧:消除两套重复实现,降低长期维护成本,但 MMGen 与 SRT 的耦合关系需要形成稳定的模块边界约定,避免后续改动互相踩踏。
  • 性能侧:Qwen-Image E2E 12697.67ms 对 12799.17ms、MiniMax-H3 2GPU 47120.89ms 对 47538.57ms,视觉塔保持 1.08x/1.19x 加速,均无回归。
跨模块重构:MMGen 与 SRT 耦合加深 核心基础模块变更:SRT RMSNorm 与 Qwen2_5_VLMLP 数值保真依赖 HF 算术顺序 TP 折叠路径覆盖不足(仅 2 GPU) 无 reviewer 评论,作者自合

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论