[diffusion] refactor: reuse srt qwen vision and text modules (#35006)

This commit is contained in:
Mick
2026-08-19 10:12:44 +08:00
committed by GitHub
parent 58c5bee3ac
commit 4cef72faee
26 changed files with 1347 additions and 713 deletions
@@ -182,10 +182,12 @@ class MiniMaxH3Qwen3VLEncoder(TextEncoder):
for name, loaded_weight in weights: for name, loaded_weight in weights:
if not self.should_materialize_checkpoint_weight(name): if not self.should_materialize_checkpoint_weight(name):
continue continue
param = params.get(name) param_name = name.replace(".attn.qkv.", ".attn.qkv_proj.")
param = params.get(param_name)
if param is None: if param is None:
raise KeyError( raise KeyError(
f"Unexpected MiniMax H3 Qwen3-VL checkpoint weight: {name}" "Unexpected MiniMax H3 Qwen3-VL checkpoint weight: "
f"{name} (mapped to {param_name})"
) )
weight_loader = getattr(param, "weight_loader", default_weight_loader) weight_loader = getattr(param, "weight_loader", default_weight_loader)
try: try:
@@ -196,7 +198,7 @@ class MiniMaxH3Qwen3VLEncoder(TextEncoder):
f"{name!r}: checkpoint={tuple(loaded_weight.shape)}, " f"{name!r}: checkpoint={tuple(loaded_weight.shape)}, "
f"parameter={tuple(param.shape)}" f"parameter={tuple(param.shape)}"
) from exc ) from exc
loaded.add(name) loaded.add(param_name)
return loaded return loaded
@@ -5,7 +5,6 @@ from transformers import (
DynamicCache, DynamicCache,
PretrainedConfig, PretrainedConfig,
Qwen2_5_VLTextConfig, Qwen2_5_VLTextConfig,
Qwen2RMSNorm,
) )
from transformers.masking_utils import ( from transformers.masking_utils import (
create_causal_mask, create_causal_mask,
@@ -17,23 +16,30 @@ from transformers.utils import TransformersKwargs, is_torchdynamo_compiling
from sglang.multimodal_gen.configs.models.encoders.qwen_image import Qwen2_5VLConfig from sglang.multimodal_gen.configs.models.encoders.qwen_image import Qwen2_5VLConfig
from sglang.multimodal_gen.runtime.distributed import ( from sglang.multimodal_gen.runtime.distributed import (
get_tp_rank,
get_tp_world_size, get_tp_world_size,
model_parallel_is_initialized, model_parallel_is_initialized,
) )
from sglang.multimodal_gen.runtime.layers.attention import LocalAttention from sglang.multimodal_gen.runtime.layers.attention import LocalAttention
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
MergedColumnParallelLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.quantization import QuantizationConfig from sglang.multimodal_gen.runtime.layers.quantization import QuantizationConfig
from sglang.multimodal_gen.runtime.loader.weight_utils import default_weight_loader from sglang.multimodal_gen.runtime.loader.weight_utils import default_weight_loader
from sglang.multimodal_gen.runtime.models.encoders.base import TextEncoder from sglang.multimodal_gen.runtime.models.encoders.base import TextEncoder
from sglang.multimodal_gen.runtime.models.encoders.qwen2_5vl_vision import ( from sglang.multimodal_gen.runtime.models.encoders.qwen2_5vl_vision import (
Qwen2_5VLVisionTransformer, Qwen2_5VLVisionTransformer,
) )
from sglang.multimodal_gen.runtime.models.encoders.qwen_vl_rope import (
apply_qwen_vl_text_rope,
build_qwen_vl_text_rope,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.common import add_prefix from sglang.srt.layers.layernorm import RMSNorm
from sglang.srt.layers.linear import (
ColumnParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from sglang.srt.layers.vocab_parallel_embedding import VocabParallelEmbedding
from sglang.srt.models.qwen2_5_vl import Qwen2_5_VLMLP
# coding=utf-8 # coding=utf-8
# Adapted from # Adapted from
@@ -70,12 +76,9 @@ except ImportError:
import torch import torch
import torch.nn as nn import torch.nn as nn
from transformers.activations import ACT2FN
from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import ( from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import (
Qwen2_5_VLCausalLMOutputWithPast, Qwen2_5_VLCausalLMOutputWithPast,
Qwen2_5_VLModelOutputWithPast, Qwen2_5_VLModelOutputWithPast,
Qwen2_5_VLRotaryEmbedding,
apply_multimodal_rotary_pos_emb,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -134,6 +137,12 @@ def _tp_world_size() -> int:
return get_tp_world_size() return get_tp_world_size()
def _tp_rank() -> int:
if not model_parallel_is_initialized():
return 0
return get_tp_rank()
def _linear_output(linear: nn.Module, x: torch.Tensor) -> torch.Tensor: def _linear_output(linear: nn.Module, x: torch.Tensor) -> torch.Tensor:
output = linear(x) output = linear(x)
return output[0] if isinstance(output, tuple) else output return output[0] if isinstance(output, tuple) else output
@@ -152,8 +161,10 @@ def _make_column_linear(
out_features, out_features,
bias=bias, bias=bias,
gather_output=False, gather_output=False,
tp_size=_tp_world_size(),
tp_rank=_tp_rank(),
) )
return nn.Linear(in_features, out_features, bias=bias) return ReplicatedLinear(in_features, out_features, bias=bias)
def _make_row_linear( def _make_row_linear(
@@ -168,8 +179,10 @@ def _make_row_linear(
in_features, in_features,
out_features, out_features,
bias=bias, bias=bias,
tp_size=_tp_world_size(),
tp_rank=_tp_rank(),
) )
return nn.Linear(in_features, out_features, bias=bias) return ReplicatedLinear(in_features, out_features, bias=bias)
class Qwen2_5_VLAttention(nn.Module): class Qwen2_5_VLAttention(nn.Module):
@@ -183,10 +196,9 @@ class Qwen2_5_VLAttention(nn.Module):
self.config = config self.config = config
self.layer_idx = layer_idx self.layer_idx = layer_idx
if layer_idx is None: if layer_idx is None:
logger.warn( logger.warning(
f"Instantiating {self.__class__.__name__} without passing `layer_idx` is not recommended and will " "Instantiating %s without layer_idx disables correct cache updates",
"to errors during the forward call, if caching is used. Please make sure to provide a `layer_idx` " self.__class__.__name__,
"when creating this class."
) )
self.hidden_size = config.hidden_size self.hidden_size = config.hidden_size
@@ -221,7 +233,6 @@ class Qwen2_5_VLAttention(nn.Module):
self.num_key_value_groups = self.num_heads // self.num_key_value_heads self.num_key_value_groups = self.num_heads // self.num_key_value_heads
self.is_causal = True self.is_causal = True
self.attention_dropout = config.attention_dropout self.attention_dropout = config.attention_dropout
self.rope_scaling = config.rope_scaling
self.scaling = self.head_dim**-0.5 self.scaling = self.head_dim**-0.5
self.q_proj = _make_column_linear( self.q_proj = _make_column_linear(
@@ -254,7 +265,7 @@ class Qwen2_5_VLAttention(nn.Module):
else None else None
) )
self.rotary_emb = Qwen2_5_VLRotaryEmbedding(config=config) self.rotary_emb = build_qwen_vl_text_rope(config)
self.attn = LocalAttention( self.attn = LocalAttention(
num_heads=self.num_heads, num_heads=self.num_heads,
head_size=self.head_dim, head_size=self.head_dim,
@@ -276,9 +287,6 @@ class Qwen2_5_VLAttention(nn.Module):
output_attentions: bool = False, output_attentions: bool = False,
use_cache: bool = False, use_cache: bool = False,
cache_position: Optional[torch.LongTensor] = None, cache_position: Optional[torch.LongTensor] = None,
position_embeddings: Optional[
tuple[torch.Tensor, torch.Tensor]
] = None, # necessary, but kept here for BC
**kwargs: Unpack[FlashAttentionKwargs], **kwargs: Unpack[FlashAttentionKwargs],
) -> tuple[torch.Tensor, Optional[torch.Tensor], Optional[tuple[torch.Tensor]]]: ) -> tuple[torch.Tensor, Optional[torch.Tensor], Optional[tuple[torch.Tensor]]]:
bsz, q_len, _ = hidden_states.size() bsz, q_len, _ = hidden_states.size()
@@ -291,17 +299,15 @@ class Qwen2_5_VLAttention(nn.Module):
key_states = key_states.view(bsz, q_len, -1, self.head_dim).transpose(1, 2) key_states = key_states.view(bsz, q_len, -1, self.head_dim).transpose(1, 2)
value_states = value_states.view(bsz, q_len, -1, self.head_dim).transpose(1, 2) value_states = value_states.view(bsz, q_len, -1, self.head_dim).transpose(1, 2)
cos, sin = position_embeddings query_states, key_states = apply_qwen_vl_text_rope(
query_states, key_states = apply_multimodal_rotary_pos_emb( self.rotary_emb,
query_states, key_states, cos, sin, self.rope_scaling["mrope_section"] position_ids,
query_states,
key_states,
) )
if past_key_values is not None: if past_key_values is not None:
cache_kwargs = { cache_kwargs = {"cache_position": cache_position}
"sin": sin,
"cos": cos,
"cache_position": cache_position,
} # Specific to RoPE models
key_states, value_states = past_key_values.update( key_states, value_states = past_key_values.update(
key_states, value_states, self.layer_idx, cache_kwargs key_states, value_states, self.layer_idx, cache_kwargs
) )
@@ -324,38 +330,6 @@ class Qwen2_5_VLAttention(nn.Module):
return attn_output return attn_output
class Qwen2_5_VLTextMLP(nn.Module):
def __init__(self, config: Qwen2_5_VLTextConfig):
super().__init__()
tp_size = _tp_world_size()
use_tensor_parallel = tp_size > 1 and config.intermediate_size % tp_size == 0
self.gate_proj = _make_column_linear(
config.hidden_size,
config.intermediate_size,
bias=False,
use_tensor_parallel=use_tensor_parallel,
)
self.up_proj = _make_column_linear(
config.hidden_size,
config.intermediate_size,
bias=False,
use_tensor_parallel=use_tensor_parallel,
)
self.down_proj = _make_row_linear(
config.intermediate_size,
config.hidden_size,
bias=False,
use_tensor_parallel=use_tensor_parallel,
)
self.act_fn = ACT2FN[config.hidden_act]
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.act_fn(_linear_output(self.gate_proj, x)) * _linear_output(
self.up_proj, x
)
return _linear_output(self.down_proj, x)
class Qwen2_5_VLDecoderLayer(nn.Module): class Qwen2_5_VLDecoderLayer(nn.Module):
def __init__(self, config: Qwen2_5_VLTextConfig, layer_idx: int): def __init__(self, config: Qwen2_5_VLTextConfig, layer_idx: int):
super().__init__() super().__init__()
@@ -371,11 +345,26 @@ class Qwen2_5_VLDecoderLayer(nn.Module):
) )
self.self_attn = Qwen2_5_VLAttention(config, layer_idx) self.self_attn = Qwen2_5_VLAttention(config, layer_idx)
self.mlp = Qwen2_5_VLTextMLP(config) mlp_tp_size = _tp_world_size()
self.input_layernorm = Qwen2RMSNorm(config.hidden_size, eps=config.rms_norm_eps) if config.intermediate_size % mlp_tp_size != 0:
self.post_attention_layernorm = Qwen2RMSNorm( mlp_tp_size = 1
config.hidden_size, eps=config.rms_norm_eps self.mlp = Qwen2_5_VLMLP(
config.hidden_size,
config.intermediate_size,
bias=False,
hidden_act=config.hidden_act,
prefix=f"model.language_model.layers.{layer_idx}.mlp",
fuse_gate_up=False,
tp_size=mlp_tp_size,
tp_rank=_tp_rank() if mlp_tp_size > 1 else 0,
) )
norm_kwargs = dict(
eps=config.rms_norm_eps,
cast_x_before_out_mul=True,
force_native=True,
)
self.input_layernorm = RMSNorm(config.hidden_size, **norm_kwargs)
self.post_attention_layernorm = RMSNorm(config.hidden_size, **norm_kwargs)
self.attention_type = config.layer_types[layer_idx] self.attention_type = config.layer_types[layer_idx]
def forward( def forward(
@@ -387,9 +376,6 @@ class Qwen2_5_VLDecoderLayer(nn.Module):
output_attentions: Optional[bool] = False, output_attentions: Optional[bool] = False,
use_cache: Optional[bool] = False, use_cache: Optional[bool] = False,
cache_position: Optional[torch.LongTensor] = None, cache_position: Optional[torch.LongTensor] = None,
position_embeddings: Optional[
tuple[torch.Tensor, torch.Tensor]
] = None, # necessary, but kept here for BC
**kwargs: Unpack[FlashAttentionKwargs], **kwargs: Unpack[FlashAttentionKwargs],
) -> tuple[ ) -> tuple[
torch.FloatTensor, Optional[tuple[torch.FloatTensor, torch.FloatTensor]] torch.FloatTensor, Optional[tuple[torch.FloatTensor, torch.FloatTensor]]
@@ -408,9 +394,6 @@ class Qwen2_5_VLDecoderLayer(nn.Module):
past_key_values (`Tuple(torch.FloatTensor)`, *optional*): cached past key and value projection states past_key_values (`Tuple(torch.FloatTensor)`, *optional*): cached past key and value projection states
cache_position (`torch.LongTensor` of shape `(sequence_length)`, *optional*): cache_position (`torch.LongTensor` of shape `(sequence_length)`, *optional*):
Indices depicting the position of the input sequence tokens in the sequence. Indices depicting the position of the input sequence tokens in the sequence.
position_embeddings (`tuple[torch.FloatTensor, torch.FloatTensor]`, *optional*):
Tuple containing the cosine and sine positional embeddings of shape `(batch_size, seq_len, head_dim)`,
with `head_dim` being the embedding dimension of each attention head.
kwargs (`dict`, *optional*): kwargs (`dict`, *optional*):
Arbitrary kwargs to be ignored, used for FSDP and other methods that injects code Arbitrary kwargs to be ignored, used for FSDP and other methods that injects code
into the model into the model
@@ -429,7 +412,6 @@ class Qwen2_5_VLDecoderLayer(nn.Module):
output_attentions=output_attentions, output_attentions=output_attentions,
use_cache=use_cache, use_cache=use_cache,
cache_position=cache_position, cache_position=cache_position,
position_embeddings=position_embeddings,
**kwargs, **kwargs,
) )
hidden_states = residual + hidden_states hidden_states = residual + hidden_states
@@ -443,41 +425,6 @@ class Qwen2_5_VLDecoderLayer(nn.Module):
return hidden_states return hidden_states
class Qwen2_5_VLMLP(nn.Module):
def __init__(
self,
in_features: int,
hidden_features: int = None,
bias: bool = True,
hidden_act="silu",
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
):
super().__init__()
self.gate_up_proj = MergedColumnParallelLinear(
input_size=in_features,
output_sizes=[hidden_features] * 2, # [gate_proj, up_proj]
bias=bias,
quant_config=quant_config,
prefix=add_prefix("gate_up_proj", prefix),
)
self.down_proj = RowParallelLinear(
hidden_features,
in_features,
bias=bias,
quant_config=quant_config,
prefix=add_prefix("down_proj", prefix),
)
self.act = ACT2FN[hidden_act]
def forward(self, x: torch.Tensor) -> torch.Tensor:
gate_up, _ = self.gate_up_proj(x)
gate, up = gate_up.chunk(2, dim=-1)
x = self.act(gate) * up
x_down, _ = self.down_proj(x)
return x_down
class Qwen2_5_VLTextModel(nn.Module): class Qwen2_5_VLTextModel(nn.Module):
def __init__(self, config: PretrainedConfig): def __init__(self, config: PretrainedConfig):
super().__init__() super().__init__()
@@ -485,8 +432,11 @@ class Qwen2_5_VLTextModel(nn.Module):
self.padding_idx = config.pad_token_id self.padding_idx = config.pad_token_id
self.vocab_size = config.vocab_size self.vocab_size = config.vocab_size
self.embed_tokens = nn.Embedding( self.embed_tokens = VocabParallelEmbedding(
config.vocab_size, config.hidden_size, self.padding_idx config.vocab_size,
config.hidden_size,
org_num_embeddings=config.vocab_size,
prefix="model.language_model.embed_tokens",
) )
self.layers = nn.ModuleList( self.layers = nn.ModuleList(
[ [
@@ -495,8 +445,12 @@ class Qwen2_5_VLTextModel(nn.Module):
] ]
) )
self._attn_implementation = config._attn_implementation self._attn_implementation = config._attn_implementation
self.norm = Qwen2RMSNorm(config.hidden_size, eps=config.rms_norm_eps) self.norm = RMSNorm(
self.rotary_emb = Qwen2_5_VLRotaryEmbedding(config=config) config.hidden_size,
eps=config.rms_norm_eps,
cast_x_before_out_mul=True,
force_native=True,
)
self.has_sliding_layers = "sliding_attention" in self.config.layer_types self.has_sliding_layers = "sliding_attention" in self.config.layer_types
self.gradient_checkpointing = False self.gradient_checkpointing = False
@@ -600,9 +554,6 @@ class Qwen2_5_VLTextModel(nn.Module):
hidden_states = inputs_embeds hidden_states = inputs_embeds
# create position embeddings to be shared across the decoder layers
position_embeddings = self.rotary_emb(hidden_states, position_ids)
# decoder layers # decoder layers
all_hidden_states = () if output_hidden_states else None all_hidden_states = () if output_hidden_states else None
all_self_attns = () if output_attentions else None all_self_attns = () if output_attentions else None
@@ -614,12 +565,11 @@ class Qwen2_5_VLTextModel(nn.Module):
hidden_states = decoder_layer( hidden_states = decoder_layer(
hidden_states, hidden_states,
attention_mask=causal_mask_mapping[decoder_layer.attention_type], attention_mask=causal_mask_mapping[decoder_layer.attention_type],
position_ids=text_position_ids, position_ids=position_ids,
past_key_values=past_key_values, past_key_values=past_key_values,
output_attentions=output_attentions, output_attentions=output_attentions,
use_cache=use_cache, use_cache=use_cache,
cache_position=cache_position, cache_position=cache_position,
position_embeddings=position_embeddings,
**kwargs, **kwargs,
) )
@@ -1426,6 +1376,26 @@ class Qwen2_5_VLForConditionalGeneration(TextEncoder):
if not self.enable_image_understanding: if not self.enable_image_understanding:
continue continue
name = name.replace("visual.", "model.visual.") name = name.replace("visual.", "model.visual.")
name = name.replace(".attn.qkv.", ".attn.qkv_proj.")
loaded_stacked_param = False
for weight_name, shard_id in (
(".gate_proj.", 0),
(".up_proj.", 1),
):
if weight_name not in name:
continue
fused_name = name.replace(weight_name, ".gate_up_proj.")
if fused_name not in params_dict:
continue
param = params_dict[fused_name]
loaded_weight = loaded_weight.to(param.dtype)
param.weight_loader(param, loaded_weight, shard_id)
loaded_params.add(fused_name)
loaded_stacked_param = True
break
if loaded_stacked_param:
continue
try: try:
# Skip loading extra bias for GPTQ models. # Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict: if name.endswith(".bias") and name not in params_dict:
@@ -3,285 +3,75 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from typing import Any from typing import Any
import torch import torch
import torch.nn as nn import torch.nn as nn
import torch.nn.functional as F import torch.nn.functional as F
from sglang.multimodal_gen.runtime.layers.attention.selector import get_attn_backend from sglang.multimodal_gen.runtime.models.encoders.qwen_vl_vision import (
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger PackedSequenceMetadata,
QwenVLVisionAttention,
logger = init_logger(__name__) )
from sglang.srt.layers.layernorm import RMSNorm
from sglang.srt.models.qwen2_5_vl import (
@dataclass(frozen=True) Qwen2_5_VisionPatchEmbed as Qwen2_5VLVisionPatchEmbed,
class _PackedSequenceMetadata: )
cu_seqlens: torch.Tensor from sglang.srt.models.qwen2_5_vl import (
cu_seqlens_host: tuple[int, ...] Qwen2_5_VisionPatchMerger as Qwen2_5VLVisionPatchMerger,
max_seqlen: int )
from sglang.srt.models.qwen2_5_vl import (
@classmethod Qwen2_5_VisionRotaryEmbedding as Qwen2_5VLVisionRotaryEmbedding,
def from_cu_seqlens(cls, cu_seqlens: torch.Tensor) -> _PackedSequenceMetadata: )
bounds = tuple(int(value) for value in cu_seqlens.tolist()) from sglang.srt.models.qwen2_5_vl import (
return cls( Qwen2_5_VLMLP,
cu_seqlens=cu_seqlens, )
cu_seqlens_host=bounds,
max_seqlen=max(
stop - start for start, stop in zip(bounds[:-1], bounds[1:])
),
)
class Qwen2_5VLVisionRMSNorm(nn.Module):
def __init__(self, hidden_size: int, eps: float = 1e-6) -> None:
super().__init__()
self.weight = nn.Parameter(torch.ones(hidden_size))
self.variance_epsilon = eps
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
input_dtype = hidden_states.dtype
hidden_states = hidden_states.float()
variance = hidden_states.square().mean(dim=-1, keepdim=True)
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
return self.weight * hidden_states.to(input_dtype)
class Qwen2_5VLVisionPatchEmbed(nn.Module):
def __init__(
self,
patch_size: int,
temporal_patch_size: int,
in_channels: int,
embed_dim: int,
) -> None:
super().__init__()
self.patch_size = patch_size
self.temporal_patch_size = temporal_patch_size
self.in_channels = in_channels
self.embed_dim = embed_dim
kernel_size = (temporal_patch_size, patch_size, patch_size)
self.proj = nn.Conv3d(
in_channels,
embed_dim,
kernel_size=kernel_size,
stride=kernel_size,
bias=False,
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = hidden_states.view(
-1,
self.in_channels,
self.temporal_patch_size,
self.patch_size,
self.patch_size,
)
return self.proj(hidden_states.to(self.proj.weight.dtype)).view(
-1, self.embed_dim
)
class Qwen2_5VLVisionRotaryEmbedding(nn.Module):
def __init__(self, dim: int, theta: float = 10000.0) -> None:
super().__init__()
inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
self.register_buffer("inv_freq", inv_freq, persistent=False)
def forward(self, position_ids: torch.Tensor) -> torch.Tensor:
return (position_ids.unsqueeze(-1) * self.inv_freq).flatten(1)
def _rotate_half(hidden_states: torch.Tensor) -> torch.Tensor:
first, second = hidden_states.chunk(2, dim=-1)
return torch.cat((-second, first), dim=-1)
def _apply_vision_rotary_embedding(
query: torch.Tensor,
key: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
query_dtype = query.dtype
key_dtype = key.dtype
query = query.float()
key = key.float()
cos = cos.unsqueeze(-2).float()
sin = sin.unsqueeze(-2).float()
query = query * cos + _rotate_half(query) * sin
key = key * cos + _rotate_half(key) * sin
return query.to(query_dtype), key.to(key_dtype)
class Qwen2_5VLVisionAttention(nn.Module):
def __init__(self, config: Any, prefix: str) -> None:
super().__init__()
self.num_heads = config.num_heads
self.head_dim = config.hidden_size // config.num_heads
self.scaling = self.head_dim**-0.5
self.prefix = prefix
self.qkv = nn.Linear(config.hidden_size, config.hidden_size * 3, bias=True)
self.proj = nn.Linear(config.hidden_size, config.hidden_size)
self._attention_impl = None
self._initialize_attention(torch.get_default_dtype())
def _initialize_attention(self, dtype: torch.dtype) -> None:
backend = get_attn_backend(self.head_dim, dtype)
if backend.supports_packed_varlen():
self._attention_impl = backend.get_impl_cls()(
num_heads=self.num_heads,
head_size=self.head_dim,
num_kv_heads=self.num_heads,
softmax_scale=self.scaling,
causal=False,
prefix=self.prefix,
)
else:
logger.warning_once(
"Qwen2.5-VL vision attention uses torch SDPA because "
f"{backend.get_enum().name.lower()} does not support packed sequences"
)
def _packed_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
cu_seqlens: torch.Tensor,
cu_seqlens_host: tuple[int, ...],
max_seqlen: int,
) -> torch.Tensor:
if self._attention_impl is not None:
return self._attention_impl.forward_varlen(
query,
key,
value,
cu_seqlens=cu_seqlens,
cu_seqlens_host=cu_seqlens_host,
max_seqlen=max_seqlen,
)
output = torch.empty_like(query)
for start, stop in zip(cu_seqlens_host[:-1], cu_seqlens_host[1:]):
if start == stop:
continue
query_segment = query[start:stop].transpose(0, 1).unsqueeze(0)
key_segment = key[start:stop].transpose(0, 1).unsqueeze(0)
value_segment = value[start:stop].transpose(0, 1).unsqueeze(0)
segment = F.scaled_dot_product_attention(
query_segment,
key_segment,
value_segment,
dropout_p=0.0,
is_causal=False,
scale=self.scaling,
)
output[start:stop] = segment.squeeze(0).transpose(0, 1)
return output
def forward(
self,
hidden_states: torch.Tensor,
*,
cu_seqlens: torch.Tensor,
cu_seqlens_host: tuple[int, ...],
max_seqlen: int,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
) -> torch.Tensor:
seq_len = hidden_states.shape[0]
query, key, value = (
self.qkv(hidden_states)
.reshape(seq_len, 3, self.num_heads, self.head_dim)
.permute(1, 0, 2, 3)
.unbind(0)
)
query, key = _apply_vision_rotary_embedding(query, key, *position_embeddings)
output = self._packed_attention(
query,
key,
value,
cu_seqlens,
cu_seqlens_host,
max_seqlen,
)
return self.proj(output.reshape(seq_len, -1).contiguous())
class Qwen2_5VLVisionMLP(nn.Module):
def __init__(self, config: Any) -> None:
super().__init__()
if config.hidden_act != "silu":
raise ValueError(
f"Unsupported Qwen2.5-VL vision activation: {config.hidden_act}"
)
self.gate_proj = nn.Linear(
config.hidden_size, config.intermediate_size, bias=True
)
self.up_proj = nn.Linear(
config.hidden_size, config.intermediate_size, bias=True
)
self.down_proj = nn.Linear(
config.intermediate_size, config.hidden_size, bias=True
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
return self.down_proj(
F.silu(self.gate_proj(hidden_states)) * self.up_proj(hidden_states)
)
class Qwen2_5VLVisionBlock(nn.Module): class Qwen2_5VLVisionBlock(nn.Module):
def __init__(self, config: Any, layer_idx: int) -> None: def __init__(self, config: Any, layer_idx: int) -> None:
super().__init__() super().__init__()
self.norm1 = Qwen2_5VLVisionRMSNorm(config.hidden_size) self.norm1 = RMSNorm(
self.norm2 = Qwen2_5VLVisionRMSNorm(config.hidden_size) config.hidden_size,
self.attn = Qwen2_5VLVisionAttention( eps=1e-6,
config, prefix=f"visual.blocks.{layer_idx}.attn" 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,
) )
self.mlp = Qwen2_5VLVisionMLP(config)
def forward( def forward(
self, self,
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
*, *,
cu_seqlens: torch.Tensor, metadata: PackedSequenceMetadata,
cu_seqlens_host: tuple[int, ...],
max_seqlen: int,
position_embeddings: tuple[torch.Tensor, torch.Tensor], position_embeddings: tuple[torch.Tensor, torch.Tensor],
) -> torch.Tensor: ) -> torch.Tensor:
hidden_states = hidden_states + self.attn( hidden_states = hidden_states + self.attn(
self.norm1(hidden_states), self.norm1(hidden_states),
cu_seqlens=cu_seqlens, metadata=metadata,
cu_seqlens_host=cu_seqlens_host,
max_seqlen=max_seqlen,
position_embeddings=position_embeddings, position_embeddings=position_embeddings,
) )
return hidden_states + self.mlp(self.norm2(hidden_states)) return hidden_states + self.mlp(self.norm2(hidden_states))
class Qwen2_5VLVisionPatchMerger(nn.Module):
def __init__(
self,
output_dim: int,
context_dim: int,
spatial_merge_size: int,
) -> None:
super().__init__()
self.hidden_size = context_dim * spatial_merge_size**2
self.ln_q = Qwen2_5VLVisionRMSNorm(context_dim)
self.mlp = nn.Sequential(
nn.Linear(self.hidden_size, self.hidden_size),
nn.GELU(),
nn.Linear(self.hidden_size, output_dim),
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = self.ln_q(hidden_states).view(-1, self.hidden_size)
return self.mlp(hidden_states)
def _vision_position_ids( def _vision_position_ids(
grid_thw: torch.Tensor, spatial_merge_size: int grid_thw: torch.Tensor, spatial_merge_size: int
) -> torch.Tensor: ) -> torch.Tensor:
@@ -383,6 +173,7 @@ class Qwen2_5VLVisionTransformer(nn.Module):
temporal_patch_size=config.temporal_patch_size, temporal_patch_size=config.temporal_patch_size,
in_channels=config.in_channels, in_channels=config.in_channels,
embed_dim=config.hidden_size, embed_dim=config.hidden_size,
disable_linear=True,
) )
head_dim = config.hidden_size // config.num_heads head_dim = config.hidden_size // config.num_heads
self.rotary_pos_emb = Qwen2_5VLVisionRotaryEmbedding(head_dim // 2) self.rotary_pos_emb = Qwen2_5VLVisionRotaryEmbedding(head_dim // 2)
@@ -390,9 +181,13 @@ class Qwen2_5VLVisionTransformer(nn.Module):
Qwen2_5VLVisionBlock(config, layer_idx) for layer_idx in range(config.depth) Qwen2_5VLVisionBlock(config, layer_idx) for layer_idx in range(config.depth)
) )
self.merger = Qwen2_5VLVisionPatchMerger( self.merger = Qwen2_5VLVisionPatchMerger(
output_dim=config.out_hidden_size, dim=config.out_hidden_size,
context_dim=config.hidden_size, context_dim=config.hidden_size,
padded_context_dim=config.hidden_size,
spatial_merge_size=config.spatial_merge_size, spatial_merge_size=config.spatial_merge_size,
prefix="visual.merger",
cast_x_before_out_mul=True,
force_native_norm=True,
) )
@property @property
@@ -440,8 +235,8 @@ class Qwen2_5VLVisionTransformer(nn.Module):
).cumsum(dim=0, dtype=torch.int32) ).cumsum(dim=0, dtype=torch.int32)
cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0) cu_seqlens = F.pad(cu_seqlens, (1, 0), value=0)
full_metadata = _PackedSequenceMetadata.from_cu_seqlens(cu_seqlens) full_metadata = PackedSequenceMetadata.from_cu_seqlens(cu_seqlens)
window_metadata = _PackedSequenceMetadata.from_cu_seqlens(cu_window_seqlens) window_metadata = PackedSequenceMetadata.from_cu_seqlens(cu_window_seqlens)
for layer_idx, block in enumerate(self.blocks): for layer_idx, block in enumerate(self.blocks):
metadata = ( metadata = (
@@ -451,9 +246,7 @@ class Qwen2_5VLVisionTransformer(nn.Module):
) )
hidden_states = block( hidden_states = block(
hidden_states, hidden_states,
cu_seqlens=metadata.cu_seqlens, metadata=metadata,
cu_seqlens_host=metadata.cu_seqlens_host,
max_seqlen=metadata.max_seqlen,
position_embeddings=position_embeddings, position_embeddings=position_embeddings,
) )
@@ -7,9 +7,8 @@ from torch import nn
from sglang.multimodal_gen.configs.models.encoders import BaseEncoderOutput from sglang.multimodal_gen.configs.models.encoders import BaseEncoderOutput
from sglang.multimodal_gen.configs.models.encoders.qwen3 import Qwen3TextConfig from sglang.multimodal_gen.configs.models.encoders.qwen3 import Qwen3TextConfig
from sglang.multimodal_gen.runtime.distributed import get_tp_world_size from sglang.multimodal_gen.runtime.distributed import get_tp_world_size
from sglang.multimodal_gen.runtime.layers.activation import SiluAndMul
from sglang.multimodal_gen.runtime.layers.attention import LocalAttention from sglang.multimodal_gen.runtime.layers.attention import LocalAttention
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm as MMGenRMSNorm
from sglang.multimodal_gen.runtime.layers.linear import ( from sglang.multimodal_gen.runtime.layers.linear import (
MergedColumnParallelLinear, MergedColumnParallelLinear,
QKVParallelLinear, QKVParallelLinear,
@@ -25,6 +24,8 @@ from sglang.multimodal_gen.runtime.loader.weight_utils import (
maybe_remap_kv_scale_name, maybe_remap_kv_scale_name,
) )
from sglang.multimodal_gen.runtime.models.encoders.base import TextEncoder from sglang.multimodal_gen.runtime.models.encoders.base import TextEncoder
from sglang.srt.layers.activation import SiluAndMul
from sglang.srt.layers.layernorm import RMSNorm
class Qwen3MLP(nn.Module): class Qwen3MLP(nn.Module):
@@ -131,8 +132,9 @@ class Qwen3Attention(nn.Module):
# QK-Norm: Key difference from LLaMA # QK-Norm: Key difference from LLaMA
rms_norm_eps = getattr(config, "rms_norm_eps", 1e-6) rms_norm_eps = getattr(config, "rms_norm_eps", 1e-6)
self.q_norm = RMSNorm(self.head_dim, eps=rms_norm_eps) # Keep the small-hidden one-pass kernel used by diffusion QK norm.
self.k_norm = RMSNorm(self.head_dim, eps=rms_norm_eps) self.q_norm = MMGenRMSNorm(self.head_dim, eps=rms_norm_eps)
self.k_norm = MMGenRMSNorm(self.head_dim, eps=rms_norm_eps)
# Rotary embeddings # Rotary embeddings
self.rotary_emb = get_rope( self.rotary_emb = get_rope(
@@ -32,7 +32,12 @@ from sglang.multimodal_gen.runtime.models.encoders.base import TextEncoder
from sglang.multimodal_gen.runtime.models.encoders.qwen3vl_vision import ( from sglang.multimodal_gen.runtime.models.encoders.qwen3vl_vision import (
Qwen3VLVisionTransformer, Qwen3VLVisionTransformer,
) )
from sglang.multimodal_gen.runtime.models.encoders.qwen_vl_rope import (
apply_qwen_vl_text_rope,
build_qwen_vl_text_rope,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.srt.layers.layernorm import RMSNorm
"""Inference-only Qwen3-VL model compatible with HuggingFace weights.""" """Inference-only Qwen3-VL model compatible with HuggingFace weights."""
import logging import logging
@@ -57,12 +62,18 @@ from transformers.models.qwen3_vl.configuration_qwen3_vl import (
from transformers.models.qwen3_vl.modeling_qwen3_vl import ( from transformers.models.qwen3_vl.modeling_qwen3_vl import (
Qwen3VLCausalLMOutputWithPast, Qwen3VLCausalLMOutputWithPast,
Qwen3VLModelOutputWithPast, Qwen3VLModelOutputWithPast,
Qwen3VLTextRMSNorm,
Qwen3VLTextRotaryEmbedding,
apply_rotary_pos_emb,
) )
def _make_text_rms_norm(hidden_size: int, eps: float) -> RMSNorm:
return RMSNorm(
hidden_size,
eps=eps,
cast_x_before_out_mul=True,
force_native=True,
)
class Qwen3VLQuantizedLinear(ReplicatedLinear): class Qwen3VLQuantizedLinear(ReplicatedLinear):
def forward(self, x: torch.Tensor) -> torch.Tensor: def forward(self, x: torch.Tensor) -> torch.Tensor:
return super().forward(x)[0] return super().forward(x)[0]
@@ -270,12 +281,9 @@ class Qwen3VLTextAttention(nn.Module):
use_tensor_parallel=use_tensor_parallel, use_tensor_parallel=use_tensor_parallel,
prefix=f"{prefix}.o_proj", prefix=f"{prefix}.o_proj",
) )
self.q_norm = Qwen3VLTextRMSNorm( self.q_norm = _make_text_rms_norm(self.head_dim, config.rms_norm_eps)
self.head_dim, eps=config.rms_norm_eps self.k_norm = _make_text_rms_norm(self.head_dim, config.rms_norm_eps)
) # unlike olmo, only on the head dim! self.rotary_emb = build_qwen_vl_text_rope(config, mrope_interleaved=True)
self.k_norm = Qwen3VLTextRMSNorm(
self.head_dim, eps=config.rms_norm_eps
) # thus post q_norm does not need reshape
self.attn = LocalAttention( self.attn = LocalAttention(
num_heads=self.num_heads, num_heads=self.num_heads,
@@ -292,7 +300,7 @@ class Qwen3VLTextAttention(nn.Module):
def forward( def forward(
self, self,
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor], position_ids: torch.LongTensor,
attention_mask: Optional[torch.Tensor], attention_mask: Optional[torch.Tensor],
past_key_values: Optional[Cache] = None, past_key_values: Optional[Cache] = None,
cache_position: Optional[torch.LongTensor] = None, cache_position: Optional[torch.LongTensor] = None,
@@ -309,14 +317,15 @@ class Qwen3VLTextAttention(nn.Module):
).transpose(1, 2) ).transpose(1, 2)
value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2) value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
cos, sin = position_embeddings query_states, key_states = apply_qwen_vl_text_rope(
query_states, key_states = apply_rotary_pos_emb( self.rotary_emb,
query_states, key_states, cos, sin position_ids,
query_states,
key_states,
) )
if past_key_values is not None: if past_key_values is not None:
# sin and cos are specific to RoPE models; cache_position needed for the static cache cache_kwargs = {"cache_position": cache_position}
cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
key_states, value_states = past_key_values.update( key_states, value_states = past_key_values.update(
key_states, value_states, self.layer_idx, cache_kwargs key_states, value_states, self.layer_idx, cache_kwargs
) )
@@ -432,17 +441,16 @@ class Qwen3VLTextDecoderLayer(nn.Module):
use_tensor_parallel=use_tensor_parallel, use_tensor_parallel=use_tensor_parallel,
prefix=f"{prefix}.mlp", prefix=f"{prefix}.mlp",
) )
self.input_layernorm = Qwen3VLTextRMSNorm( self.input_layernorm = _make_text_rms_norm(
config.hidden_size, eps=config.rms_norm_eps config.hidden_size, config.rms_norm_eps
) )
self.post_attention_layernorm = Qwen3VLTextRMSNorm( self.post_attention_layernorm = _make_text_rms_norm(
config.hidden_size, eps=config.rms_norm_eps config.hidden_size, config.rms_norm_eps
) )
def forward( def forward(
self, self,
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
attention_mask: Optional[torch.Tensor] = None, attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None, position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Cache] = None, past_key_values: Optional[Cache] = None,
@@ -460,7 +468,6 @@ class Qwen3VLTextDecoderLayer(nn.Module):
past_key_values=past_key_values, past_key_values=past_key_values,
use_cache=use_cache, use_cache=use_cache,
cache_position=cache_position, cache_position=cache_position,
position_embeddings=position_embeddings,
**kwargs, **kwargs,
) )
hidden_states = residual + hidden_states hidden_states = residual + hidden_states
@@ -505,8 +512,7 @@ class Qwen3VLTextModel(nn.Module):
for layer_idx in range(config.num_hidden_layers) for layer_idx in range(config.num_hidden_layers)
] ]
) )
self.norm = Qwen3VLTextRMSNorm(config.hidden_size, eps=config.rms_norm_eps) self.norm = _make_text_rms_norm(config.hidden_size, config.rms_norm_eps)
self.rotary_emb = Qwen3VLTextRotaryEmbedding(config=config)
self.gradient_checkpointing = False self.gradient_checkpointing = False
# Initialize weights and apply final processing # Initialize weights and apply final processing
@@ -582,15 +588,10 @@ class Qwen3VLTextModel(nn.Module):
position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1) position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1)
if position_ids.ndim == 3 and position_ids.shape[0] == 4: if position_ids.ndim == 3 and position_ids.shape[0] == 4:
text_position_ids = position_ids[0]
position_ids = position_ids[1:] position_ids = position_ids[1:]
else:
text_position_ids = position_ids[0]
hidden_states = inputs_embeds hidden_states = inputs_embeds
# create position embeddings to be shared across the decoder layers
position_embeddings = self.rotary_emb(hidden_states, position_ids)
all_hidden_states = () if output_hidden_states else None all_hidden_states = () if output_hidden_states else None
all_self_attns = () if output_attentions else None all_self_attns = () if output_attentions else None
# decoder layers # decoder layers
@@ -598,11 +599,10 @@ class Qwen3VLTextModel(nn.Module):
hidden_states = decoder_layer( hidden_states = decoder_layer(
hidden_states, hidden_states,
attention_mask=attention_mask, attention_mask=attention_mask,
position_ids=text_position_ids, position_ids=position_ids,
past_key_values=past_key_values, past_key_values=past_key_values,
cache_position=cache_position, cache_position=cache_position,
output_attentions=output_attentions, output_attentions=output_attentions,
position_embeddings=position_embeddings,
**kwargs, **kwargs,
) )
# hidden_states = layer_outputs # hidden_states = layer_outputs
@@ -1269,6 +1269,8 @@ class Qwen3VLForConditionalGeneration(TextEncoder):
for name, loaded_weight in weights: for name, loaded_weight in weights:
if "rotary_emb.inv_freq" in name: if "rotary_emb.inv_freq" in name:
continue continue
if "visual." in name:
name = name.replace(".attn.qkv.", ".attn.qkv_proj.")
try: try:
param = params_dict[name] param = params_dict[name]
@@ -10,10 +10,16 @@ import torch
import torch.nn as nn import torch.nn as nn
import torch.nn.functional as F import torch.nn.functional as F
from sglang.multimodal_gen.runtime.layers.attention.selector import get_attn_backend from sglang.multimodal_gen.runtime.models.encoders.qwen_vl_vision import (
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger PackedSequenceMetadata,
QwenVLVisionAttention,
logger = init_logger(__name__) )
from sglang.srt.models.qwen3_vl import (
Qwen3_VisionMLP,
Qwen3VLMoeVisionPatchMerger,
Qwen3VLVisionPatchEmbed,
)
from sglang.srt.runtime_context import get_parallel
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -23,57 +29,6 @@ class Qwen3VLVisionOutput:
deepstack_features: list[torch.Tensor] deepstack_features: list[torch.Tensor]
@dataclass(frozen=True)
class _PackedSequenceMetadata:
cu_seqlens: torch.Tensor
cu_seqlens_host: tuple[int, ...]
max_seqlen: int
@classmethod
def from_cu_seqlens(cls, cu_seqlens: torch.Tensor) -> _PackedSequenceMetadata:
bounds = tuple(int(value) for value in cu_seqlens.tolist())
return cls(
cu_seqlens=cu_seqlens,
cu_seqlens_host=bounds,
max_seqlen=max(
stop - start for start, stop in zip(bounds[:-1], bounds[1:])
),
)
class Qwen3VLVisionPatchEmbed(nn.Module):
def __init__(self, config: Any) -> None:
super().__init__()
self.patch_size = config.patch_size
self.temporal_patch_size = config.temporal_patch_size
self.in_channels = config.in_channels
self.embed_dim = config.hidden_size
kernel_size = (
config.temporal_patch_size,
config.patch_size,
config.patch_size,
)
self.proj = nn.Conv3d(
config.in_channels,
config.hidden_size,
kernel_size=kernel_size,
stride=kernel_size,
bias=True,
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = hidden_states.view(
-1,
self.in_channels,
self.temporal_patch_size,
self.patch_size,
self.patch_size,
)
return self.proj(hidden_states.to(self.proj.weight.dtype)).view(
-1, self.embed_dim
)
class Qwen3VLVisionRotaryEmbedding(nn.Module): class Qwen3VLVisionRotaryEmbedding(nn.Module):
def __init__(self, dim: int, theta: float = 10000.0) -> None: def __init__(self, dim: int, theta: float = 10000.0) -> None:
super().__init__() super().__init__()
@@ -89,140 +44,32 @@ class Qwen3VLVisionRotaryEmbedding(nn.Module):
return torch.outer(positions, self.inv_freq) return torch.outer(positions, self.inv_freq)
def _rotate_half(hidden_states: torch.Tensor) -> torch.Tensor:
first, second = hidden_states.chunk(2, dim=-1)
return torch.cat((-second, first), dim=-1)
def _apply_vision_rotary_embedding(
query: torch.Tensor,
key: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
query_dtype = query.dtype
key_dtype = key.dtype
query = query.float()
key = key.float()
cos = cos.unsqueeze(-2).float()
sin = sin.unsqueeze(-2).float()
query = query * cos + _rotate_half(query) * sin
key = key * cos + _rotate_half(key) * sin
return query.to(query_dtype), key.to(key_dtype)
class Qwen3VLVisionAttention(nn.Module):
def __init__(self, config: Any, prefix: str) -> None:
super().__init__()
self.num_heads = config.num_heads
self.head_dim = config.hidden_size // config.num_heads
self.scaling = self.head_dim**-0.5
self.qkv = nn.Linear(config.hidden_size, config.hidden_size * 3, bias=True)
self.proj = nn.Linear(config.hidden_size, config.hidden_size)
backend = get_attn_backend(self.head_dim, torch.get_default_dtype())
self._attention_impl = None
if backend.supports_packed_varlen():
self._attention_impl = backend.get_impl_cls()(
num_heads=self.num_heads,
head_size=self.head_dim,
num_kv_heads=self.num_heads,
softmax_scale=self.scaling,
causal=False,
prefix=prefix,
)
else:
logger.warning_once(
"Qwen3-VL vision attention uses torch SDPA because "
f"{backend.get_enum().name.lower()} does not support packed sequences"
)
def _packed_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
metadata: _PackedSequenceMetadata,
) -> torch.Tensor:
if self._attention_impl is not None:
return self._attention_impl.forward_varlen(
query,
key,
value,
cu_seqlens=metadata.cu_seqlens,
cu_seqlens_host=metadata.cu_seqlens_host,
max_seqlen=metadata.max_seqlen,
)
output = torch.empty_like(query)
for start, stop in zip(
metadata.cu_seqlens_host[:-1], metadata.cu_seqlens_host[1:]
):
if start == stop:
continue
segment = F.scaled_dot_product_attention(
query[start:stop].transpose(0, 1).unsqueeze(0),
key[start:stop].transpose(0, 1).unsqueeze(0),
value[start:stop].transpose(0, 1).unsqueeze(0),
dropout_p=0.0,
is_causal=False,
scale=self.scaling,
)
output[start:stop] = segment.squeeze(0).transpose(0, 1)
return output
def forward(
self,
hidden_states: torch.Tensor,
*,
metadata: _PackedSequenceMetadata,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
) -> torch.Tensor:
sequence_length = hidden_states.shape[0]
query, key, value = (
self.qkv(hidden_states)
.reshape(sequence_length, 3, self.num_heads, self.head_dim)
.permute(1, 0, 2, 3)
.unbind(0)
)
query, key = _apply_vision_rotary_embedding(query, key, *position_embeddings)
output = self._packed_attention(query, key, value, metadata)
return self.proj(output.reshape(sequence_length, -1).contiguous())
class Qwen3VLVisionMLP(nn.Module):
def __init__(self, config: Any) -> None:
super().__init__()
if config.hidden_act != "gelu_pytorch_tanh":
raise ValueError(
f"Unsupported Qwen3-VL vision activation: {config.hidden_act}"
)
self.linear_fc1 = nn.Linear(
config.hidden_size, config.intermediate_size, bias=True
)
self.linear_fc2 = nn.Linear(
config.intermediate_size, config.hidden_size, bias=True
)
self.act_fn = nn.GELU(approximate="tanh")
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
return self.linear_fc2(self.act_fn(self.linear_fc1(hidden_states)))
class Qwen3VLVisionBlock(nn.Module): class Qwen3VLVisionBlock(nn.Module):
def __init__(self, config: Any, layer_idx: int) -> None: def __init__(self, config: Any, layer_idx: int) -> None:
super().__init__() super().__init__()
parallel = get_parallel()
self.norm1 = nn.LayerNorm(config.hidden_size, eps=1e-6) self.norm1 = nn.LayerNorm(config.hidden_size, eps=1e-6)
self.norm2 = nn.LayerNorm(config.hidden_size, eps=1e-6) self.norm2 = nn.LayerNorm(config.hidden_size, eps=1e-6)
self.attn = Qwen3VLVisionAttention( self.attn = QwenVLVisionAttention(
config, prefix=f"visual.blocks.{layer_idx}.attn" config,
prefix=f"visual.blocks.{layer_idx}.attn",
model_name="Qwen3-VL",
)
self.mlp = Qwen3_VisionMLP(
config.hidden_size,
config.intermediate_size,
bias=True,
hidden_act=config.hidden_act,
prefix=f"visual.blocks.{layer_idx}.mlp",
tp_rank=parallel.tp_rank,
tp_size=parallel.tp_size,
) )
self.mlp = Qwen3VLVisionMLP(config)
def forward( def forward(
self, self,
hidden_states: torch.Tensor, hidden_states: torch.Tensor,
*, *,
metadata: _PackedSequenceMetadata, metadata: PackedSequenceMetadata,
position_embeddings: tuple[torch.Tensor, torch.Tensor], position_embeddings: tuple[torch.Tensor, torch.Tensor],
) -> torch.Tensor: ) -> torch.Tensor:
hidden_states = hidden_states + self.attn( hidden_states = hidden_states + self.attn(
@@ -233,24 +80,6 @@ class Qwen3VLVisionBlock(nn.Module):
return hidden_states + self.mlp(self.norm2(hidden_states)) return hidden_states + self.mlp(self.norm2(hidden_states))
class Qwen3VLVisionPatchMerger(nn.Module):
def __init__(self, config: Any, *, use_postshuffle_norm: bool) -> None:
super().__init__()
self.hidden_size = config.hidden_size * config.spatial_merge_size**2
self.use_postshuffle_norm = use_postshuffle_norm
norm_size = self.hidden_size if use_postshuffle_norm else config.hidden_size
self.norm = nn.LayerNorm(norm_size, eps=1e-6)
self.linear_fc1 = nn.Linear(self.hidden_size, self.hidden_size)
self.act_fn = nn.GELU()
self.linear_fc2 = nn.Linear(self.hidden_size, config.out_hidden_size)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
if self.use_postshuffle_norm:
hidden_states = hidden_states.view(-1, self.hidden_size)
hidden_states = self.norm(hidden_states).view(-1, self.hidden_size)
return self.linear_fc2(self.act_fn(self.linear_fc1(hidden_states)))
def _vision_position_ids( def _vision_position_ids(
grid_thw: torch.Tensor, spatial_merge_size: int grid_thw: torch.Tensor, spatial_merge_size: int
) -> torch.Tensor: ) -> torch.Tensor:
@@ -349,11 +178,12 @@ def _vision_cu_seqlens(grid_thw: torch.Tensor) -> torch.Tensor:
class Qwen3VLVisionTransformer(nn.Module): class Qwen3VLVisionTransformer(nn.Module):
def __init__(self, config: Any) -> None: def __init__(self, config: Any) -> None:
super().__init__() super().__init__()
parallel = get_parallel()
self.config = config self.config = config
self.spatial_merge_size = config.spatial_merge_size self.spatial_merge_size = config.spatial_merge_size
self.spatial_merge_unit = config.spatial_merge_size**2 self.spatial_merge_unit = config.spatial_merge_size**2
self.patch_size = config.patch_size self.patch_size = config.patch_size
self.patch_embed = Qwen3VLVisionPatchEmbed(config) self.patch_embed = Qwen3VLVisionPatchEmbed(config, disable_linear=True)
self.pos_embed = nn.Embedding( self.pos_embed = nn.Embedding(
config.num_position_embeddings, config.hidden_size config.num_position_embeddings, config.hidden_size
) )
@@ -363,11 +193,29 @@ class Qwen3VLVisionTransformer(nn.Module):
self.blocks = nn.ModuleList( self.blocks = nn.ModuleList(
Qwen3VLVisionBlock(config, layer_idx) for layer_idx in range(config.depth) Qwen3VLVisionBlock(config, layer_idx) for layer_idx in range(config.depth)
) )
self.merger = Qwen3VLVisionPatchMerger(config, use_postshuffle_norm=False) self.merger = Qwen3VLMoeVisionPatchMerger(
dim=config.out_hidden_size,
context_dim=config.hidden_size,
padded_context_dim=config.hidden_size,
spatial_merge_size=config.spatial_merge_size,
use_postshuffle_norm=False,
prefix="visual.merger",
tp_rank=parallel.tp_rank,
tp_size=parallel.tp_size,
)
self.deepstack_visual_indexes = tuple(config.deepstack_visual_indexes) self.deepstack_visual_indexes = tuple(config.deepstack_visual_indexes)
self.deepstack_merger_list = nn.ModuleList( self.deepstack_merger_list = nn.ModuleList(
Qwen3VLVisionPatchMerger(config, use_postshuffle_norm=True) Qwen3VLMoeVisionPatchMerger(
for _ in self.deepstack_visual_indexes dim=config.out_hidden_size,
context_dim=config.hidden_size,
padded_context_dim=config.hidden_size,
spatial_merge_size=config.spatial_merge_size,
use_postshuffle_norm=True,
prefix=f"visual.deepstack_merger_list.{merger_idx}",
tp_rank=parallel.tp_rank,
tp_size=parallel.tp_size,
)
for merger_idx, _ in enumerate(self.deepstack_visual_indexes)
) )
self._deepstack_merger_by_layer = { self._deepstack_merger_by_layer = {
layer_idx: merger_idx layer_idx: merger_idx
@@ -407,7 +255,7 @@ class Qwen3VLVisionTransformer(nn.Module):
rotary = rotary.flatten(1) rotary = rotary.flatten(1)
rotary = torch.cat((rotary, rotary), dim=-1) rotary = torch.cat((rotary, rotary), dim=-1)
position_embeddings = (rotary.cos(), rotary.sin()) position_embeddings = (rotary.cos(), rotary.sin())
metadata = _PackedSequenceMetadata.from_cu_seqlens(_vision_cu_seqlens(grid_thw)) metadata = PackedSequenceMetadata.from_cu_seqlens(_vision_cu_seqlens(grid_thw))
deepstack_features = [] deepstack_features = []
for layer_idx, block in enumerate(self.blocks): for layer_idx, block in enumerate(self.blocks):
@@ -0,0 +1,65 @@
# SPDX-License-Identifier: Apache-2.0
"""Shared SRT rotary embedding adapter for Qwen-VL text encoders."""
from typing import Any
import torch
from sglang.srt.layers.rotary_embedding import get_rope
from sglang.srt.layers.rotary_embedding.base import RotaryEmbedding
from sglang.srt.utils.hf_transformers.common import get_rope_config
def build_qwen_vl_text_rope(
config: Any, *, mrope_interleaved: bool = False
) -> RotaryEmbedding:
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]:
"""Apply three-axis MRoPE to batched attention tensors."""
if query.ndim != 4 or key.ndim != 4:
raise ValueError(
"Qwen-VL query and key must have shape "
"[batch, heads, sequence, head_dim]"
)
if position_ids.ndim != 3 or position_ids.shape[0] != 3:
raise ValueError(
"Qwen-VL text position_ids must have shape [3, batch, sequence]"
)
batch_size, num_query_heads, sequence_length, head_dim = query.shape
key_batch_size, num_key_value_heads, key_sequence_length, key_head_dim = key.shape
if (key_batch_size, key_sequence_length, key_head_dim) != (
batch_size,
sequence_length,
head_dim,
):
raise ValueError("Qwen-VL query and key shapes are incompatible")
if tuple(position_ids.shape[1:]) != (batch_size, sequence_length):
raise ValueError("Qwen-VL position_ids do not match the attention input")
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)
# Preserve HF's bf16 arithmetic order; fused MRoPE changes generated images.
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)
@@ -0,0 +1,154 @@
# SPDX-License-Identifier: Apache-2.0
"""Shared Qwen-VL vision attention for multimodal generation."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
from sglang.multimodal_gen.runtime.layers.attention.selector import get_attn_backend
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.srt.layers.linear import QKVParallelLinear, RowParallelLinear
from sglang.srt.runtime_context import get_parallel
logger = init_logger(__name__)
@dataclass(frozen=True)
class PackedSequenceMetadata:
cu_seqlens: torch.Tensor
cu_seqlens_host: tuple[int, ...]
max_seqlen: int
@classmethod
def from_cu_seqlens(cls, cu_seqlens: torch.Tensor) -> PackedSequenceMetadata:
bounds = tuple(int(value) for value in cu_seqlens.tolist())
return cls(
cu_seqlens=cu_seqlens,
cu_seqlens_host=bounds,
max_seqlen=max(
stop - start for start, stop in zip(bounds[:-1], bounds[1:])
),
)
def _rotate_half(hidden_states: torch.Tensor) -> torch.Tensor:
first, second = hidden_states.chunk(2, dim=-1)
return torch.cat((-second, first), dim=-1)
def _apply_rotary_embedding(
query: torch.Tensor,
key: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
query_dtype = query.dtype
key_dtype = key.dtype
query = query.float()
key = key.float()
cos = cos.unsqueeze(-2).float()
sin = sin.unsqueeze(-2).float()
query = query * cos + _rotate_half(query) * sin
key = key * cos + _rotate_half(key) * sin
return query.to(query_dtype), key.to(key_dtype)
class QwenVLVisionAttention(nn.Module):
def __init__(self, config: Any, *, prefix: str, model_name: str) -> None:
super().__init__()
parallel = get_parallel()
self.num_heads = config.num_heads // parallel.tp_size
self.head_dim = config.hidden_size // config.num_heads
self.scaling = self.head_dim**-0.5
self.qkv_proj = QKVParallelLinear(
hidden_size=config.hidden_size,
head_size=self.head_dim,
total_num_heads=config.num_heads,
bias=True,
prefix=f"{prefix}.qkv_proj",
tp_rank=parallel.tp_rank,
tp_size=parallel.tp_size,
)
self.proj = RowParallelLinear(
input_size=config.hidden_size,
output_size=config.hidden_size,
bias=True,
prefix=f"{prefix}.proj",
tp_rank=parallel.tp_rank,
tp_size=parallel.tp_size,
)
backend = get_attn_backend(self.head_dim, torch.get_default_dtype())
self._attention_impl = None
if backend.supports_packed_varlen():
self._attention_impl = backend.get_impl_cls()(
num_heads=self.num_heads,
head_size=self.head_dim,
num_kv_heads=self.num_heads,
softmax_scale=self.scaling,
causal=False,
prefix=prefix,
)
else:
logger.warning_once(
f"{model_name} vision attention uses torch SDPA because "
f"{backend.get_enum().name.lower()} does not support packed sequences"
)
def _packed_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
metadata: PackedSequenceMetadata,
) -> torch.Tensor:
if self._attention_impl is not None:
return self._attention_impl.forward_varlen(
query,
key,
value,
cu_seqlens=metadata.cu_seqlens,
cu_seqlens_host=metadata.cu_seqlens_host,
max_seqlen=metadata.max_seqlen,
)
output = torch.empty_like(query)
for start, stop in zip(
metadata.cu_seqlens_host[:-1], metadata.cu_seqlens_host[1:]
):
if start == stop:
continue
segment = F.scaled_dot_product_attention(
query[start:stop].transpose(0, 1).unsqueeze(0),
key[start:stop].transpose(0, 1).unsqueeze(0),
value[start:stop].transpose(0, 1).unsqueeze(0),
dropout_p=0.0,
is_causal=False,
scale=self.scaling,
)
output[start:stop] = segment.squeeze(0).transpose(0, 1)
return output
def forward(
self,
hidden_states: torch.Tensor,
*,
metadata: PackedSequenceMetadata,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
) -> torch.Tensor:
sequence_length = hidden_states.shape[0]
qkv, _ = self.qkv_proj(hidden_states)
query, key, value = (
qkv.reshape(sequence_length, 3, self.num_heads, self.head_dim)
.permute(1, 0, 2, 3)
.unbind(0)
)
query, key = _apply_rotary_embedding(query, key, *position_embeddings)
output = self._packed_attention(query, key, value, metadata)
output, _ = self.proj(output.reshape(sequence_length, -1).contiguous())
return output
@@ -1177,6 +1177,7 @@ STANDALONE_FILES = {
"../single_test_file/test_disagg_server.py", "../single_test_file/test_disagg_server.py",
"../single_test_file/test_ar_models.py", "../single_test_file/test_ar_models.py",
"../single_test_file/test_ipc_a2a_2_gpu.py", "../single_test_file/test_ipc_a2a_2_gpu.py",
"../single_test_file/test_encoder_fold_srt_linear_2_gpu.py",
"../single_test_file/test_encoder_fold_srt_2_gpu.py", "../single_test_file/test_encoder_fold_srt_2_gpu.py",
"../single_test_file/test_diffusion_bcg_tp2_zimage_turbo.py", "../single_test_file/test_diffusion_bcg_tp2_zimage_turbo.py",
"../single_test_file/test_dp_serving_2_gpu.py", "../single_test_file/test_dp_serving_2_gpu.py",
@@ -1216,6 +1217,7 @@ STANDALONE_FILE_EST_TIMES = {
"../single_test_file/test_ar_models.py": 600.0, "../single_test_file/test_ar_models.py": 600.0,
# no model load; the cost is the one-time JIT build of the sync kernels # no model load; the cost is the one-time JIT build of the sync kernels
"../single_test_file/test_ipc_a2a_2_gpu.py": 240.0, "../single_test_file/test_ipc_a2a_2_gpu.py": 240.0,
"../single_test_file/test_encoder_fold_srt_linear_2_gpu.py": 120.0,
"../single_test_file/test_encoder_fold_srt_2_gpu.py": 240.0, "../single_test_file/test_encoder_fold_srt_2_gpu.py": 240.0,
# ~60 s locally with a warm HF cache (load + one capture + 4 steps); # ~60 s locally with a warm HF cache (load + one capture + 4 steps);
# padded for cold-cache CI. # padded for cold-cache CI.
@@ -477,37 +477,40 @@ class AccuracyEngine:
for name, tensor in target.named_parameters(): for name, tensor in target.named_parameters():
total += 1 total += 1
src_tensor = None src_tensor = None
for cand in generate_name_candidates(name, reverse_mapping): candidates = generate_name_candidates(name, reverse_mapping)
for cand in candidates:
if cand in lookup: if cand in lookup:
src_tensor = lookup[cand] src_tensor = lookup[cand]
break break
if src_tensor is None: if src_tensor is None:
for cand in generate_name_candidates(name, reverse_mapping): for cand in candidates:
src_tensor = fuse_qkv(lookup, cand) src_tensor = fuse_qkv(lookup, cand)
if src_tensor is not None: if src_tensor is not None:
break break
if src_tensor is None: if src_tensor is None:
for cand in generate_name_candidates(name, reverse_mapping): for cand in candidates:
src_tensor = fuse_gate_up_proj(lookup, cand) src_tensor = fuse_gate_up_proj(lookup, cand)
if src_tensor is not None: if src_tensor is not None:
break break
if src_tensor is None:
unmatched_details.append(f"{name}: no matching source tensor")
continue
shard_context = shard_contexts.get(name) shard_context = shard_contexts.get(name)
shard_world_size = ( shard_world_size = (
shard_context.world_size if shard_context is not None else tp_world shard_context.world_size if shard_context is not None else tp_world
) )
shard_rank = shard_context.rank if shard_context is not None else rank shard_rank = shard_context.rank if shard_context is not None else rank
# TP-sharded params must load via their own weight_loader; the # Production loaders own fused projection sharding and alignment
# generic narrow mis-slices fused QKV/gate_up projections. # padding. Use them for TP parameters and whenever a direct copy
needs_weight_loader = ( # cannot represent the source layout, including TP=1.
shard_world_size > 1 or tensor.shape != src_tensor.shape requires_weight_loader = (
shard_world_size > 1
or src_tensor is None
or src_tensor.shape != tensor.shape
) )
if needs_weight_loader and load_param_with_weight_loader( if requires_weight_loader and load_param_with_weight_loader(
tensor, name, lookup, reverse_mapping tensor, name, lookup, reverse_mapping
): ):
matched += 1 matched += 1
elif src_tensor is None:
unmatched_details.append(f"{name}: no matching source tensor")
elif copy_tensor(tensor, src_tensor, shard_world_size, shard_rank): elif copy_tensor(tensor, src_tensor, shard_world_size, shard_rank):
matched += 1 matched += 1
else: else:
@@ -915,7 +915,11 @@ def load_param_with_weight_loader(param, name, lookup, reverse_mapping) -> bool:
loader(param, tensor.to(dtype=param.dtype), shard_id) loader(param, tensor.to(dtype=param.dtype), shard_id)
return True return True
for cand in candidates: for cand in candidates:
src = lookup.get(cand) source_names = [cand]
if "qkv_proj" in cand:
source_names.append(cand.replace("qkv_proj", "qkv"))
for source_name in source_names:
src = lookup.get(source_name)
if src is not None: if src is not None:
loader(param, src.to(dtype=param.dtype)) loader(param, src.to(dtype=param.dtype))
return True return True
@@ -0,0 +1,120 @@
"""A folded encoder must run SRT collectives on its bound TP group."""
from __future__ import annotations
import os
import subprocess
import sys
import unittest
import torch
import torch.nn.functional as F
from torch import nn
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.test.test_utils import CustomTestCase
_WORLD_SIZE = 2
def _worker() -> int:
from sglang.multimodal_gen.runtime.distributed import (
cleanup_dist_env_and_memory,
get_tp_group,
get_world_group,
init_distributed_environment,
initialize_model_parallel,
)
from sglang.multimodal_gen.runtime.models.encoders.base import (
EncoderTensorParallelMixin,
)
from sglang.srt.distributed import parallel_state as srt_parallel_state
from sglang.srt.layers.linear import RowParallelLinear
rank = int(os.environ["RANK"])
world_size = int(os.environ["WORLD_SIZE"])
device = torch.device(f"cuda:{rank}")
torch.cuda.set_device(device)
init_distributed_environment(
world_size=world_size,
rank=rank,
local_rank=rank,
)
initialize_model_parallel(
tensor_parallel_degree=1,
sequence_parallel_degree=world_size,
ulysses_degree=world_size,
ring_degree=1,
)
class FoldedEncoder(EncoderTensorParallelMixin, nn.Module):
def __init__(self):
super().__init__()
self.bind_encoder_tp_group(get_world_group())
self.proj = RowParallelLinear(
input_size=8,
output_size=6,
bias=False,
tp_rank=rank,
tp_size=world_size,
params_dtype=torch.float32,
)
def forward(self, inputs: torch.Tensor) -> torch.Tensor:
local_inputs = inputs.chunk(world_size, dim=-1)[rank].contiguous()
output, _ = self.proj(local_inputs)
return output
full_weight = torch.arange(48, dtype=torch.float32, device=device).reshape(6, 8)
full_weight = (full_weight - 23.5) / 32
inputs = torch.arange(24, dtype=torch.float32, device=device).reshape(3, 8) / 8
model = FoldedEncoder().to(device).eval()
with torch.no_grad():
model.proj.weight.copy_(full_weight[:, rank * 4 : (rank + 1) * 4].contiguous())
expected = F.linear(inputs, full_weight)
actual = model(inputs)
torch.testing.assert_close(actual, expected, rtol=1e-6, atol=1e-6)
assert get_tp_group().world_size == 1
assert srt_parallel_state.get_tp_group().world_size == 1
assert srt_parallel_state.get_attn_tp_group().world_size == 1
if rank == 0:
print("ENCODER_FOLD_SRT_LINEAR_PARITY PASS", flush=True)
torch.distributed.barrier()
cleanup_dist_env_and_memory()
return 0
class TestEncoderFoldSrtLinearTwoGpu(CustomTestCase):
def test_folded_srt_linear_matches_unsharded_reference(self):
if not current_platform.is_cuda():
self.skipTest("CUDA-only test")
if torch.cuda.device_count() < _WORLD_SIZE:
self.skipTest(f"needs {_WORLD_SIZE} GPUs")
proc = subprocess.run(
[
sys.executable,
"-m",
"torch.distributed.run",
f"--nproc-per-node={_WORLD_SIZE}",
"--master-port=29618",
__file__,
"--worker",
],
capture_output=True,
text=True,
timeout=600,
)
print(proc.stdout[-4000:])
if proc.returncode != 0:
print(proc.stderr[-4000:], file=sys.stderr)
self.assertEqual(proc.returncode, 0, "folded SRT linear output diverged")
self.assertIn("ENCODER_FOLD_SRT_LINEAR_PARITY PASS", proc.stdout)
if __name__ == "__main__":
if "--worker" in sys.argv:
raise SystemExit(_worker())
unittest.main()
@@ -0,0 +1,59 @@
import torch
from torch import nn
from sglang.multimodal_gen.test.single_test_file.component_accuracy.engine import (
AccuracyEngine,
)
class _SourceProjectionSet(nn.Module):
def __init__(self) -> None:
super().__init__()
self.qkv = nn.Linear(2, 6, bias=False)
self.gate_proj = nn.Linear(2, 3, bias=False)
self.up_proj = nn.Linear(2, 3, bias=False)
self.down_proj = nn.Linear(3, 2, bias=False)
class _TargetProjectionSet(nn.Module):
def __init__(self) -> None:
super().__init__()
self.qkv_proj = nn.Linear(2, 6, bias=False)
self.gate_up_proj = nn.Linear(2, 8, bias=False)
self.down_proj = nn.Linear(4, 2, bias=False)
self.qkv_proj.weight.weight_loader = self._load_qkv
self.gate_up_proj.weight.weight_loader = self._load_gate_up
self.down_proj.weight.weight_loader = self._load_down
@staticmethod
def _load_qkv(param: nn.Parameter, source: torch.Tensor) -> None:
param.data.copy_(source)
@staticmethod
def _load_gate_up(param: nn.Parameter, source: torch.Tensor, shard_id: int) -> None:
offset = shard_id * 4
param.data[offset : offset + source.shape[0]].copy_(source)
@staticmethod
def _load_down(param: nn.Parameter, source: torch.Tensor) -> None:
param.data[:, : source.shape[1]].copy_(source)
def test_transfer_weights_uses_loaders_for_fused_aliases_and_padding() -> None:
source = _SourceProjectionSet().to(dtype=torch.bfloat16)
target = _TargetProjectionSet()
with torch.no_grad():
for index, parameter in enumerate(source.parameters(), start=1):
parameter.fill_(index)
for parameter in target.parameters():
parameter.zero_()
AccuracyEngine.transfer_weights(source, target, target_device=torch.device("cpu"))
torch.testing.assert_close(target.qkv_proj.weight, source.qkv.weight)
torch.testing.assert_close(target.gate_up_proj.weight[:3], source.gate_proj.weight)
torch.testing.assert_close(target.gate_up_proj.weight[4:7], source.up_proj.weight)
assert torch.count_nonzero(target.gate_up_proj.weight[[3, 7]]) == 0
torch.testing.assert_close(target.down_proj.weight[:, :3], source.down_proj.weight)
assert torch.count_nonzero(target.down_proj.weight[:, 3]) == 0
@@ -15,6 +15,8 @@ from sglang.multimodal_gen.runtime.models.encoders.qwen2_5vl import (
Qwen2_5_VLAttention, Qwen2_5_VLAttention,
Qwen2_5_VLForConditionalGeneration, Qwen2_5_VLForConditionalGeneration,
_apply_repetition_penalty, _apply_repetition_penalty,
_make_column_linear,
_make_row_linear,
_select_next_token, _select_next_token,
) )
from sglang.multimodal_gen.runtime.models.encoders.qwen2_5vl_vision import ( from sglang.multimodal_gen.runtime.models.encoders.qwen2_5vl_vision import (
@@ -24,6 +26,17 @@ from sglang.multimodal_gen.runtime.models.encoders.qwen2_5vl_vision import (
_vision_window_index, _vision_window_index,
) )
from sglang.multimodal_gen.runtime.pipelines.longcat_image import LongCatImagePipeline from sglang.multimodal_gen.runtime.pipelines.longcat_image import LongCatImagePipeline
from sglang.srt.layers.linear import (
ColumnParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from sglang.srt.models.qwen2_5_vl import (
Qwen2_5_VisionPatchEmbed,
Qwen2_5_VisionPatchMerger,
Qwen2_5_VLMLP,
)
from sglang.srt.runtime_context import get_parallel
class _StubQwen2_5VL(Qwen2_5_VLForConditionalGeneration): class _StubQwen2_5VL(Qwen2_5_VLForConditionalGeneration):
@@ -66,6 +79,75 @@ class _AttentionRecorder(nn.Module):
return query return query
def test_native_vision_reuses_srt_modules():
config = SimpleNamespace(
hidden_size=16,
intermediate_size=24,
hidden_act="silu",
num_heads=2,
depth=0,
patch_size=2,
temporal_patch_size=1,
in_channels=3,
spatial_merge_size=2,
out_hidden_size=12,
fullatt_block_indexes=[],
window_size=8,
)
with get_parallel().override(tp_size=1, tp_rank=0):
model = Qwen2_5VLVisionTransformer(config)
mlp = Qwen2_5_VLMLP(
16,
24,
fuse_gate_up=False,
)
fused_mlp = Qwen2_5_VLMLP(16, 24)
assert isinstance(model.patch_embed, Qwen2_5_VisionPatchEmbed)
assert isinstance(model.merger, Qwen2_5_VisionPatchMerger)
assert not mlp.fuse_gate_up
assert isinstance(mlp.gate_proj, ColumnParallelLinear)
assert isinstance(mlp.up_proj, ColumnParallelLinear)
assert mlp.gate_proj.tp_size == mlp.up_proj.tp_size == 1
assert isinstance(mlp.down_proj, ReplicatedLinear)
assert isinstance(fused_mlp.down_proj, RowParallelLinear)
assert mlp.act is not None
assert isinstance(
_make_column_linear(16, 24, bias=False, use_tensor_parallel=False),
ReplicatedLinear,
)
assert isinstance(
_make_row_linear(24, 16, bias=False, use_tensor_parallel=False),
ReplicatedLinear,
)
def test_text_mlp_uses_single_rank_when_intermediate_size_is_not_tp_divisible(
monkeypatch,
):
monkeypatch.setattr(qwen2_5vl, "Qwen2_5_VLAttention", lambda *_args: nn.Identity())
monkeypatch.setattr(qwen2_5vl, "_tp_world_size", lambda: 3)
monkeypatch.setattr(qwen2_5vl, "_tp_rank", lambda: 2)
config = SimpleNamespace(
hidden_size=16,
intermediate_size=25,
hidden_act="silu",
rms_norm_eps=1e-6,
use_sliding_window=False,
_attn_implementation="flash_attention_2",
layer_types=["full_attention"],
)
layer = qwen2_5vl.Qwen2_5_VLDecoderLayer(config, layer_idx=0)
assert layer.mlp.tp_size == 1
assert layer.mlp.tp_rank == 0
assert isinstance(layer.mlp.gate_proj, ColumnParallelLinear)
assert isinstance(layer.mlp.up_proj, ColumnParallelLinear)
assert layer.mlp.gate_proj.tp_rank == layer.mlp.up_proj.tp_rank == 0
assert isinstance(layer.mlp.down_proj, ReplicatedLinear)
def test_explicit_attention_mask_is_limited_to_cached_generation(monkeypatch): def test_explicit_attention_mask_is_limited_to_cached_generation(monkeypatch):
attention = Qwen2_5_VLAttention.__new__(Qwen2_5_VLAttention) attention = Qwen2_5_VLAttention.__new__(Qwen2_5_VLAttention)
nn.Module.__init__(attention) nn.Module.__init__(attention)
@@ -76,12 +158,12 @@ def test_explicit_attention_mask_is_limited_to_cached_generation(monkeypatch):
attention.num_heads = 1 attention.num_heads = 1
attention.num_key_value_heads = 1 attention.num_key_value_heads = 1
attention.head_dim = 4 attention.head_dim = 4
attention.rope_scaling = {"mrope_section": [1, 1, 0]} attention.rotary_emb = object()
attention.attn = _AttentionRecorder() attention.attn = _AttentionRecorder()
monkeypatch.setattr( monkeypatch.setattr(
qwen2_5vl, qwen2_5vl,
"apply_multimodal_rotary_pos_emb", "apply_qwen_vl_text_rope",
lambda query, key, *_args: (query, key), lambda _rotary_emb, _position_ids, query, key: (query, key),
) )
hidden_states = torch.randn(1, 2, 4) hidden_states = torch.randn(1, 2, 4)
@@ -89,7 +171,7 @@ def test_explicit_attention_mask_is_limited_to_cached_generation(monkeypatch):
kwargs = { kwargs = {
"hidden_states": hidden_states, "hidden_states": hidden_states,
"attention_mask": explicit_mask, "attention_mask": explicit_mask,
"position_embeddings": (torch.empty(0), torch.empty(0)), "position_ids": torch.zeros(3, 1, 2, dtype=torch.long),
} }
attention(**kwargs, use_cache=False) attention(**kwargs, use_cache=False)
@@ -2,7 +2,10 @@ from types import SimpleNamespace
import torch import torch
import sglang.multimodal_gen.runtime.models.encoders.qwen3 as qwen3
import sglang.srt.layers.activation as srt_activation
from sglang.multimodal_gen.runtime.models.encoders.qwen3 import Qwen3ForCausalLM from sglang.multimodal_gen.runtime.models.encoders.qwen3 import Qwen3ForCausalLM
from sglang.srt.layers.activation import SiluAndMul
class _CaptureLayer(torch.nn.Module): class _CaptureLayer(torch.nn.Module):
@@ -26,6 +29,53 @@ class _IdentityNorm(torch.nn.Module):
return hidden_states, None return hidden_states, None
def test_mlp_reuses_srt_activation_without_server_context(monkeypatch):
def fail_get_exec():
raise AssertionError("SiluAndMul must not read an unpublished context")
monkeypatch.setattr(srt_activation, "publish_role", lambda: None)
monkeypatch.setattr(srt_activation, "get_exec", fail_get_exec)
def make_linear(*_args, **_kwargs):
return torch.nn.Identity()
monkeypatch.setattr(qwen3, "MergedColumnParallelLinear", make_linear)
monkeypatch.setattr(qwen3, "RowParallelLinear", make_linear)
mlp = qwen3.Qwen3MLP(16, 24, "silu")
assert isinstance(mlp.act_fn, SiluAndMul)
def test_attention_keeps_diffusion_one_pass_qk_norm(monkeypatch):
monkeypatch.setattr(qwen3, "get_tp_world_size", lambda: 1)
monkeypatch.setattr(
qwen3, "QKVParallelLinear", lambda **kwargs: torch.nn.Identity()
)
monkeypatch.setattr(
qwen3, "RowParallelLinear", lambda **kwargs: torch.nn.Identity()
)
monkeypatch.setattr(qwen3, "get_rope", lambda *args, **kwargs: torch.nn.Identity())
monkeypatch.setattr(
qwen3, "LocalAttention", lambda *args, **kwargs: torch.nn.Identity()
)
config = SimpleNamespace(
head_dim=128,
rms_norm_eps=1e-6,
_supported_attention_backends=(),
)
attention = qwen3.Qwen3Attention(
config,
hidden_size=256,
num_heads=2,
num_kv_heads=1,
)
assert isinstance(attention.q_norm, qwen3.MMGenRMSNorm)
assert isinstance(attention.k_norm, qwen3.MMGenRMSNorm)
def test_default_position_ids_batch_shape(): def test_default_position_ids_batch_shape():
model = Qwen3ForCausalLM.__new__(Qwen3ForCausalLM) model = Qwen3ForCausalLM.__new__(Qwen3ForCausalLM)
torch.nn.Module.__init__(model) torch.nn.Module.__init__(model)
@@ -0,0 +1,78 @@
from types import SimpleNamespace
import torch
from torch import nn
import sglang.multimodal_gen.runtime.models.encoders.qwen3vl as qwen3vl
class _IdentityAttention(nn.Module):
def forward(self, query, key, value):
return query
def test_qwen3vl_attention_uses_interleaved_mrope(monkeypatch):
captured_kwargs = {}
def build_rope(_config, **kwargs):
captured_kwargs.update(kwargs)
return object()
monkeypatch.setattr(qwen3vl, "build_qwen_vl_text_rope", build_rope)
monkeypatch.setattr(
qwen3vl, "_make_text_linear", lambda *args, **kwargs: nn.Identity()
)
monkeypatch.setattr(
qwen3vl, "_make_text_row_linear", lambda *args, **kwargs: nn.Identity()
)
monkeypatch.setattr(
qwen3vl, "_make_text_rms_norm", lambda *args, **kwargs: nn.Identity()
)
monkeypatch.setattr(qwen3vl, "LocalAttention", lambda **kwargs: nn.Identity())
config = SimpleNamespace(
head_dim=8,
hidden_size=8,
num_attention_heads=1,
num_key_value_heads=1,
attention_dropout=0.0,
attention_bias=False,
rms_norm_eps=1e-6,
)
qwen3vl.Qwen3VLTextAttention(config, layer_idx=0)
assert captured_kwargs == {"mrope_interleaved": True}
def test_qwen3vl_attention_passes_three_axis_positions_to_srt_rope(monkeypatch):
attention = qwen3vl.Qwen3VLTextAttention.__new__(qwen3vl.Qwen3VLTextAttention)
nn.Module.__init__(attention)
attention.q_proj = nn.Identity()
attention.k_proj = nn.Identity()
attention.v_proj = nn.Identity()
attention.o_proj = nn.Identity()
attention.q_norm = nn.Identity()
attention.k_norm = nn.Identity()
attention.head_dim = 4
attention.rotary_emb = object()
attention.attn = _IdentityAttention()
captured_position_ids = None
def apply_rope(_rotary_emb, position_ids, query, key):
nonlocal captured_position_ids
captured_position_ids = position_ids
return query, key
monkeypatch.setattr(qwen3vl, "apply_qwen_vl_text_rope", apply_rope)
hidden_states = torch.randn(1, 2, 4)
position_ids = torch.arange(6).view(3, 1, 2)
output = attention(
hidden_states,
position_ids=position_ids,
attention_mask=None,
)
assert captured_position_ids is position_ids
torch.testing.assert_close(output, hidden_states)
@@ -9,6 +9,7 @@ from sglang.multimodal_gen.runtime.models.encoders.minimax_h3_qwen3vl import (
) )
from sglang.multimodal_gen.runtime.models.encoders.qwen3vl import ( from sglang.multimodal_gen.runtime.models.encoders.qwen3vl import (
Qwen3VLForConditionalGeneration, Qwen3VLForConditionalGeneration,
_make_text_rms_norm,
) )
from sglang.multimodal_gen.runtime.models.encoders.qwen3vl_vision import ( from sglang.multimodal_gen.runtime.models.encoders.qwen3vl_vision import (
Qwen3VLVisionRotaryEmbedding, Qwen3VLVisionRotaryEmbedding,
@@ -16,6 +17,12 @@ from sglang.multimodal_gen.runtime.models.encoders.qwen3vl_vision import (
_vision_cu_seqlens, _vision_cu_seqlens,
_vision_position_ids, _vision_position_ids,
) )
from sglang.srt.layers.layernorm import RMSNorm
from sglang.srt.models.qwen3_vl import (
Qwen3VLMoeVisionPatchMerger,
Qwen3VLVisionPatchEmbed,
)
from sglang.srt.runtime_context import get_parallel
def test_native_vision_layout_matches_qwen3_merge_order(): def test_native_vision_layout_matches_qwen3_merge_order():
@@ -38,6 +45,13 @@ def test_native_vision_layout_matches_qwen3_merge_order():
assert cu_seqlens.tolist() == [0, 24, 32, 40] assert cu_seqlens.tolist() == [0, 24, 32, 40]
def test_qwen3vl_text_reuses_srt_rms_norm():
norm = _make_text_rms_norm(16, 1e-6)
assert isinstance(norm, RMSNorm)
assert norm.cast_x_before_out_mul
def test_native_vision_keeps_checkpoint_parameter_names(): def test_native_vision_keeps_checkpoint_parameter_names():
config = SimpleNamespace( config = SimpleNamespace(
hidden_size=16, hidden_size=16,
@@ -53,8 +67,12 @@ def test_native_vision_keeps_checkpoint_parameter_names():
out_hidden_size=12, out_hidden_size=12,
deepstack_visual_indexes=[], deepstack_visual_indexes=[],
) )
with get_parallel().override(tp_size=1, tp_rank=0):
model = Qwen3VLVisionTransformer(config) model = Qwen3VLVisionTransformer(config)
assert isinstance(model.patch_embed, Qwen3VLVisionPatchEmbed)
assert isinstance(model.merger, Qwen3VLMoeVisionPatchMerger)
assert set(model.state_dict()) == { assert set(model.state_dict()) == {
"patch_embed.proj.weight", "patch_embed.proj.weight",
"patch_embed.proj.bias", "patch_embed.proj.bias",
@@ -0,0 +1,150 @@
from types import SimpleNamespace
import pytest
import torch
from torch import nn
import sglang.multimodal_gen.runtime.models.encoders.qwen_vl_rope as qwen_vl_rope
import sglang.srt.layers.rotary_embedding.base as rope_base
import sglang.srt.layers.rotary_embedding.factory as rope_factory
from sglang.multimodal_gen.runtime.models.encoders.qwen_vl_rope import (
apply_qwen_vl_text_rope,
build_qwen_vl_text_rope,
)
class _RecordingRotaryEmbedding(nn.Module):
def __init__(self):
super().__init__()
self.positions = None
self.query_shape = None
self.key_shape = None
def forward_native(self, positions, query, key):
self.positions = positions
self.query_shape = query.shape
self.key_shape = key.shape
return query + 1, key + 2
def test_qwen_vl_rope_supports_transformers_v5_config(monkeypatch):
rope_parameters = {
"rope_type": "default",
"rope_theta": 1_000_000.0,
"mrope_section": [2, 1, 1],
}
config = SimpleNamespace(
head_dim=None,
hidden_size=32,
num_attention_heads=4,
max_position_embeddings=128,
rope_parameters=rope_parameters,
)
captured_kwargs = {}
rotary_emb = object()
def get_rope(**kwargs):
captured_kwargs.update(kwargs)
return rotary_emb
monkeypatch.setattr(qwen_vl_rope, "get_rope", get_rope)
assert build_qwen_vl_text_rope(config) is rotary_emb
assert captured_kwargs == {
"head_size": 8,
"rotary_dim": 8,
"max_position": 128,
"base": 1_000_000.0,
"is_neox_style": True,
"rope_scaling": {**rope_parameters, "mrope_interleaved": False},
}
def test_qwen_vl_rope_enables_interleaved_layout_explicitly(monkeypatch):
config = SimpleNamespace(
head_dim=8,
max_position_embeddings=128,
rope_parameters={
"rope_type": "default",
"rope_theta": 1_000_000.0,
"mrope_section": [2, 1, 1],
},
)
captured_kwargs = {}
def get_rope(**kwargs):
captured_kwargs.update(kwargs)
return object()
monkeypatch.setattr(qwen_vl_rope, "get_rope", get_rope)
build_qwen_vl_text_rope(config, mrope_interleaved=True)
assert captured_kwargs["rope_scaling"] == {
**config.rope_parameters,
"mrope_interleaved": True,
}
def test_qwen_vl_rope_does_not_require_srt_runtime_context(monkeypatch):
def fail_get_exec():
raise AssertionError("Qwen-VL RoPE must not read an unpublished context")
monkeypatch.setattr(rope_base, "get_exec", fail_get_exec)
monkeypatch.setattr(rope_base, "publish_role", lambda: None)
monkeypatch.setattr(rope_factory, "_ROPE_DICT", {})
config = SimpleNamespace(
head_dim=None,
hidden_size=32,
num_attention_heads=4,
max_position_embeddings=37,
rope_parameters={
"rope_type": "default",
"rope_theta": 123_457.0,
"mrope_section": [2, 1, 1],
},
)
rotary_emb = build_qwen_vl_text_rope(config)
positions = torch.arange(9).view(3, 3)
query = torch.randn(3, 16)
key = torch.randn(3, 8)
rotated_query, rotated_key = rotary_emb.forward_native(positions, query, key)
assert rotated_query.shape == query.shape
assert rotated_key.shape == key.shape
def test_qwen_vl_rope_adapts_batched_gqa_layout():
rotary_emb = _RecordingRotaryEmbedding()
query = torch.randn(2, 4, 5, 8)
key = torch.randn(2, 2, 5, 8)
position_ids = torch.arange(30).view(3, 2, 5)
rotated_query, rotated_key = apply_qwen_vl_text_rope(
rotary_emb, position_ids, query, key
)
assert rotary_emb.query_shape == (10, 32)
assert rotary_emb.key_shape == (10, 16)
assert torch.equal(rotary_emb.positions, position_ids.reshape(3, -1))
torch.testing.assert_close(rotated_query, query + 1)
torch.testing.assert_close(rotated_key, key + 2)
@pytest.mark.parametrize(
("position_ids", "key"),
[
(torch.zeros(2, 1, 3, dtype=torch.long), torch.zeros(1, 1, 3, 4)),
(torch.zeros(3, 1, 2, dtype=torch.long), torch.zeros(1, 1, 3, 4)),
],
)
def test_qwen_vl_rope_rejects_incompatible_shapes(position_ids, key):
with pytest.raises(ValueError):
apply_qwen_vl_text_rope(
_RecordingRotaryEmbedding(),
position_ids,
torch.zeros(1, 1, 3, 4),
key,
)
@@ -108,6 +108,26 @@ class TestMiniMaxH3CheckpointFilter(unittest.TestCase):
expected, expected,
) )
def test_vision_qkv_checkpoint_name_maps_to_native_projection(self):
encoder = MiniMaxH3Qwen3VLEncoder.__new__(MiniMaxH3Qwen3VLEncoder)
torch.nn.Module.__init__(encoder)
encoder.model = torch.nn.Module()
encoder.model.visual = torch.nn.Module()
block = torch.nn.Module()
block.attn = torch.nn.Module()
block.attn.qkv_proj = torch.nn.Linear(2, 2)
encoder.model.visual.blocks = torch.nn.ModuleList([block])
loaded = encoder.load_weights(
[("model.visual.blocks.0.attn.qkv.bias", torch.tensor([1.0, 2.0]))]
)
self.assertEqual(loaded, {"model.visual.blocks.0.attn.qkv_proj.bias"})
torch.testing.assert_close(
encoder.model.visual.blocks[0].attn.qkv_proj.bias,
torch.tensor([1.0, 2.0]),
)
class TestTextEncoderQuantization(unittest.TestCase): class TestTextEncoderQuantization(unittest.TestCase):
def setUp(self): def setUp(self):
+5 -2
View File
@@ -33,7 +33,7 @@ from sglang.srt.model_executor.cuda_graph_config import (
Phase, Phase,
check_cuda_graph_backend, check_cuda_graph_backend,
) )
from sglang.srt.runtime_context import get_exec, get_parallel from sglang.srt.runtime_context import get_exec, get_parallel, publish_role
from sglang.srt.utils import ( from sglang.srt.utils import (
cpu_has_amx_support, cpu_has_amx_support,
get_bool_env_var, get_bool_env_var,
@@ -130,7 +130,10 @@ logger = logging.getLogger(__name__)
class SiluAndMul(BaseFusedOp): class SiluAndMul(BaseFusedOp):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if get_exec().deterministic.rl_on_policy_target is not None: if (
publish_role() is not None
and get_exec().deterministic.rl_on_policy_target is not None
):
self._forward_method = self.forward_native self._forward_method = self.forward_native
elif _use_aiter and envs.SGLANG_OPT_USE_AITER_SILU_MUL.get(): elif _use_aiter and envs.SGLANG_OPT_USE_AITER_SILU_MUL.get():
self._forward_method = self.forward_aiter self._forward_method = self.forward_aiter
+73 -25
View File
@@ -432,6 +432,7 @@ class RMSNorm(BaseFusedOp):
weight_dtype: Optional = None, weight_dtype: Optional = None,
override_orig_dtype: Optional = None, override_orig_dtype: Optional = None,
x_pad_to_multiple: int = 0, x_pad_to_multiple: int = 0,
force_native: bool = False,
) -> None: ) -> None:
super().__init__() super().__init__()
self.has_weight = has_weight self.has_weight = has_weight
@@ -467,6 +468,8 @@ class RMSNorm(BaseFusedOp):
except ImportError: except ImportError:
self._fused_pad_kernel = None self._fused_pad_kernel = None
self._forward_method = self.forward_aiter self._forward_method = self.forward_aiter
if force_native:
self._forward_method = self.forward_native
def forward_cuda( def forward_cuda(
self, self,
@@ -481,11 +484,6 @@ class RMSNorm(BaseFusedOp):
residual = residual + post_residual_addition residual = residual + post_residual_addition
return x, residual return x, residual
return x return x
# sgl_kernel rmsnorm requires 2D input; reshape higher-rank tensors
needs_reshape = x.dim() != 2 and residual is None
if needs_reshape:
original_shape = x.shape
x = x.contiguous().reshape(-1, original_shape[-1])
if self.variance_size_override is not None: if self.variance_size_override is not None:
return self.forward_native(x, residual, post_residual_addition) return self.forward_native(x, residual, post_residual_addition)
if is_batch_invariant_mode_enabled(): if is_batch_invariant_mode_enabled():
@@ -495,6 +493,10 @@ class RMSNorm(BaseFusedOp):
or get_exec().deterministic.rl_on_policy_target == "fsdp" or get_exec().deterministic.rl_on_policy_target == "fsdp"
): ):
return self.forward_native(x, residual, post_residual_addition) return self.forward_native(x, residual, post_residual_addition)
original_shape = x.shape
needs_reshape = x.dim() != 2
if needs_reshape:
x = x.contiguous().reshape(-1, original_shape[-1])
out = rms_norm_batch_invariant( out = rms_norm_batch_invariant(
x, x,
self.weight.data, self.weight.data,
@@ -517,6 +519,21 @@ class RMSNorm(BaseFusedOp):
return self.forward_with_per_tensor_quant_fusion( return self.forward_with_per_tensor_quant_fusion(
x, scale, residual, post_residual_addition x, scale, residual, post_residual_addition
) )
# CUDA RMSNorm kernels require 2D inputs. Flatten token dimensions for
# the kernel call and restore each returned tensor to its input shape.
original_shape = x.shape
residual_shape = residual.shape if residual is not None else original_shape
needs_reshape = x.dim() != 2
if needs_reshape:
x = x.contiguous().reshape(-1, original_shape[-1])
if residual is not None:
residual = residual.contiguous().reshape(-1, residual_shape[-1])
if post_residual_addition is not None:
post_residual_addition = post_residual_addition.contiguous().reshape(
-1, post_residual_addition.shape[-1]
)
if self.cast_x_before_out_mul and residual is None: if self.cast_x_before_out_mul and residual is None:
# Use HF-semantics kernel (cast to dtype before weight multiply). # Use HF-semantics kernel (cast to dtype before weight multiply).
if ( if (
@@ -531,10 +548,8 @@ class RMSNorm(BaseFusedOp):
else: else:
# Fallback: pure-Python HF semantics (already implemented in forward_native). # Fallback: pure-Python HF semantics (already implemented in forward_native).
out = self.forward_native(x, None, None) out = self.forward_native(x, None, None)
if needs_reshape: result = out
out = out.reshape(original_shape) elif residual is not None:
return out
if residual is not None:
if self.cast_x_before_out_mul: if self.cast_x_before_out_mul:
if ( if (
x.dtype in (torch.float16, torch.bfloat16) x.dtype in (torch.float16, torch.bfloat16)
@@ -554,8 +569,10 @@ class RMSNorm(BaseFusedOp):
self.variance_epsilon, self.variance_epsilon,
cast_x_before_out_mul=self.cast_x_before_out_mul, cast_x_before_out_mul=self.cast_x_before_out_mul,
) )
return x, residual result = x, residual
return self.forward_native(x, residual, post_residual_addition) else:
result = self.forward_native(x, residual, post_residual_addition)
else:
# TODO: Ideally we want to have (hidden_states+residual)+post_residual_addition. # TODO: Ideally we want to have (hidden_states+residual)+post_residual_addition.
# but right now we can only have hidden_states+(residual+post_residual_addition). # but right now we can only have hidden_states+(residual+post_residual_addition).
# (hidden_states+residual)+post_residual_addition != hidden_states+(residual+post_residual_addition), # (hidden_states+residual)+post_residual_addition != hidden_states+(residual+post_residual_addition),
@@ -563,11 +580,17 @@ class RMSNorm(BaseFusedOp):
if post_residual_addition is not None: if post_residual_addition is not None:
residual = residual + post_residual_addition residual = residual + post_residual_addition
fused_add_rmsnorm(x, residual, self.weight.data, self.variance_epsilon) fused_add_rmsnorm(x, residual, self.weight.data, self.variance_epsilon)
return x, residual result = x, residual
out = rmsnorm(x, self.weight.data, self.variance_epsilon) else:
result = rmsnorm(x, self.weight.data, self.variance_epsilon)
if needs_reshape: if needs_reshape:
out = out.reshape(original_shape) if residual is not None:
return out return result[0].reshape(original_shape), result[1].reshape(
residual_shape
)
return result.reshape(original_shape)
return result
def forward_npu( def forward_npu(
self, self,
@@ -602,15 +625,6 @@ class RMSNorm(BaseFusedOp):
# AITER's ROCm rmsnorm2d_fwd requires weight/activation dtypes to match; # AITER's ROCm rmsnorm2d_fwd requires weight/activation dtypes to match;
# FP32 weight + BF16 activation yields finite-but-corrupted output on gfx950. # FP32 weight + BF16 activation yields finite-but-corrupted output on gfx950.
return self.forward_native(x, residual, post_residual_addition) return self.forward_native(x, residual, post_residual_addition)
# Aiter's RMSNorm kernels expect 2D contiguous inputs. Keep the
# already-safe layout as a zero-copy path, and only normalize strided or
# higher-rank views such as Q/K slices from packed QKV projections.
needs_reshape = x.dim() != 2 and residual is None
if needs_reshape:
original_shape = x.shape
x = x.contiguous().reshape(-1, original_shape[-1])
elif not x.is_contiguous():
x = x.contiguous()
if is_batch_invariant_mode_enabled(): if is_batch_invariant_mode_enabled():
if ( if (
residual is not None residual is not None
@@ -619,6 +633,10 @@ class RMSNorm(BaseFusedOp):
or (self._fused_pad_kernel is not None and self.x_pad_to_multiple > 0) or (self._fused_pad_kernel is not None and self.x_pad_to_multiple > 0)
): ):
return self.forward_native(x, residual, post_residual_addition) return self.forward_native(x, residual, post_residual_addition)
original_shape = x.shape
needs_reshape = x.dim() != 2
if needs_reshape:
x = x.contiguous().reshape(-1, original_shape[-1])
out = rms_norm_batch_invariant( out = rms_norm_batch_invariant(
x, x,
self.weight.data, self.weight.data,
@@ -627,6 +645,25 @@ class RMSNorm(BaseFusedOp):
if needs_reshape: if needs_reshape:
out = out.reshape(original_shape) out = out.reshape(original_shape)
return out return out
# AITER's RMSNorm kernels require 2D contiguous inputs.
original_shape = x.shape
residual_shape = residual.shape if residual is not None else original_shape
needs_reshape = x.dim() != 2
if needs_reshape:
x = x.contiguous().reshape(-1, original_shape[-1])
if residual is not None:
residual = residual.contiguous().reshape(-1, residual_shape[-1])
if post_residual_addition is not None:
post_residual_addition = post_residual_addition.contiguous().reshape(
-1, post_residual_addition.shape[-1]
)
else:
if not x.is_contiguous():
x = x.contiguous()
if residual is not None and not residual.is_contiguous():
residual = residual.contiguous()
# Fused (add +) rmsnorm + zero-pad path. Triggered when caller # Fused (add +) rmsnorm + zero-pad path. Triggered when caller
# constructed RMSNorm with x_pad_to_multiple > 0. Output last # constructed RMSNorm with x_pad_to_multiple > 0. Output last
# dim is padded up; residual_out stays at original width. Used # dim is padded up; residual_out stays at original width. Used
@@ -636,13 +673,20 @@ class RMSNorm(BaseFusedOp):
if self._fused_pad_kernel is not None and self.x_pad_to_multiple > 0: if self._fused_pad_kernel is not None and self.x_pad_to_multiple > 0:
if post_residual_addition is not None and residual is not None: if post_residual_addition is not None and residual is not None:
residual = residual + post_residual_addition residual = residual + post_residual_addition
return self._fused_pad_kernel( result = self._fused_pad_kernel(
x, x,
self.weight.data, self.weight.data,
self.variance_epsilon, self.variance_epsilon,
residual, residual,
self.x_pad_to_multiple, self.x_pad_to_multiple,
) )
if needs_reshape and residual is not None:
output, residual_out = result
output_shape = (*original_shape[:-1], output.shape[-1])
return output.reshape(output_shape), residual_out.reshape(
residual_shape
)
return result
if residual is not None: if residual is not None:
residual_out = torch.empty_like(x) residual_out = torch.empty_like(x)
output = torch.empty_like(x) output = torch.empty_like(x)
@@ -656,6 +700,10 @@ class RMSNorm(BaseFusedOp):
self.weight.data, self.weight.data,
self.variance_epsilon, self.variance_epsilon,
) )
if needs_reshape:
return output.reshape(original_shape), residual_out.reshape(
residual_shape
)
return output, residual_out return output, residual_out
output = rms_norm(x, self.weight.data, self.variance_epsilon) output = rms_norm(x, self.weight.data, self.variance_epsilon)
if needs_reshape: if needs_reshape:
@@ -11,7 +11,7 @@ from sglang.kernels.fused_op import BaseFusedOp
from sglang.srt.environ import envs from sglang.srt.environ import envs
from sglang.srt.layers.rotary_embedding.utils import apply_rotary_emb from sglang.srt.layers.rotary_embedding.utils import apply_rotary_emb
from sglang.srt.platforms import current_platform from sglang.srt.platforms import current_platform
from sglang.srt.runtime_context import get_exec from sglang.srt.runtime_context import get_exec, publish_role
from sglang.srt.utils import ( from sglang.srt.utils import (
cpu_has_amx_support, cpu_has_amx_support,
get_bool_env_var, get_bool_env_var,
@@ -94,6 +94,10 @@ class RotaryEmbedding(BaseFusedOp):
self.base = base self.base = base
self.is_neox_style = is_neox_style self.is_neox_style = is_neox_style
self.dtype = dtype self.dtype = dtype
self._force_native = (
publish_role() is not None
and get_exec().deterministic.rl_on_policy_target is not None
)
cache = self._compute_cos_sin_cache() cache = self._compute_cos_sin_cache()
# NOTE(ByronHsu): cache needs to be in FP32 for numerical stability. # NOTE(ByronHsu): cache needs to be in FP32 for numerical stability.
@@ -129,7 +133,7 @@ class RotaryEmbedding(BaseFusedOp):
self._apply_rotary_emb_wrapped = apply_rotary_emb self._apply_rotary_emb_wrapped = apply_rotary_emb
# XXX (MUSA): Implement sgl_kernel.rotary_embedding support for MUSA backend # XXX (MUSA): Implement sgl_kernel.rotary_embedding support for MUSA backend
if get_exec().deterministic.rl_on_policy_target is not None or _is_musa: if self._force_native or _is_musa:
self._forward_method = self.forward_native self._forward_method = self.forward_native
self._apply_rotary_emb_wrapped = torch.compile( self._apply_rotary_emb_wrapped = torch.compile(
dynamic=True, dynamic=True,
@@ -152,9 +156,7 @@ class RotaryEmbedding(BaseFusedOp):
# use CPU to compute the cache and then move it to GPU. However, we # use CPU to compute the cache and then move it to GPU. However, we
# create the cache on GPU for faster initialization. This may cause # create the cache on GPU for faster initialization. This may cause
# a slight numerical difference between the HF implementation and ours. # a slight numerical difference between the HF implementation and ours.
init_device = ( init_device = "cpu" if self._force_native else None
"cpu" if get_exec().deterministic.rl_on_policy_target is not None else None
)
inv_freq = 1.0 / ( inv_freq = 1.0 / (
base base
** ( ** (
@@ -164,7 +166,7 @@ class RotaryEmbedding(BaseFusedOp):
/ self.rotary_dim / self.rotary_dim
) )
) )
if get_exec().deterministic.rl_on_policy_target is not None: if self._force_native:
inv_freq = inv_freq.cuda() inv_freq = inv_freq.cuda()
return inv_freq return inv_freq
@@ -18,7 +18,7 @@ from sglang.srt.layers.rotary_embedding.yarn import (
yarn_get_mscale_simple, yarn_get_mscale_simple,
yarn_linear_ramp_mask, yarn_linear_ramp_mask,
) )
from sglang.srt.runtime_context import attention_backends, get_exec from sglang.srt.runtime_context import attention_backends
from sglang.srt.utils import ( from sglang.srt.utils import (
cpu_has_amx_support, cpu_has_amx_support,
is_cuda, is_cuda,
@@ -131,7 +131,7 @@ class MRotaryEmbedding(RotaryEmbedding):
self.register_buffer("axis_map", axis_map, persistent=False) self.register_buffer("axis_map", axis_map, persistent=False)
else: else:
self.axis_map = None self.axis_map = None
if get_exec().deterministic.rl_on_policy_target is not None: if self._force_native:
self._forward_method = self.forward_native self._forward_method = self.forward_native
def get_cos_sin_with_position(self, positions): def get_cos_sin_with_position(self, positions):
+108 -12
View File
@@ -37,10 +37,6 @@ from transformers.models.qwen2_5_vl.configuration_qwen2_5_vl import (
Qwen2_5_VLConfig, Qwen2_5_VLConfig,
Qwen2_5_VLVisionConfig, Qwen2_5_VLVisionConfig,
) )
from transformers.models.qwen2_5_vl.modeling_qwen2_5_vl import (
Qwen2_5_VisionPatchEmbed,
Qwen2_5_VisionRotaryEmbedding,
)
from sglang.srt.distributed.parallel_state import get_pp_group from sglang.srt.distributed.parallel_state import get_pp_group
from sglang.srt.environ import envs from sglang.srt.environ import envs
@@ -50,10 +46,12 @@ from sglang.srt.layers.attention.vision import (
VisionAttentionMetadata, VisionAttentionMetadata,
prepare_vision_attention_metadata, prepare_vision_attention_metadata,
) )
from sglang.srt.layers.conv import Conv3dLayer
from sglang.srt.layers.layernorm import RMSNorm from sglang.srt.layers.layernorm import RMSNorm
from sglang.srt.layers.linear import ( from sglang.srt.layers.linear import (
ColumnParallelLinear, ColumnParallelLinear,
MergedColumnParallelLinear, MergedColumnParallelLinear,
ReplicatedLinear,
RowParallelLinear, RowParallelLinear,
) )
from sglang.srt.layers.logits_processor import LogitsProcessor from sglang.srt.layers.logits_processor import LogitsProcessor
@@ -85,6 +83,52 @@ _is_cpu = is_cpu()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Qwen2_5_VisionPatchEmbed(nn.Module):
def __init__(
self,
patch_size: int,
temporal_patch_size: int,
in_channels: int,
embed_dim: int,
disable_linear: bool = False,
) -> None:
super().__init__()
self.patch_size = patch_size
self.temporal_patch_size = temporal_patch_size
self.in_channels = in_channels
self.embed_dim = embed_dim
kernel_size = (temporal_patch_size, patch_size, patch_size)
self.proj = Conv3dLayer(
in_channels,
embed_dim,
kernel_size=kernel_size,
stride=kernel_size,
bias=False,
disable_linear=disable_linear,
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = hidden_states.view(
-1,
self.in_channels,
self.temporal_patch_size,
self.patch_size,
self.patch_size,
)
hidden_states = self.proj(hidden_states.to(self.proj.weight.dtype))
return hidden_states.view(-1, self.embed_dim)
class Qwen2_5_VisionRotaryEmbedding(nn.Module):
def __init__(self, dim: int, theta: float = 10000.0) -> None:
super().__init__()
inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
self.register_buffer("inv_freq", inv_freq, persistent=False)
def forward(self, position_ids: torch.Tensor) -> torch.Tensor:
return (position_ids.unsqueeze(-1) * self.inv_freq).flatten(1)
class Qwen2_5_VLMLP(nn.Module): class Qwen2_5_VLMLP(nn.Module):
def __init__( def __init__(
self, self,
@@ -95,10 +139,24 @@ class Qwen2_5_VLMLP(nn.Module):
quant_config: Optional[QuantizationConfig] = None, quant_config: Optional[QuantizationConfig] = None,
prefix: str = "", prefix: str = "",
use_data_parallel: bool = False, use_data_parallel: bool = False,
fuse_gate_up: bool = True,
tp_size: Optional[int] = None,
tp_rank: Optional[int] = None,
): ):
super().__init__() super().__init__()
self.tp_size = 1 if use_data_parallel else get_parallel().tp_size if use_data_parallel:
self.tp_rank = 0 if use_data_parallel else get_parallel().tp_rank if tp_size is not None or tp_rank is not None:
raise ValueError(
"Explicit MLP TP cannot be combined with data parallel"
)
self.tp_size, self.tp_rank = 1, 0
else:
if (tp_size is None) != (tp_rank is None):
raise ValueError("MLP tp_size and tp_rank must be set together")
self.tp_size = get_parallel().tp_size if tp_size is None else tp_size
self.tp_rank = get_parallel().tp_rank if tp_rank is None else tp_rank
self.fuse_gate_up = fuse_gate_up
if fuse_gate_up:
self.gate_up_proj = MergedColumnParallelLinear( self.gate_up_proj = MergedColumnParallelLinear(
input_size=in_features, input_size=in_features,
output_sizes=[hidden_features] * 2, # [gate_proj, up_proj] output_sizes=[hidden_features] * 2, # [gate_proj, up_proj]
@@ -108,6 +166,32 @@ class Qwen2_5_VLMLP(nn.Module):
tp_size=self.tp_size, tp_size=self.tp_size,
tp_rank=self.tp_rank, tp_rank=self.tp_rank,
) )
else:
projection_kwargs = dict(
input_size=in_features,
output_size=hidden_features,
bias=bias,
quant_config=quant_config,
tp_size=self.tp_size,
tp_rank=self.tp_rank,
)
self.gate_proj = ColumnParallelLinear(
**projection_kwargs,
prefix=add_prefix("gate_proj", prefix),
)
self.up_proj = ColumnParallelLinear(
**projection_kwargs,
prefix=add_prefix("up_proj", prefix),
)
if not self.fuse_gate_up and self.tp_size == 1:
self.down_proj = ReplicatedLinear(
hidden_features,
in_features,
bias=bias,
quant_config=quant_config,
prefix=add_prefix("down_proj", prefix),
)
else:
self.down_proj = RowParallelLinear( self.down_proj = RowParallelLinear(
hidden_features, hidden_features,
in_features, in_features,
@@ -118,8 +202,10 @@ class Qwen2_5_VLMLP(nn.Module):
tp_rank=self.tp_rank, tp_rank=self.tp_rank,
) )
self.hidden_act = hidden_act self.hidden_act = hidden_act
if self.hidden_act == "silu": if self.fuse_gate_up and self.hidden_act == "silu":
self.act = SiluAndMul() self.act = SiluAndMul()
elif not self.fuse_gate_up:
self.act = ACT2FN[self.hidden_act]
else: else:
base_act = ACT2FN[self.hidden_act] base_act = ACT2FN[self.hidden_act]
@@ -130,8 +216,13 @@ class Qwen2_5_VLMLP(nn.Module):
self.act = _act_fn self.act = _act_fn
def forward(self, x: torch.Tensor) -> torch.Tensor: def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.fuse_gate_up:
gate_up, _ = self.gate_up_proj(x) gate_up, _ = self.gate_up_proj(x)
x = self.act(gate_up) x = self.act(gate_up)
else:
gate, _ = self.gate_proj(x)
up, _ = self.up_proj(x)
x = self.act(gate) * up
x_down, _ = self.down_proj(x) x_down, _ = self.down_proj(x)
return x_down return x_down
@@ -225,11 +316,18 @@ class Qwen2_5_VisionPatchMerger(nn.Module):
quant_config: Optional[QuantizationConfig] = None, quant_config: Optional[QuantizationConfig] = None,
prefix: str = "", prefix: str = "",
use_data_parallel: bool = False, use_data_parallel: bool = False,
cast_x_before_out_mul: bool = False,
force_native_norm: bool = False,
) -> None: ) -> None:
super().__init__() super().__init__()
self.hidden_size = context_dim * (spatial_merge_size**2) self.hidden_size = context_dim * (spatial_merge_size**2)
self.padded_context_dim = padded_context_dim * (spatial_merge_size**2) self.padded_context_dim = padded_context_dim * (spatial_merge_size**2)
self.ln_q = RMSNorm(context_dim, eps=1e-6) self.ln_q = RMSNorm(
context_dim,
eps=1e-6,
cast_x_before_out_mul=cast_x_before_out_mul,
force_native=force_native_norm,
)
tp_size = 1 if use_data_parallel else get_parallel().tp_size tp_size = 1 if use_data_parallel else get_parallel().tp_size
tp_rank = 0 if use_data_parallel else get_parallel().tp_rank tp_rank = 0 if use_data_parallel else get_parallel().tp_rank
self.mlp = nn.ModuleList( self.mlp = nn.ModuleList(
@@ -257,10 +355,8 @@ class Qwen2_5_VisionPatchMerger(nn.Module):
) )
def forward(self, x: torch.Tensor) -> torch.Tensor: def forward(self, x: torch.Tensor) -> torch.Tensor:
# x expected shape: [S, B, context_dim] x2d = x.reshape(-1, x.shape[-1])
S, B, D = x.shape x2d = self.ln_q(x2d)
x2d = x.reshape(-1, D)
x2d = self.ln_q(x2d) # RMSNorm expects 2D
x2d = x2d.view(-1, self.hidden_size) # group into spatial_merge_unit x2d = x2d.view(-1, self.hidden_size) # group into spatial_merge_unit
mlp_fc1, mlp_act, mlp_fc2 = self.mlp mlp_fc1, mlp_act, mlp_fc2 = self.mlp
x_parallel, _ = mlp_fc1(x2d) x_parallel, _ = mlp_fc1(x2d)
+35 -5
View File
@@ -102,6 +102,25 @@ _is_cpu = is_cpu()
_VECTORIZED_VL_POS_EMBED_MIN_IMAGES = 6 _VECTORIZED_VL_POS_EMBED_MIN_IMAGES = 6
def _resolve_vision_tp(
*,
use_data_parallel: bool,
tp_size: Optional[int],
tp_rank: Optional[int],
) -> tuple[int, int]:
if use_data_parallel:
if tp_size is not None or tp_rank is not None:
raise ValueError("Explicit vision TP cannot be combined with data parallel")
return 1, 0
if (tp_size is None) != (tp_rank is None):
raise ValueError("Vision tp_size and tp_rank must be set together")
if tp_size is None:
parallel = get_parallel()
return parallel.attn_tp_size, parallel.attn_tp_rank
assert tp_rank is not None
return tp_size, tp_rank
class Qwen3_VisionMLP(nn.Module): class Qwen3_VisionMLP(nn.Module):
def __init__( def __init__(
@@ -113,10 +132,15 @@ class Qwen3_VisionMLP(nn.Module):
quant_config: Optional[QuantizationConfig] = None, quant_config: Optional[QuantizationConfig] = None,
prefix: str = "", prefix: str = "",
use_data_parallel: bool = False, use_data_parallel: bool = False,
tp_size: Optional[int] = None,
tp_rank: Optional[int] = None,
): ):
super().__init__() super().__init__()
self.tp_size = 1 if use_data_parallel else get_parallel().attn_tp_size self.tp_size, self.tp_rank = _resolve_vision_tp(
self.tp_rank = 0 if use_data_parallel else get_parallel().attn_tp_rank use_data_parallel=use_data_parallel,
tp_size=tp_size,
tp_rank=tp_rank,
)
self.linear_fc1 = ColumnParallelLinear( self.linear_fc1 = ColumnParallelLinear(
in_features, in_features,
hidden_features, hidden_features,
@@ -145,7 +169,7 @@ class Qwen3_VisionMLP(nn.Module):
class Qwen3VLVisionPatchEmbed(nn.Module): class Qwen3VLVisionPatchEmbed(nn.Module):
def __init__(self, config) -> None: def __init__(self, config, disable_linear: bool = False) -> None:
super().__init__() super().__init__()
self.patch_size = config.patch_size self.patch_size = config.patch_size
self.temporal_patch_size = config.temporal_patch_size self.temporal_patch_size = config.temporal_patch_size
@@ -159,6 +183,7 @@ class Qwen3VLVisionPatchEmbed(nn.Module):
kernel_size=kernel_size, kernel_size=kernel_size,
stride=kernel_size, stride=kernel_size,
bias=True, bias=True,
disable_linear=disable_linear,
) )
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
@@ -265,6 +290,8 @@ class Qwen3VLMoeVisionPatchMerger(nn.Module):
quant_config: Optional[QuantizationConfig] = None, quant_config: Optional[QuantizationConfig] = None,
prefix: str = "", prefix: str = "",
use_data_parallel: bool = False, use_data_parallel: bool = False,
tp_size: Optional[int] = None,
tp_rank: Optional[int] = None,
) -> None: ) -> None:
super().__init__() super().__init__()
self.hidden_size = context_dim * (spatial_merge_size**2) self.hidden_size = context_dim * (spatial_merge_size**2)
@@ -277,8 +304,11 @@ class Qwen3VLMoeVisionPatchMerger(nn.Module):
self.norm = norm_layer( self.norm = norm_layer(
self.hidden_size if use_postshuffle_norm else context_dim self.hidden_size if use_postshuffle_norm else context_dim
) )
self.tp_size = 1 if use_data_parallel else get_parallel().attn_tp_size self.tp_size, self.tp_rank = _resolve_vision_tp(
self.tp_rank = 0 if use_data_parallel else get_parallel().attn_tp_rank use_data_parallel=use_data_parallel,
tp_size=tp_size,
tp_rank=tp_rank,
)
self.linear_fc1 = ColumnParallelLinear( self.linear_fc1 = ColumnParallelLinear(
self.hidden_size, self.hidden_size,
self.padded_context_dim, self.padded_context_dim,
@@ -4,10 +4,43 @@ import unittest
import torch import torch
from sglang.srt.layers.layernorm import RMSNorm from sglang.srt.layers.layernorm import RMSNorm
from sglang.test.ci.ci_register import register_cuda_ci from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import CustomTestCase from sglang.test.test_utils import CustomTestCase
register_cuda_ci(est_time=15, stage="base-b", runner_config="1-gpu-large") register_cuda_ci(est_time=15, stage="base-b", runner_config="1-gpu-large")
register_amd_ci(est_time=2, suite="stage-b-test-1-gpu-small-amd")
class TestRMSNormInputShape(CustomTestCase):
@classmethod
def setUpClass(cls):
if not torch.cuda.is_available():
raise unittest.SkipTest("CUDA is not available")
def test_higher_rank_residual(self):
torch.manual_seed(0)
shape = (2, 3, 512)
cast_modes = (False,) if torch.version.hip is not None else (False, True)
for cast_x_before_out_mul in cast_modes:
with self.subTest(cast_x_before_out_mul=cast_x_before_out_mul):
layer = RMSNorm(
shape[-1], cast_x_before_out_mul=cast_x_before_out_mul
).to(device="cuda", dtype=torch.bfloat16)
layer.weight.data.normal_(mean=1.0, std=0.1)
x = torch.randn(shape, device="cuda", dtype=torch.bfloat16)
residual = torch.randn_like(x)
with torch.inference_mode():
expected = layer.forward_native(x.clone(), residual.clone())
actual = layer(x.clone(), residual.clone())
self.assertEqual(actual[0].shape, x.shape)
self.assertEqual(actual[1].shape, residual.shape)
torch.testing.assert_close(
actual[0], expected[0], atol=1e-2, rtol=1.5e-2
)
torch.testing.assert_close(actual[1], expected[1], atol=1e-2, rtol=1e-2)
class TestRMSNormFp8QuantFusion(CustomTestCase): class TestRMSNormFp8QuantFusion(CustomTestCase):