[diffusion] chore: use native qwen3-vl vision encoder (#34945)
This commit is contained in:
@@ -7,6 +7,7 @@ from sglang.multimodal_gen.configs.models.encoders.base import (
|
|||||||
TextEncoderConfig,
|
TextEncoderConfig,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.configs.models.fsdp import (
|
from sglang.multimodal_gen.configs.models.fsdp import (
|
||||||
|
is_block,
|
||||||
is_embed_tokens,
|
is_embed_tokens,
|
||||||
is_final_norm,
|
is_final_norm,
|
||||||
is_layer,
|
is_layer,
|
||||||
@@ -56,7 +57,7 @@ class Qwen3VLArchConfig(TextEncoderArchConfig):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
_fsdp_shard_conditions: list = field(
|
_fsdp_shard_conditions: list = field(
|
||||||
default_factory=lambda: [is_layer, is_embed_tokens, is_final_norm]
|
default_factory=lambda: [is_layer, is_block, is_embed_tokens, is_final_norm]
|
||||||
)
|
)
|
||||||
|
|
||||||
# JoyImage specific settings
|
# JoyImage specific settings
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class MiniMaxH3Qwen3VLEncoder(TextEncoder):
|
|||||||
# encode_ids drives the forward pass; __call__ is never used, so FSDP2
|
# encode_ids drives the forward pass; __call__ is never used, so FSDP2
|
||||||
# needs it registered or the root group (the vision tower) stays sharded.
|
# needs it registered or the root group (the vision tower) stays sharded.
|
||||||
_fsdp_forward_methods = ("encode_ids",)
|
_fsdp_forward_methods = ("encode_ids",)
|
||||||
|
layer_names = [*TextEncoder.layer_names, "model.visual.blocks"]
|
||||||
|
|
||||||
supports_dp_encode = True
|
supports_dp_encode = True
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ from transformers import (
|
|||||||
)
|
)
|
||||||
from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
|
from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
|
||||||
from transformers.utils import TransformersKwargs, is_torchdynamo_compiling
|
from transformers.utils import TransformersKwargs, is_torchdynamo_compiling
|
||||||
from transformers.utils.generic import is_flash_attention_requested
|
|
||||||
from transformers.vision_utils import get_vision_cu_seqlens, get_vision_position_ids
|
|
||||||
|
|
||||||
from sglang.multimodal_gen.configs.models.encoders.qwen3vl import Qwen3VLConfig
|
from sglang.multimodal_gen.configs.models.encoders.qwen3vl import Qwen3VLConfig
|
||||||
from sglang.multimodal_gen.runtime.distributed import (
|
from sglang.multimodal_gen.runtime.distributed import (
|
||||||
@@ -31,6 +29,9 @@ from sglang.multimodal_gen.runtime.layers.quantization.weight_only_fp8 import (
|
|||||||
)
|
)
|
||||||
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.qwen3vl_vision import (
|
||||||
|
Qwen3VLVisionTransformer,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
|
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
|
||||||
|
|
||||||
"""Inference-only Qwen3-VL model compatible with HuggingFace weights."""
|
"""Inference-only Qwen3-VL model compatible with HuggingFace weights."""
|
||||||
@@ -58,7 +59,6 @@ from transformers.models.qwen3_vl.modeling_qwen3_vl import (
|
|||||||
Qwen3VLModelOutputWithPast,
|
Qwen3VLModelOutputWithPast,
|
||||||
Qwen3VLTextRMSNorm,
|
Qwen3VLTextRMSNorm,
|
||||||
Qwen3VLTextRotaryEmbedding,
|
Qwen3VLTextRotaryEmbedding,
|
||||||
Qwen3VLVisionModel,
|
|
||||||
apply_rotary_pos_emb,
|
apply_rotary_pos_emb,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -664,7 +664,7 @@ class Qwen3VLModel(nn.Module):
|
|||||||
|
|
||||||
def __init__(self, config, *, use_tensor_parallel: bool = False):
|
def __init__(self, config, *, use_tensor_parallel: bool = False):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.visual = Qwen3VLVisionModel._from_config(config.vision_config)
|
self.visual = Qwen3VLVisionTransformer(config.vision_config)
|
||||||
self.language_model = Qwen3VLTextModel(
|
self.language_model = Qwen3VLTextModel(
|
||||||
config.text_config,
|
config.text_config,
|
||||||
use_tensor_parallel=use_tensor_parallel,
|
use_tensor_parallel=use_tensor_parallel,
|
||||||
@@ -874,18 +874,7 @@ class Qwen3VLModel(nn.Module):
|
|||||||
pixel_values: torch.FloatTensor,
|
pixel_values: torch.FloatTensor,
|
||||||
grid_thw: Optional[torch.LongTensor],
|
grid_thw: Optional[torch.LongTensor],
|
||||||
):
|
):
|
||||||
pixel_values = pixel_values.type(self.visual.dtype)
|
visual_out = self.visual(pixel_values, grid_thw=grid_thw)
|
||||||
vision_kwargs = {}
|
|
||||||
if grid_thw is not None and grid_thw.device.type == "cpu":
|
|
||||||
if not is_flash_attention_requested(self.visual.config):
|
|
||||||
vision_kwargs = {
|
|
||||||
"position_ids": get_vision_position_ids(
|
|
||||||
grid_thw, self.visual.spatial_merge_size
|
|
||||||
).to(pixel_values.device),
|
|
||||||
"cu_seqlens": get_vision_cu_seqlens(grid_thw),
|
|
||||||
}
|
|
||||||
grid_thw = grid_thw.to(pixel_values.device)
|
|
||||||
visual_out = self.visual(pixel_values, grid_thw=grid_thw, **vision_kwargs)
|
|
||||||
return visual_out.pooler_output, visual_out.deepstack_features
|
return visual_out.pooler_output, visual_out.deepstack_features
|
||||||
|
|
||||||
def get_image_features(
|
def get_image_features(
|
||||||
@@ -1161,6 +1150,7 @@ class Qwen3VLModel(nn.Module):
|
|||||||
|
|
||||||
|
|
||||||
class Qwen3VLForConditionalGeneration(TextEncoder):
|
class Qwen3VLForConditionalGeneration(TextEncoder):
|
||||||
|
layer_names = [*TextEncoder.layer_names, "model.visual.blocks"]
|
||||||
default_bitsandbytes_target_modules = [
|
default_bitsandbytes_target_modules = [
|
||||||
".gate_up_proj.",
|
".gate_up_proj.",
|
||||||
".down_proj.",
|
".down_proj.",
|
||||||
|
|||||||
@@ -0,0 +1,429 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Native Qwen3-VL vision encoder."""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Qwen3VLVisionOutput:
|
||||||
|
last_hidden_state: torch.Tensor
|
||||||
|
pooler_output: 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):
|
||||||
|
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, sequence_length: int) -> torch.Tensor:
|
||||||
|
positions = torch.arange(
|
||||||
|
sequence_length,
|
||||||
|
device=self.inv_freq.device,
|
||||||
|
dtype=self.inv_freq.dtype,
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
def __init__(self, config: Any, layer_idx: int) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.norm1 = nn.LayerNorm(config.hidden_size, eps=1e-6)
|
||||||
|
self.norm2 = nn.LayerNorm(config.hidden_size, eps=1e-6)
|
||||||
|
self.attn = Qwen3VLVisionAttention(
|
||||||
|
config, prefix=f"visual.blocks.{layer_idx}.attn"
|
||||||
|
)
|
||||||
|
self.mlp = Qwen3VLVisionMLP(config)
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
hidden_states: torch.Tensor,
|
||||||
|
*,
|
||||||
|
metadata: _PackedSequenceMetadata,
|
||||||
|
position_embeddings: tuple[torch.Tensor, torch.Tensor],
|
||||||
|
) -> torch.Tensor:
|
||||||
|
hidden_states = hidden_states + self.attn(
|
||||||
|
self.norm1(hidden_states),
|
||||||
|
metadata=metadata,
|
||||||
|
position_embeddings=position_embeddings,
|
||||||
|
)
|
||||||
|
return hidden_states + self.mlp(self.norm2(hidden_states))
|
||||||
|
|
||||||
|
|
||||||
|
class 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(
|
||||||
|
grid_thw: torch.Tensor, spatial_merge_size: int
|
||||||
|
) -> torch.Tensor:
|
||||||
|
position_ids = []
|
||||||
|
device = grid_thw.device
|
||||||
|
for temporal, height, width in grid_thw.tolist():
|
||||||
|
merged_height = height // spatial_merge_size
|
||||||
|
merged_width = width // spatial_merge_size
|
||||||
|
block_rows = torch.arange(merged_height, device=device)
|
||||||
|
block_cols = torch.arange(merged_width, device=device)
|
||||||
|
intra_rows = torch.arange(spatial_merge_size, device=device)
|
||||||
|
intra_cols = torch.arange(spatial_merge_size, device=device)
|
||||||
|
rows = (
|
||||||
|
block_rows[:, None, None, None] * spatial_merge_size
|
||||||
|
+ intra_rows[None, None, :, None]
|
||||||
|
)
|
||||||
|
cols = (
|
||||||
|
block_cols[None, :, None, None] * spatial_merge_size
|
||||||
|
+ intra_cols[None, None, None, :]
|
||||||
|
)
|
||||||
|
rows = rows.expand(
|
||||||
|
merged_height, merged_width, spatial_merge_size, spatial_merge_size
|
||||||
|
).reshape(-1)
|
||||||
|
cols = cols.expand(
|
||||||
|
merged_height, merged_width, spatial_merge_size, spatial_merge_size
|
||||||
|
).reshape(-1)
|
||||||
|
coordinates = torch.stack((rows, cols), dim=-1)
|
||||||
|
position_ids.append(coordinates.repeat(temporal, 1))
|
||||||
|
return torch.cat(position_ids)
|
||||||
|
|
||||||
|
|
||||||
|
def _vision_bilinear_indices_and_weights(
|
||||||
|
grid_thw: torch.Tensor,
|
||||||
|
num_grid_per_side: int,
|
||||||
|
spatial_merge_size: int,
|
||||||
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
index_parts: list[list[torch.Tensor]] = [[] for _ in range(4)]
|
||||||
|
weight_parts: list[list[torch.Tensor]] = [[] for _ in range(4)]
|
||||||
|
device = grid_thw.device
|
||||||
|
|
||||||
|
for temporal, height, width in grid_thw.tolist():
|
||||||
|
height_positions = torch.linspace(
|
||||||
|
0, num_grid_per_side - 1, height, device=device
|
||||||
|
)
|
||||||
|
width_positions = torch.linspace(0, num_grid_per_side - 1, width, device=device)
|
||||||
|
height_floor = height_positions.int()
|
||||||
|
width_floor = width_positions.int()
|
||||||
|
height_ceil = (height_floor + 1).clip(max=num_grid_per_side - 1)
|
||||||
|
width_ceil = (width_floor + 1).clip(max=num_grid_per_side - 1)
|
||||||
|
height_fraction = height_positions - height_floor
|
||||||
|
width_fraction = width_positions - width_floor
|
||||||
|
base_height = height_floor * num_grid_per_side
|
||||||
|
base_height_ceil = height_ceil * num_grid_per_side
|
||||||
|
|
||||||
|
corner_indices = (
|
||||||
|
(base_height[:, None] + width_floor[None]).flatten(),
|
||||||
|
(base_height[:, None] + width_ceil[None]).flatten(),
|
||||||
|
(base_height_ceil[:, None] + width_floor[None]).flatten(),
|
||||||
|
(base_height_ceil[:, None] + width_ceil[None]).flatten(),
|
||||||
|
)
|
||||||
|
corner_weights = (
|
||||||
|
((1 - height_fraction)[:, None] * (1 - width_fraction)[None]).flatten(),
|
||||||
|
((1 - height_fraction)[:, None] * width_fraction[None]).flatten(),
|
||||||
|
(height_fraction[:, None] * (1 - width_fraction)[None]).flatten(),
|
||||||
|
(height_fraction[:, None] * width_fraction[None]).flatten(),
|
||||||
|
)
|
||||||
|
|
||||||
|
height_order = torch.arange(height, device=device).view(
|
||||||
|
height // spatial_merge_size, spatial_merge_size
|
||||||
|
)
|
||||||
|
width_order = torch.arange(width, device=device).view(
|
||||||
|
width // spatial_merge_size, spatial_merge_size
|
||||||
|
)
|
||||||
|
merge_order = (
|
||||||
|
(height_order[:, :, None, None] * width + width_order[None, None, :, :])
|
||||||
|
.transpose(1, 2)
|
||||||
|
.flatten()
|
||||||
|
.repeat(temporal)
|
||||||
|
)
|
||||||
|
for corner in range(4):
|
||||||
|
index_parts[corner].append(corner_indices[corner][merge_order])
|
||||||
|
weight_parts[corner].append(corner_weights[corner][merge_order])
|
||||||
|
|
||||||
|
indices = torch.stack([torch.cat(parts) for parts in index_parts])
|
||||||
|
weights = torch.stack([torch.cat(parts) for parts in weight_parts])
|
||||||
|
return indices, weights
|
||||||
|
|
||||||
|
|
||||||
|
def _vision_cu_seqlens(grid_thw: torch.Tensor) -> torch.Tensor:
|
||||||
|
cu_seqlens = torch.repeat_interleave(
|
||||||
|
grid_thw[:, 1] * grid_thw[:, 2], grid_thw[:, 0]
|
||||||
|
).cumsum(dim=0, dtype=torch.int32)
|
||||||
|
return F.pad(cu_seqlens, (1, 0), value=0)
|
||||||
|
|
||||||
|
|
||||||
|
class Qwen3VLVisionTransformer(nn.Module):
|
||||||
|
def __init__(self, config: Any) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.config = config
|
||||||
|
self.spatial_merge_size = config.spatial_merge_size
|
||||||
|
self.spatial_merge_unit = config.spatial_merge_size**2
|
||||||
|
self.patch_size = config.patch_size
|
||||||
|
self.patch_embed = Qwen3VLVisionPatchEmbed(config)
|
||||||
|
self.pos_embed = nn.Embedding(
|
||||||
|
config.num_position_embeddings, config.hidden_size
|
||||||
|
)
|
||||||
|
self.num_grid_per_side = int(config.num_position_embeddings**0.5)
|
||||||
|
head_dim = config.hidden_size // config.num_heads
|
||||||
|
self.rotary_pos_emb = Qwen3VLVisionRotaryEmbedding(head_dim // 2)
|
||||||
|
self.blocks = nn.ModuleList(
|
||||||
|
Qwen3VLVisionBlock(config, layer_idx) for layer_idx in range(config.depth)
|
||||||
|
)
|
||||||
|
self.merger = Qwen3VLVisionPatchMerger(config, use_postshuffle_norm=False)
|
||||||
|
self.deepstack_visual_indexes = tuple(config.deepstack_visual_indexes)
|
||||||
|
self.deepstack_merger_list = nn.ModuleList(
|
||||||
|
Qwen3VLVisionPatchMerger(config, use_postshuffle_norm=True)
|
||||||
|
for _ in self.deepstack_visual_indexes
|
||||||
|
)
|
||||||
|
self._deepstack_merger_by_layer = {
|
||||||
|
layer_idx: merger_idx
|
||||||
|
for merger_idx, layer_idx in enumerate(self.deepstack_visual_indexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dtype(self) -> torch.dtype:
|
||||||
|
return self.patch_embed.proj.weight.dtype
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self) -> torch.device:
|
||||||
|
return self.patch_embed.proj.weight.device
|
||||||
|
|
||||||
|
def _interpolate_position_embeddings(self, grid_thw: torch.Tensor) -> torch.Tensor:
|
||||||
|
indices, weights = _vision_bilinear_indices_and_weights(
|
||||||
|
grid_thw,
|
||||||
|
num_grid_per_side=self.num_grid_per_side,
|
||||||
|
spatial_merge_size=self.spatial_merge_size,
|
||||||
|
)
|
||||||
|
return (self.pos_embed(indices) * weights[:, :, None]).sum(0)
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
hidden_states: torch.Tensor,
|
||||||
|
grid_thw: torch.Tensor,
|
||||||
|
**_: Any,
|
||||||
|
) -> Qwen3VLVisionOutput:
|
||||||
|
hidden_states = hidden_states.to(device=self.device, dtype=self.dtype)
|
||||||
|
grid_thw = grid_thw.to(self.device)
|
||||||
|
hidden_states = self.patch_embed(hidden_states)
|
||||||
|
position_embeddings = self._interpolate_position_embeddings(grid_thw)
|
||||||
|
hidden_states = hidden_states + position_embeddings.to(hidden_states.dtype)
|
||||||
|
|
||||||
|
position_ids = _vision_position_ids(grid_thw, self.spatial_merge_size)
|
||||||
|
rotary = self.rotary_pos_emb(int(grid_thw[:, 1:].max()))[position_ids]
|
||||||
|
rotary = rotary.flatten(1)
|
||||||
|
rotary = torch.cat((rotary, rotary), dim=-1)
|
||||||
|
position_embeddings = (rotary.cos(), rotary.sin())
|
||||||
|
metadata = _PackedSequenceMetadata.from_cu_seqlens(_vision_cu_seqlens(grid_thw))
|
||||||
|
|
||||||
|
deepstack_features = []
|
||||||
|
for layer_idx, block in enumerate(self.blocks):
|
||||||
|
hidden_states = block(
|
||||||
|
hidden_states,
|
||||||
|
metadata=metadata,
|
||||||
|
position_embeddings=position_embeddings,
|
||||||
|
)
|
||||||
|
merger_idx = self._deepstack_merger_by_layer.get(layer_idx)
|
||||||
|
if merger_idx is not None:
|
||||||
|
deepstack_features.append(
|
||||||
|
self.deepstack_merger_list[merger_idx](hidden_states)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Qwen3VLVisionOutput(
|
||||||
|
last_hidden_state=hidden_states,
|
||||||
|
pooler_output=self.merger(hidden_states),
|
||||||
|
deepstack_features=deepstack_features,
|
||||||
|
)
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from torch import nn
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.models.encoders.qwen3vl import Qwen3VLArchConfig
|
||||||
|
from sglang.multimodal_gen.runtime.models.encoders.minimax_h3_qwen3vl import (
|
||||||
|
MiniMaxH3Qwen3VLEncoder,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.models.encoders.qwen3vl import (
|
||||||
|
Qwen3VLForConditionalGeneration,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.models.encoders.qwen3vl_vision import (
|
||||||
|
Qwen3VLVisionRotaryEmbedding,
|
||||||
|
Qwen3VLVisionTransformer,
|
||||||
|
_vision_cu_seqlens,
|
||||||
|
_vision_position_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_vision_layout_matches_qwen3_merge_order():
|
||||||
|
grid_thw = torch.tensor([[1, 4, 6], [2, 2, 4]])
|
||||||
|
|
||||||
|
position_ids = _vision_position_ids(grid_thw, spatial_merge_size=2)
|
||||||
|
cu_seqlens = _vision_cu_seqlens(grid_thw)
|
||||||
|
|
||||||
|
assert position_ids.shape == (40, 2)
|
||||||
|
assert position_ids[:8].tolist() == [
|
||||||
|
[0, 0],
|
||||||
|
[0, 1],
|
||||||
|
[1, 0],
|
||||||
|
[1, 1],
|
||||||
|
[0, 2],
|
||||||
|
[0, 3],
|
||||||
|
[1, 2],
|
||||||
|
[1, 3],
|
||||||
|
]
|
||||||
|
assert cu_seqlens.tolist() == [0, 24, 32, 40]
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_vision_keeps_checkpoint_parameter_names():
|
||||||
|
config = SimpleNamespace(
|
||||||
|
hidden_size=16,
|
||||||
|
intermediate_size=24,
|
||||||
|
hidden_act="gelu_pytorch_tanh",
|
||||||
|
num_heads=2,
|
||||||
|
depth=0,
|
||||||
|
patch_size=2,
|
||||||
|
temporal_patch_size=1,
|
||||||
|
in_channels=3,
|
||||||
|
num_position_embeddings=16,
|
||||||
|
spatial_merge_size=2,
|
||||||
|
out_hidden_size=12,
|
||||||
|
deepstack_visual_indexes=[],
|
||||||
|
)
|
||||||
|
model = Qwen3VLVisionTransformer(config)
|
||||||
|
|
||||||
|
assert set(model.state_dict()) == {
|
||||||
|
"patch_embed.proj.weight",
|
||||||
|
"patch_embed.proj.bias",
|
||||||
|
"pos_embed.weight",
|
||||||
|
"merger.norm.weight",
|
||||||
|
"merger.norm.bias",
|
||||||
|
"merger.linear_fc1.weight",
|
||||||
|
"merger.linear_fc1.bias",
|
||||||
|
"merger.linear_fc2.weight",
|
||||||
|
"merger.linear_fc2.bias",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_native_vision_keeps_position_math_in_fp32():
|
||||||
|
class PatchEmbed(nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.proj = nn.Linear(1, 8, bias=False, dtype=torch.bfloat16)
|
||||||
|
|
||||||
|
def forward(self, hidden_states):
|
||||||
|
return self.proj(hidden_states)
|
||||||
|
|
||||||
|
class BlockRecorder(nn.Module):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.position_embedding_dtypes = None
|
||||||
|
|
||||||
|
def forward(self, hidden_states, *, position_embeddings, **_kwargs):
|
||||||
|
self.position_embedding_dtypes = tuple(
|
||||||
|
embedding.dtype for embedding in position_embeddings
|
||||||
|
)
|
||||||
|
return hidden_states
|
||||||
|
|
||||||
|
class Merger(nn.Module):
|
||||||
|
def forward(self, hidden_states):
|
||||||
|
return hidden_states.reshape(-1, 4, hidden_states.shape[-1])[:, 0]
|
||||||
|
|
||||||
|
model = Qwen3VLVisionTransformer.__new__(Qwen3VLVisionTransformer)
|
||||||
|
nn.Module.__init__(model)
|
||||||
|
model.spatial_merge_size = 2
|
||||||
|
model.patch_embed = PatchEmbed()
|
||||||
|
model.pos_embed = nn.Embedding(16, 8, dtype=torch.bfloat16)
|
||||||
|
model.num_grid_per_side = 4
|
||||||
|
model.rotary_pos_emb = Qwen3VLVisionRotaryEmbedding(2)
|
||||||
|
block = BlockRecorder()
|
||||||
|
model.blocks = nn.ModuleList([block])
|
||||||
|
model.merger = Merger()
|
||||||
|
model.deepstack_merger_list = nn.ModuleList()
|
||||||
|
model._deepstack_merger_by_layer = {}
|
||||||
|
|
||||||
|
grid_thw = torch.tensor([[1, 4, 6]])
|
||||||
|
interpolated_position = model._interpolate_position_embeddings(grid_thw)
|
||||||
|
output = model(torch.zeros(24, 1, dtype=torch.bfloat16), grid_thw=grid_thw)
|
||||||
|
|
||||||
|
assert interpolated_position.dtype == torch.float32
|
||||||
|
assert output.pooler_output.dtype == torch.bfloat16
|
||||||
|
assert block.position_embedding_dtypes == (torch.float32, torch.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def test_qwen3_multimodal_encoders_layerwise_offload_vision_blocks():
|
||||||
|
assert "model.visual.blocks" in Qwen3VLForConditionalGeneration.layer_names
|
||||||
|
assert "model.visual.blocks" in MiniMaxH3Qwen3VLEncoder.layer_names
|
||||||
|
assert any(
|
||||||
|
condition.__name__ == "is_block"
|
||||||
|
for condition in Qwen3VLArchConfig()._fsdp_shard_conditions
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user