Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

1773 lines
64 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Inference-only Qwen3.5 model and Qwen3.5 MoE model compatible with HuggingFace weights."""
from __future__ import annotations
import logging
from collections.abc import Iterable
import torch
import torch.nn as nn
import triton
import triton.language as tl
from tokenspeed_kernel.ops.activation.triton import sigmoid_mul
from tokenspeed_kernel.ops.layernorm.triton import (
fused_qk_rmsnorm_rope_gate,
qk_rmsnorm,
)
# Configs
from tokenspeed.runtime.configs.paged_cache_spec import FULL_ATTENTION
from tokenspeed.runtime.configs.qwen3_5_config import (
Qwen3_5Config,
Qwen3_5TextConfig,
)
from tokenspeed.runtime.configs.utils import get_rope_parameters
# Distributed
from tokenspeed.runtime.distributed.comm_manager import CommManager
from tokenspeed.runtime.distributed.mapping import Mapping
from tokenspeed.runtime.execution.context import ForwardContext
# Layers - Attention
from tokenspeed.runtime.layers.attention.linear.layernorm_gated import (
RMSNorm as RMSNormGated,
)
# Layers - Others
from tokenspeed.runtime.layers.layernorm import GemmaRMSNorm
# Layers - Linear
from tokenspeed.runtime.layers.linear import (
ColumnParallelLinear,
MergedColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from tokenspeed.runtime.layers.logits_processor import LogitsMetadata
from tokenspeed.runtime.layers.moe import (
ExpertCheckpointSchema,
build_moe_checkpoint_loader,
)
from tokenspeed.runtime.layers.paged_attention import PagedAttention
from tokenspeed.runtime.layers.parameter import (
BlockQuantScaleParameter,
PerTensorScaleParameter,
)
from tokenspeed.runtime.layers.quantization.base_config import QuantizationConfig
from tokenspeed.runtime.layers.rotary_embedding import get_rope
from tokenspeed.runtime.layers.vocab_parallel_embedding import VocabParallelEmbedding
from tokenspeed.runtime.model_loader.weight_utils import (
default_weight_loader,
mamba_v2_sharded_weight_loader,
sharded_weight_loader,
)
from tokenspeed.runtime.models.base import BaseCausalLM
from tokenspeed.runtime.models.qwen3_5_moe import (
Qwen3_5MoeMLP,
Qwen3_5MoeSparseMoeBlock,
)
from tokenspeed.runtime.models.qwen3_vision import Qwen3VLMoeVisionModel
from tokenspeed.runtime.models.utils import validate_attention_partition
from tokenspeed.runtime.moe.distribution_recorder import (
get_global_expert_distribution_recorder,
)
from tokenspeed.runtime.moe.expert_location import ModelConfigForExpertLocation
from tokenspeed.runtime.multimodal.embedder import (
EncoderSpec,
VisionEmbedder,
pad_input_tokens,
)
from tokenspeed.runtime.multimodal.encoder_cudagraph import (
EncoderCudaGraphWrapper,
VisionEncoderCudaGraphAdapter,
)
from tokenspeed.runtime.multimodal.inputs import (
Modality,
MultimodalDataItem,
MultimodalInputs,
)
from tokenspeed.runtime.utils import (
add_prefix,
make_layers,
set_weight_attrs,
)
from tokenspeed.runtime.utils.env import envs
logger = logging.getLogger(__name__)
class Qwen3_5GatedDeltaNet(nn.Module):
def __init__(
self,
config: Qwen3_5TextConfig,
mapping: Mapping,
layer_id: int,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
self.mapping = mapping
self.attn_tp_rank = mapping.attn.tp_rank
self.attn_tp_size = mapping.attn.tp_size
self.attn_tp_group = mapping.attn.tp_group
self.hidden_size = config.hidden_size
self.num_v_heads = config.linear_num_value_heads
self.num_k_heads = config.linear_num_key_heads
self.head_k_dim = config.linear_key_head_dim
self.head_v_dim = config.linear_value_head_dim
self.key_dim = self.head_k_dim * self.num_k_heads
self.value_dim = self.head_v_dim * self.num_v_heads
self.conv_kernel_size = config.linear_conv_kernel_dim
self.layer_id = layer_id
self.activation = config.hidden_act
self.layer_norm_epsilon = config.rms_norm_eps
# Conv1d layer
self.conv_dim = self.key_dim * 2 + self.value_dim
self.conv1d = ColumnParallelLinear(
input_size=self.conv_kernel_size,
output_size=self.conv_dim,
bias=False,
quant_config=None,
tp_rank=self.attn_tp_rank,
tp_size=self.attn_tp_size,
tp_group=self.attn_tp_group,
prefix=add_prefix("conv1d", prefix),
)
self.conv1d.weight.data = self.conv1d.weight.data.unsqueeze(1)
self.in_proj_qkvzba = MergedColumnParallelLinear(
input_size=self.hidden_size,
output_sizes=[
self.key_dim,
self.key_dim,
self.value_dim,
self.value_dim,
self.num_v_heads,
self.num_v_heads,
],
bias=False,
quant_config=quant_config,
tp_rank=self.attn_tp_rank,
tp_size=self.attn_tp_size,
tp_group=self.attn_tp_group,
prefix=add_prefix("in_proj_qkvzba", prefix),
)
self._qkvz_dim = (self.key_dim * 2 + self.value_dim * 2) // self.attn_tp_size
self._ba_dim = (self.num_v_heads * 2) // self.attn_tp_size
# Override weight loaders for packed checkpoint format.
# Important: for FP8, this must cover not only `.weight` but also
# `weight_scale_inv` / `weight_scale` / `input_scale` if present.
self._bind_packed_weight_loaders(self.in_proj_qkvzba)
# Conv1d weight loader setup
query_key_settings = (self.key_dim, 0, False)
value_settings = (self.value_dim, 0, False)
delattr(self.conv1d.weight, "weight_loader")
set_weight_attrs(
self.conv1d.weight,
{
"weight_loader": mamba_v2_sharded_weight_loader(
[
query_key_settings,
query_key_settings,
value_settings,
],
self.attn_tp_size,
self.attn_tp_rank,
)
},
)
# State parameters
self.dt_bias = nn.Parameter(
torch.ones(self.num_v_heads // self.attn_tp_size),
)
self.A_log = nn.Parameter(
torch.empty(self.num_v_heads // self.attn_tp_size),
)
set_weight_attrs(
self.A_log, {"weight_loader": sharded_weight_loader(0, self.attn_tp_rank)}
)
set_weight_attrs(
self.dt_bias, {"weight_loader": sharded_weight_loader(0, self.attn_tp_rank)}
)
conv_weights = self.conv1d.weight.view(
self.conv1d.weight.size(0), self.conv1d.weight.size(2)
)
self.conv_weights = conv_weights
# Normalization layer
self.norm = RMSNormGated(
self.head_v_dim,
eps=self.layer_norm_epsilon,
group_size=None,
norm_before_gate=True,
device=torch.get_device_module().current_device(),
dtype=config.dtype,
)
# Output projection
self.out_proj = RowParallelLinear(
self.value_dim,
self.hidden_size,
bias=False,
input_is_parallel=True,
reduce_results=False,
quant_config=quant_config,
tp_rank=self.attn_tp_rank,
tp_size=self.attn_tp_size,
tp_group=self.attn_tp_group,
prefix=add_prefix("out_proj", prefix),
)
@staticmethod
def _override_weight_loader(param, loader):
"""Robustly override loader for:
1) BaseWeightParameter subclasses: real storage is `_weight_loader`
2) regular Parameters that already have mutable `weight_loader`
3) regular Parameters without `weight_loader` yet
"""
if hasattr(param, "_weight_loader"):
# FP8 / quantized BaseWeightParameter path
param._weight_loader = loader
return
if hasattr(param, "weight_loader"):
# Regular parameter/tensor that already has a mutable attr.
# Do NOT call set_weight_attrs here; overwriting an existing
# attribute is rejected.
param.weight_loader = loader
return
# Fresh attribute on a normal tensor/Parameter
set_weight_attrs(param, {"weight_loader": loader})
def _bind_packed_weight_loaders(self, module):
"""Bind packed-checkpoint-aware loaders to all relevant params of a merged module."""
for attr_name in ("weight", "weight_scale_inv", "weight_scale", "input_scale"):
param = getattr(module, attr_name, None)
if param is None:
continue
original_loader = getattr(param, "weight_loader", None)
if original_loader is None:
continue
wrapped_loader = self._make_packed_weight_loader(module, original_loader)
self._override_weight_loader(param, wrapped_loader)
@staticmethod
def _get_split_sizes_for_param(module, param, loaded_shard_id):
"""Return checkpoint-side split sizes for this param type."""
if isinstance(param, BlockQuantScaleParameter):
# Split by output blocks, not raw output sizes.
block_n, _ = module.quant_method.quant_config.weight_block_size
block_n = 1 if getattr(param, "format_ue8m0", False) else block_n
return [
(module.output_sizes[idx] + block_n - 1) // block_n
for idx in loaded_shard_id
]
if isinstance(param, PerTensorScaleParameter):
# One logical scale per logical shard.
return [1 for _ in loaded_shard_id]
# Normal weight / non-block quant tensor
return [module.output_sizes[idx] for idx in loaded_shard_id]
@classmethod
def _make_packed_weight_loader(cls, module, original_weight_loader):
"""Wrap the param's original loader so split checkpoints:
- in_proj_qkv + in_proj_z + in_proj_b + in_proj_a -> merged in_proj_qkvzba
can load correctly for both normal and FP8 params.
"""
def weight_loader(param, loaded_weight, loaded_shard_id=None):
# Only intercept split-checkpoint tuple shards.
# int shard_id and None should preserve original behavior.
if isinstance(loaded_shard_id, tuple):
split_sizes = cls._get_split_sizes_for_param(
module, param, loaded_shard_id
)
if len(loaded_weight.shape) == 0:
# Scalar only makes sense for a single logical shard.
if len(split_sizes) != 1 or split_sizes[0] != 1:
raise ValueError(
f"Unexpected scalar for tuple shard load: "
f"{loaded_shard_id=}, {split_sizes=}"
)
chunks = [loaded_weight.reshape(1)]
else:
split_dim = getattr(param, "output_dim", 0)
chunks = loaded_weight.split(split_sizes, dim=split_dim)
if len(chunks) != len(loaded_shard_id):
raise ValueError(
f"Chunk/shard mismatch: {len(chunks)=}, "
f"{len(loaded_shard_id)=}, {split_sizes=}"
)
for idx, chunk in zip(loaded_shard_id, chunks):
# Delegate each chunk to the param's original int-shard loader.
original_weight_loader(param, chunk, idx)
return
return original_weight_loader(param, loaded_weight, loaded_shard_id)
return weight_loader
def fix_query_key_value_ordering(
self,
mixed_qkvz: torch.Tensor,
mixed_ba: torch.Tensor,
):
"""
Derives `query`, `key` and `value` tensors from `mixed_qkvzba`.
"""
k_tp = self.key_dim // self.attn_tp_size
v_tp = self.value_dim // self.attn_tp_size
nv_tp = self.num_v_heads // self.attn_tp_size
# Directly split, no head group reshape
query, key, value, z = mixed_qkvz.split([k_tp, k_tp, v_tp, v_tp], dim=-1)
b, a = mixed_ba.split([nv_tp, nv_tp], dim=-1)
# value / z reshape to (seq, num_v_heads/tp, head_v_dim)
value = value.reshape(value.size(0), -1, self.head_v_dim)
z = z.reshape(z.size(0), -1, self.head_v_dim)
return query, key, value, z, b, a
def _forward_input_proj(self, hidden_states: torch.Tensor):
projected_all, _ = self.in_proj_qkvzba(hidden_states)
projected_states_qkvz, projected_states_ba = projected_all.split(
[self._qkvz_dim, self._ba_dim], dim=-1
)
return projected_states_qkvz, projected_states_ba
def forward(
self,
hidden_states: torch.Tensor,
ctx: ForwardContext,
):
seq_len, _ = hidden_states.shape
projected_states_qkvz, projected_states_ba = self._forward_input_proj(
hidden_states
)
if self.num_v_heads // self.num_k_heads in [1, 2, 4]:
mixed_qkv, z, b, a = fused_qkvzba_split_reshape_cat_contiguous(
projected_states_qkvz,
projected_states_ba,
triton.cdiv(self.num_k_heads, self.attn_tp_size),
triton.cdiv(self.num_v_heads, self.attn_tp_size),
self.head_k_dim,
self.head_v_dim,
)
else:
query, key, value, z, b, a = self.fix_query_key_value_ordering(
projected_states_qkvz, projected_states_ba
)
query, key, value = map(
lambda x: x.reshape(x.shape[0], -1), (query, key, value)
)
mixed_qkv = torch.cat((query, key, value), dim=-1)
kwargs = {
"mixed_qkv": mixed_qkv,
"conv_weights": self.conv_weights,
"bias": self.conv1d.bias,
"activation": self.activation,
"key_dim": self.key_dim,
"value_dim": self.value_dim,
"attention_tp_size": self.attn_tp_size,
"head_k_dim": self.head_k_dim,
"head_v_dim": self.head_v_dim,
"a": a,
"b": b,
"A_log": self.A_log,
"dt_bias": self.dt_bias,
"layer_id": self.layer_id,
"seq_len": seq_len,
"z": z,
}
core_attn_out = ctx.attn_backend.forward(
q=None,
k=None,
v=None,
layer=None,
out_cache_loc=None,
token_to_kv_pool=ctx.token_to_kv_pool,
forward_mode=ctx.forward_mode,
bs=ctx.bs,
**kwargs,
)
z_shape_og = z.shape
core_attn_out = core_attn_out.reshape(-1, core_attn_out.shape[-1])
z = z.reshape(-1, z.shape[-1])
core_attn_out = self.norm(core_attn_out, z)
core_attn_out = core_attn_out.reshape(z_shape_og)
core_attn_out = core_attn_out.reshape(*core_attn_out.shape[:-2], -1)
output, _ = self.out_proj(core_attn_out)
return output
class Qwen3_5LinearDecoderLayer(nn.Module):
"""Qwen3.5 Decoder Layer with Linear Attention (GatedDeltaNet)."""
def __init__(
self,
config: Qwen3_5TextConfig,
mapping: Mapping,
layer_id: int,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
alt_stream: torch.cuda.Stream | None = None,
) -> None:
super().__init__()
self.config = config
self.mapping = mapping
self.layer_id = layer_id
linear_attn_quant_config = (
None
if quant_config and quant_config.get_name() in ("fp8", "nvfp4")
else quant_config
)
self.linear_attn = Qwen3_5GatedDeltaNet(
config, mapping, layer_id, linear_attn_quant_config, prefix=prefix
)
# Determine the MLP type based on the model type
# Qwen3.5 use all layers for MLP / Qwen3.5-MoE use sparse MoE blocks
if config.model_type == "qwen3_5_moe_text":
self.mlp = Qwen3_5MoeSparseMoeBlock(
config=config,
mapping=self.mapping,
quant_config=quant_config,
layer_index=layer_id,
alt_stream=alt_stream,
prefix=add_prefix("mlp", prefix.replace(".linear_attn", "")),
)
is_moe = True
elif config.model_type == "qwen3_5_text":
self.mlp = Qwen3_5MoeMLP(
mapping=self.mapping,
hidden_size=config.hidden_size,
intermediate_size=config.intermediate_size,
hidden_act=config.hidden_act,
quant_config=quant_config,
reduce_results=False,
prefix=add_prefix("mlp", prefix.replace(".linear_attn", "")),
)
is_moe = False
else:
raise ValueError(f"Invalid model type: {config.model_type}")
self.input_layernorm = GemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = GemmaRMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
self.is_moe = is_moe
self.comm_manager = CommManager(
mapping=self.mapping,
layer_id=self.layer_id,
is_moe=is_moe,
prev_is_moe=is_moe,
input_layernorm=self.input_layernorm,
post_attn_layernorm=self.post_attention_layernorm,
)
def forward(
self,
hidden_states: torch.Tensor,
residual: torch.Tensor | None,
ctx: ForwardContext,
**kwargs,
):
num_global_tokens, max_num_tokens_per_gpu = self.comm_manager.get_num_tokens(
ctx
)
if not ctx.forward_mode.is_idle():
hidden_states, residual = self.comm_manager.input_reduce_norm(
hidden_states, residual
)
hidden_states = self.comm_manager.pre_attn_comm(hidden_states, ctx)
hidden_states = self.linear_attn(
hidden_states,
ctx,
)
hidden_states, residual = self.comm_manager.post_attn_reduce_norm(
hidden_states, residual, ctx
)
hidden_states = self.forward_mlp(
hidden_states,
residual,
ctx,
num_global_tokens,
max_num_tokens_per_gpu,
)
return hidden_states, residual
def forward_mlp(
self,
hidden_states,
residual,
ctx: ForwardContext,
num_global_tokens,
max_num_tokens_per_gpu,
):
if isinstance(self.mlp, Qwen3_5MoeSparseMoeBlock):
hidden_states = self.mlp(
hidden_states, num_global_tokens, max_num_tokens_per_gpu, ctx
)
else:
hidden_states = self.comm_manager.pre_mlp_comm(hidden_states, ctx)
hidden_states = self.mlp(hidden_states)
hidden_states, residual = self.comm_manager.post_mlp_fused(
hidden_states, residual, ctx
)
return hidden_states
class Qwen3_5AttentionDecoderLayer(nn.Module):
"""Qwen3.5 Decoder Layer with Full Attention."""
def __init__(
self,
config: Qwen3_5TextConfig,
mapping: Mapping,
layer_id: int,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
alt_stream: torch.cuda.Stream | None = None,
) -> None:
super().__init__()
self.config = config
self.mapping = mapping
self.hidden_size = config.hidden_size
self.attn_tp_rank = mapping.attn.tp_rank
self.attn_tp_size = mapping.attn.tp_size
self.attn_tp_group = mapping.attn.tp_group
self.total_num_heads = config.num_attention_heads
self.total_num_kv_heads = config.num_key_value_heads
validate_attention_partition(
self.total_num_heads,
self.total_num_kv_heads,
self.attn_tp_size,
)
self.num_heads = self.total_num_heads // self.attn_tp_size
self.num_kv_heads = max(1, self.total_num_kv_heads // self.attn_tp_size)
self.head_dim = config.head_dim or (self.hidden_size // self.num_heads)
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_kv_heads * self.head_dim
self.scaling = self.head_dim**-0.5
self.max_position_embeddings = getattr(config, "max_position_embeddings", 8192)
self.rope_scaling = get_rope_parameters(config)
self.rope_theta = self.rope_scaling.get("rope_theta", 10000)
self.partial_rotary_factor = self.rope_scaling.get("partial_rotary_factor", 1.0)
self.layer_id = layer_id
self.attn_output_gate = getattr(config, "attn_output_gate", True)
if self.attn_output_gate:
logger.warning_once("using attn output gate!")
self.rotary_emb = get_rope(
head_size=self.head_dim,
rotary_dim=self.head_dim,
max_position=self.max_position_embeddings,
rope_scaling=self.rope_scaling,
base=self.rope_theta,
partial_rotary_factor=self.partial_rotary_factor,
is_neox_style=True,
dtype=torch.get_default_dtype(),
)
attn_quant_config = (
None
if quant_config and quant_config.get_name() == "nvfp4"
else quant_config
)
self.qkv_proj = QKVParallelLinear(
config.hidden_size,
self.head_dim,
self.total_num_heads * (1 + self.attn_output_gate),
self.total_num_kv_heads,
bias=False,
quant_config=attn_quant_config,
tp_rank=self.attn_tp_rank,
tp_size=self.attn_tp_size,
tp_group=self.attn_tp_group,
prefix=add_prefix("qkv_proj", prefix),
)
self.o_proj = RowParallelLinear(
self.total_num_heads * self.head_dim,
config.hidden_size,
bias=False,
quant_config=attn_quant_config,
reduce_results=False,
tp_rank=self.attn_tp_rank,
tp_size=self.attn_tp_size,
tp_group=self.attn_tp_group,
prefix=add_prefix("o_proj", prefix),
)
self.attn = PagedAttention(
self.num_heads,
self.head_dim,
self.scaling,
num_kv_heads=self.num_kv_heads,
layer_id=layer_id,
group_id=FULL_ATTENTION,
)
# Dense MLP for non-MoE variant
if config.model_type == "qwen3_5_text":
self.mlp = Qwen3_5MoeMLP(
mapping=self.mapping,
hidden_size=config.hidden_size,
intermediate_size=config.intermediate_size,
hidden_act=config.hidden_act,
quant_config=quant_config,
reduce_results=False,
prefix=add_prefix("mlp", prefix.replace(".self_attn", "")),
)
is_moe = False
elif config.model_type == "qwen3_5_moe_text":
self.mlp = Qwen3_5MoeSparseMoeBlock(
config=config,
mapping=self.mapping,
quant_config=quant_config,
layer_index=layer_id,
alt_stream=alt_stream,
prefix=add_prefix("mlp", prefix.replace(".self_attn", "")),
)
is_moe = True
else:
raise ValueError(f"Invalid model type: {config.model_type}")
self.input_layernorm = GemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = GemmaRMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
self.q_norm = GemmaRMSNorm(self.head_dim, eps=config.rms_norm_eps)
self.k_norm = GemmaRMSNorm(self.head_dim, eps=config.rms_norm_eps)
self.is_moe = is_moe
self.comm_manager = CommManager(
mapping=self.mapping,
layer_id=self.layer_id,
is_moe=is_moe,
prev_is_moe=is_moe,
input_layernorm=self.input_layernorm,
post_attn_layernorm=self.post_attention_layernorm,
)
def _apply_qk_norm(
self, q: torch.Tensor, k: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
# qk_rmsnorm expects GemmaRMSNorm's effective gamma.
return qk_rmsnorm(
q,
k,
self.q_norm.gemma_weight,
self.k_norm.gemma_weight,
self.q_norm.variance_epsilon,
)
def _project_qkv_rope(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor | None]:
"""qkv_proj + split + rope (+ optional gate). ``gate`` is ``None`` when ``attn_output_gate=False``."""
qkv, _ = self.qkv_proj(hidden_states)
if self.attn_output_gate:
q_gate, k, v = qkv.split(
[self.q_size * 2, self.kv_size, self.kv_size], dim=-1
)
q, k, gate = fused_qk_rmsnorm_rope_gate(
q_gate,
k,
self.q_norm.gemma_weight,
self.k_norm.gemma_weight,
self.rotary_emb.cos_sin_cache,
positions,
self.q_norm.variance_epsilon,
self.num_heads,
self.num_kv_heads,
self.head_dim,
self.rotary_emb.rotary_dim,
)
return q, k, v, gate
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
q, k = self._apply_qk_norm(q, k)
q, k = self.rotary_emb(positions, q, k)
return q, k, v, None
def _attn(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
gate: torch.Tensor | None,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
) -> torch.Tensor:
"""Backend attention call + optional gate apply. Subclasses override."""
attn_output = self.attn(q, k, v, ctx, out_cache_loc)
if gate is not None:
sigmoid_mul(attn_output, gate)
return attn_output
def self_attention(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
) -> torch.Tensor:
"""Full attention forward pass."""
q, k, v, gate = self._project_qkv_rope(positions, hidden_states)
attn_output = self._attn(q, k, v, gate, ctx, out_cache_loc)
output, _ = self.o_proj(attn_output)
return output
def _maybe_narrow_residual(
self,
residual: torch.Tensor,
ctx: ForwardContext,
) -> torch.Tensor:
"""Hook: subclasses narrow residual to match a sliced attn output."""
return residual
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
residual: torch.Tensor | None,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
**kwargs,
):
num_global_tokens, max_num_tokens_per_gpu = self.comm_manager.get_num_tokens(
ctx
)
if not ctx.forward_mode.is_idle():
hidden_states, residual = self.comm_manager.input_reduce_norm(
hidden_states, residual
)
hidden_states = self.comm_manager.pre_attn_comm(hidden_states, ctx)
hidden_states = self.self_attention(
positions=positions,
hidden_states=hidden_states,
ctx=ctx,
out_cache_loc=out_cache_loc,
)
residual = self._maybe_narrow_residual(residual, ctx)
hidden_states, residual = self.comm_manager.post_attn_reduce_norm(
hidden_states, residual, ctx
)
hidden_states = self.forward_mlp(
hidden_states,
residual,
ctx,
num_global_tokens,
max_num_tokens_per_gpu,
)
return hidden_states, residual
def forward_mlp(
self,
hidden_states,
residual,
ctx: ForwardContext,
num_global_tokens,
max_num_tokens_per_gpu,
):
if isinstance(self.mlp, Qwen3_5MoeSparseMoeBlock):
hidden_states = self.mlp(
hidden_states, num_global_tokens, max_num_tokens_per_gpu, ctx
)
else:
hidden_states = self.comm_manager.pre_mlp_comm(hidden_states, ctx)
hidden_states = self.mlp(hidden_states)
hidden_states, residual = self.comm_manager.post_mlp_fused(
hidden_states, residual, ctx
)
return hidden_states
class Qwen3_5ForCausalLM(nn.Module):
"""Qwen3.5 Model with support for dense variant."""
ATTENTION_LAYER_CLS: type = Qwen3_5AttentionDecoderLayer
LINEAR_LAYER_CLS: type = Qwen3_5LinearDecoderLayer
def __init__(
self,
config: Qwen3_5TextConfig,
mapping: Mapping,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
self.mapping = mapping
self.hidden_size = config.hidden_size
alt_stream = torch.cuda.Stream()
# Embedding layer
self.embed_tokens = VocabParallelEmbedding(
config.vocab_size,
config.hidden_size,
org_num_embeddings=config.vocab_size,
tp_rank=self.mapping.attn.tp_rank,
tp_size=self.mapping.attn.tp_size,
tp_group=self.mapping.attn.tp_group,
)
layer_cls_by_type = {
"attention": self.ATTENTION_LAYER_CLS,
"linear_attention": self.LINEAR_LAYER_CLS,
}
def get_layer(idx: int, prefix: str):
layer_type = config.layers_block_type[idx]
layer_class = layer_cls_by_type[layer_type]
if layer_type == "attention":
prefix = add_prefix("self_attn", prefix)
else:
prefix = add_prefix("linear_attn", prefix)
return layer_class(
config=config,
mapping=self.mapping,
layer_id=idx,
quant_config=quant_config,
prefix=prefix,
alt_stream=alt_stream,
)
self.layers = make_layers(
config.num_hidden_layers,
get_layer,
prefix=f"{prefix}.layers",
)
# Final normalization
self.norm = GemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def get_input_embeddings(self) -> nn.Embedding:
return self.embed_tokens
@torch.no_grad()
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
ctx: ForwardContext,
out_cache_loc: torch.Tensor,
input_embeds: torch.Tensor | None = None,
pp_proxy_tensors=None,
input_deepstack_embeds: torch.Tensor | None = None,
) -> tuple[torch.Tensor, None]:
# Initialize hidden states
if input_embeds is None:
# Only skip embedding allreduce when the first layer's fused
# allreduce+residual+norm will handle it
if self.layers[0].comm_manager.should_fuse(input_ids.shape[0]):
hidden_states = self.embed_tokens(input_ids, reduce_results=False)
residual = torch.zeros_like(hidden_states)
else:
hidden_states = self.embed_tokens(input_ids)
residual = None
else:
hidden_states = input_embeds
residual = None
# Pass through decoder layers
for layer_idx in range(len(self.layers)):
layer = self.layers[layer_idx]
with get_global_expert_distribution_recorder().with_current_layer(
layer_idx
):
hidden_states, residual = layer(
positions=positions,
hidden_states=hidden_states,
residual=residual,
ctx=ctx,
out_cache_loc=out_cache_loc,
)
# Process deepstack embeddings if provided
if (
input_deepstack_embeds is not None
and input_deepstack_embeds.numel() > 0
and layer_idx < 3
):
sep = self.hidden_size * layer_idx
hidden_states.add_(
input_deepstack_embeds[:, sep : sep + self.hidden_size]
)
# Apply final normalization with optional allreduce fusion
hidden_states, _ = layer.comm_manager.final_norm(
hidden_states, residual, ctx, self.norm
)
return hidden_states, None
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
stacked_params_mapping = [
# (param_name, shard_name, shard_id)
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
("gate_up_proj", "gate_proj", 0),
("gate_up_proj", "up_proj", 1),
# GDN (GatedDeltaNet) linear attention projections
# Split checkpoint format (separate qkv/z/b/a files)
("in_proj_qkvzba.", "in_proj_qkv.", (0, 1, 2)),
("in_proj_qkvzba.", "in_proj_z.", 3),
("in_proj_qkvzba.", "in_proj_b.", 4),
("in_proj_qkvzba.", "in_proj_a.", 5),
# Pre-packed checkpoint format (already merged qkvz and ba)
("in_proj_qkvzba.", "in_proj_qkvz.", (0, 1, 2, 3)),
("in_proj_qkvzba.", "in_proj_ba.", (4, 5)),
]
loaded_params: set[str] = set()
params_dict = dict(self.named_parameters(remove_duplicate=False))
for name, loaded_weight in weights:
if "rotary_emb.inv_freq" in name:
continue
if "mtp" in name:
continue
if "visual" in name:
continue
if "language_model" in name:
name = name.replace(r"model.language_model.", r"model.")
if ".self_attn." in name:
name = name.replace(".self_attn", "")
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
if "mlp.experts" in name:
continue
name = name.replace(weight_name, param_name)
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader")
weight_loader(param, loaded_weight, shard_id)
break
else:
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
logger.warning("Parameter %s not found in params_dict", name)
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
class Qwen3_5MoeForCausalLM(Qwen3_5ForCausalLM):
def __init__(
self,
config: Qwen3_5TextConfig,
mapping: Mapping,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__(
config=config, mapping=mapping, quant_config=quant_config, prefix=prefix
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
stacked_params_mapping = [
# (param_name, shard_name, shard_id)
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
("gate_up_proj", "gate_proj", 0),
("gate_up_proj", "up_proj", 1),
# GDN (GatedDeltaNet) linear attention projections
# Split checkpoint format (separate qkv/z/b/a files)
("in_proj_qkvzba.", "in_proj_qkv.", (0, 1, 2)),
("in_proj_qkvzba.", "in_proj_z.", 3),
("in_proj_qkvzba.", "in_proj_b.", 4),
("in_proj_qkvzba.", "in_proj_a.", 5),
# Pre-packed checkpoint format (already merged qkvz and ba)
("in_proj_qkvzba.", "in_proj_qkvz.", (0, 1, 2, 3)),
("in_proj_qkvzba.", "in_proj_ba.", (4, 5)),
]
# Skip loading extra parameters for GPTQ/nvfp4 models.
ignore_suffixes = (
".bias",
"_bias",
".k_scale",
"_k_scale",
".v_scale",
"_v_scale",
".weight_scale",
"_weight_scale",
".input_scale",
"_input_scale",
)
loaded_params: set[str] = set()
params_dict = dict(self.named_parameters(remove_duplicate=False))
# MoE expert weights, scales, and activation scales are handled
# by the checkpoint loader.
moe_loader = build_moe_checkpoint_loader(
params_dict=params_dict,
expert_schema=ExpertCheckpointSchema(
gate_proj_name="gate_proj",
down_proj_name="down_proj",
up_proj_name="up_proj",
),
fused_schema=ExpertCheckpointSchema(
gate_up_fused_name="gate_up_proj",
down_proj_name="down_proj",
),
num_experts=self.config.num_experts,
ep_rank=self.mapping.moe.ep_rank,
ep_size=self.mapping.moe.ep_size,
)
for name, loaded_weight in weights:
if "rotary_emb.inv_freq" in name:
continue
if "mtp" in name:
continue
if "visual" in name:
continue
if "language_model" in name:
name = name.replace(r"model.language_model.", r"model.")
if ".self_attn." in name:
name = name.replace(".self_attn", "")
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
if "mlp.experts" in name:
continue
name = name.replace(weight_name, param_name)
# Skip loading extra parameters for GPTQ/nvfp4 models.
if name.endswith(ignore_suffixes) and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
break
else:
# Skip loading extra bias for GPTQ models.
if name.endswith((".bias", "_bias")) and name not in params_dict:
continue
if moe_loader.matches(name):
mapped_name = moe_loader.load(name, loaded_weight)
loaded_params.add(mapped_name)
continue
if moe_loader.is_expert_checkpoint_weight(name):
continue
# Skip loading extra parameters for GPTQ/nvfp4 models.
if name.endswith(ignore_suffixes) and name not in params_dict:
continue
if name in params_dict.keys():
param = params_dict[name]
weight_loader = getattr(
param, "weight_loader", default_weight_loader
)
weight_loader(param, loaded_weight)
else:
logger.warning("Parameter %s not found in params_dict", name)
loaded_params.add(name)
return loaded_params
class Qwen3_5ForConditionalGeneration(BaseCausalLM):
model_cls = Qwen3_5ForCausalLM
def __init__(
self,
config: Qwen3_5Config,
mapping: Mapping,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
is_multimodal_active: bool = True,
mm_attention_backend: str | None = None,
):
super().__init__(
config=config.text_config,
mapping=mapping,
quant_config=quant_config,
prefix=prefix,
encoder_only=getattr(config, "encoder_only", False),
)
rope_config = get_rope_parameters(self.config)
self.is_mrope_enabled = "mrope_section" in rope_config
self.is_multimodal_active = is_multimodal_active
if not self.is_multimodal_active:
self.visual = None
self.deepstack_visual_indexes = []
self.num_deepstack_embeddings = 0
self.vision_embedder = None
self.image_encoder = None
self.video_encoder = None
else:
self.visual = Qwen3VLMoeVisionModel(
config.vision_config,
quant_config=None,
norm_eps=getattr(config, "rms_norm_eps", 1e-6),
prefix=add_prefix("model.visual", prefix),
mapping=mapping,
mm_attention_backend=mm_attention_backend,
)
self.deepstack_visual_indexes = self.visual.deepstack_visual_indexes
self.num_deepstack_embeddings = len(self.deepstack_visual_indexes)
# Encoder callables may be swapped to cudagraph wrappers by
# ModelExecutor.
self.vision_embedder = VisionEmbedder()
self.image_encoder = self.get_image_feature
self.video_encoder = self.get_video_feature
def separate_deepstack_embeds(self, embedding: torch.Tensor):
divisor = 1 + self.num_deepstack_embeddings
if embedding.shape[-1] % divisor != 0:
raise ValueError(
f"hidden_state of {embedding.shape} should be divisible by {divisor}"
)
separate_index = self.config.hidden_size
input_embeds = embedding[:, :separate_index]
input_deepstack_embeds = embedding[:, separate_index:]
return input_embeds, input_deepstack_embeds
def pad_input_ids(self, input_ids: list[int], mm_inputs: MultimodalInputs):
return pad_input_tokens(input_ids, mm_inputs)
def get_image_feature(self, items: list[MultimodalDataItem]) -> torch.Tensor:
"""Eager image encode via the ``pre_encode`` / ``forward_blocks`` /
``post_encode`` decomposition the cudagraph wrapper uses, so eager
and captured paths share a single source of truth."""
tokens, grid = self.pre_encode(items)
metadata = self.visual.prepare_metadata(grid)
encoded = self.visual.forward_blocks(tokens, metadata)
return self.post_encode([encoded], grid)
def get_video_feature(self, items: list[MultimodalDataItem]) -> torch.Tensor:
"""Eager video encode; the cudagraph path uses the same pre/post hooks."""
tokens, grid = self.pre_encode(items)
metadata = self.visual.prepare_metadata(grid)
encoded = self.visual.forward_blocks(tokens, metadata)
return self.post_encode([encoded], grid)
def pre_encode(
self,
items: list[MultimodalDataItem],
) -> tuple[torch.Tensor, torch.Tensor]:
"""Eager patch-embed before the captured region; returns ``(tokens, grid)``.
The grid field is selected per item by modality (``video_grid_thw`` for
video, ``image_grid_thw`` otherwise) so a single shared encoder cudagraph
wrapper can serve both image and video batches.
"""
device = self.visual.device
pixel_values = torch.cat(
[item.feature.to(device, non_blocking=True) for item in items], dim=0
).type(self.visual.dtype)
grid = torch.concat(
[
getattr(
item,
(
"video_grid_thw"
if item.modality == Modality.VIDEO
else "image_grid_thw"
),
)
for item in items
],
dim=0,
)
if pixel_values.dim() != 2:
raise ValueError(f"pixel_values must be 2D, got {pixel_values.dim()}D.")
if grid.dim() != 2:
raise ValueError(f"grid must be 2D, got {grid.dim()}D.")
x = self.visual.prepare_patch_embed(pixel_values, grid)
return x, grid
def post_encode(
self, encoder_outs: list[torch.Tensor], grid: torch.Tensor
) -> torch.Tensor:
"""Eager step after the captured region; returns features."""
return torch.cat(encoder_outs, dim=0)
def _build_encoder_cudagraph_wrapper(
self,
mapping,
*,
max_metadata_sequences_per_batch: int | None = None,
metadata_sequence_budget_from_encoder_output_budget: bool = False,
):
# Captured region is ``Qwen3VLMoeVisionModel.forward_blocks`` (blocks +
# deepstack mergers + merger); the merger applies a
# ``spatial_merge_size ** 2`` token reduction, so budgets count
# post-merge tokens while the capture input buffer holds
# ``spatial_merge_size ** 2 * budget`` patches.
adapter = VisionEncoderCudaGraphAdapter(
tower=self.visual,
pre_encode=self.pre_encode,
post_encode=self.post_encode,
out_div=self.visual.spatial_merge_size**2,
merge=self.visual.spatial_merge_size,
input_feature_shape=(1, self.visual.hidden_size),
modality_name="vision",
capture_tp_size=mapping.vision.tp_size,
capture_tp_group=mapping.vision.tp_group,
)
return EncoderCudaGraphWrapper(
adapter=adapter,
budget_range=(64, 4096),
max_metadata_sequences_per_batch=max_metadata_sequences_per_batch,
metadata_sequence_budget_from_encoder_output_budget=(
metadata_sequence_budget_from_encoder_output_budget
),
)
def make_encoder_cudagraph_wrappers(self, mapping):
max_video_metadata_sequences = (
envs.TOKENSPEED_MM_VIDEO_ENCODER_CUDA_GRAPH_MAX_SEQUENCES_PER_BATCH.get()
)
if max_video_metadata_sequences is not None:
max_video_metadata_sequences = max(1, max_video_metadata_sequences)
# Image and video encode through the identical captured region
# (``visual.forward_blocks`` over the same post-merge token buckets), so
# one wrapper serves both -- ``pre_encode`` selects the grid field per
# item by modality. Sharing a single set of budget graphs (rather than
# one set per modality) halves the captured-graph GPU memory. The video
# metadata-sequence policy is the superset (a video batch packs more
# sequences per item than an image batch at a given token budget), so it
# also covers image batches.
shared = self._build_encoder_cudagraph_wrapper(
mapping,
max_metadata_sequences_per_batch=max_video_metadata_sequences,
metadata_sequence_budget_from_encoder_output_budget=(
max_video_metadata_sequences is None
),
)
return {"image_encoder": shared, "video_encoder": shared}
def get_input_embeddings(self):
return self.model.embed_tokens
@torch.no_grad()
def forward(
self,
ctx: ForwardContext,
input_ids: torch.Tensor,
positions: torch.Tensor,
out_cache_loc: torch.Tensor,
**kwargs,
) -> torch.Tensor:
multimodal_context = kwargs.pop("multimodal_context", None)
if (
multimodal_context is None
or not multimodal_context.has_extend_inputs()
or ctx.forward_mode.is_decode_or_idle()
):
return super().forward(
ctx,
input_ids,
positions,
out_cache_loc,
**kwargs,
)
input_embeds, model_kwargs = self.vision_embedder.apply(
input_ids=input_ids,
text_embedding=self.model.get_input_embeddings(),
ctx=multimodal_context,
encoders={
Modality.IMAGE: EncoderSpec(self.image_encoder, deepstack=True),
Modality.VIDEO: EncoderSpec(self.video_encoder, deepstack=True),
},
multimodal_model=self,
is_decode_or_idle=ctx.forward_mode.is_decode_or_idle(),
)
hidden_states, aux_hidden_states = self.model(
input_ids,
positions,
ctx,
out_cache_loc,
input_embeds=input_embeds,
**model_kwargs,
)
logits_metadata = LogitsMetadata.from_forward_context(ctx)
return self.logits_processor(
input_ids,
hidden_states,
self.lm_head,
logits_metadata,
aux_hidden_states,
)
def resolve_model(
self,
config: Qwen3_5TextConfig,
mapping: Mapping,
quant_config: QuantizationConfig | None,
prefix: str,
):
return self.model_cls(
config=config,
mapping=mapping,
quant_config=quant_config,
prefix=add_prefix("model.language_model", prefix),
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
stacked_params_mapping = [
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
("gate_up_proj", "gate_proj", 0),
("gate_up_proj", "up_proj", 1),
# GDN (GatedDeltaNet) linear attention projections
# Split checkpoint format (separate qkv/z/b/a files)
("in_proj_qkvzba.", "in_proj_qkv.", (0, 1, 2)),
("in_proj_qkvzba.", "in_proj_z.", 3),
("in_proj_qkvzba.", "in_proj_b.", 4),
("in_proj_qkvzba.", "in_proj_a.", 5),
# Pre-packed checkpoint format (already merged qkvz and ba)
("in_proj_qkvzba.", "in_proj_qkvz.", (0, 1, 2, 3)),
("in_proj_qkvzba.", "in_proj_ba.", (4, 5)),
]
loaded_params: set[str] = set()
params_dict = dict(self.named_parameters(remove_duplicate=False))
for name, loaded_weight in weights:
if "rotary_emb.inv_freq" in name:
continue
if "mtp" in name:
continue
if not self.is_multimodal_active and "visual" in name:
continue
# Vision-only role: drop every non-visual (LM / lm_head / norm /
# embed) weight up front, before any rename or params_dict lookup,
# so none is routed into a None module. self.model is None here, so
# named_parameters() exposes only visual params.
if getattr(self, "encoder_only", False) and "visual" not in name:
continue
if "language_model" in name:
name = name.replace(r"model.language_model.", r"model.")
if ".self_attn." in name:
name = name.replace(".self_attn", "")
if "visual" in name:
name = name.replace(r"attn.qkv.", r"attn.qkv_proj.")
name = name.replace(r"model.visual.", r"visual.")
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
if "visual" in name or "mlp.experts" in name:
continue
name = name.replace(weight_name, param_name)
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader")
weight_loader(param, loaded_weight, shard_id)
break
else:
if "visual" in name:
name = name.replace(r"attn.qkv.", r"attn.qkv_proj.")
name = name.replace(r"model.visual.", r"visual.")
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
logger.warning("Parameter %s not found in params_dict", name)
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
class Qwen3_5MoeForConditionalGeneration(Qwen3_5ForConditionalGeneration):
"""Qwen3.5 MoE Vision-Language Model."""
model_cls = Qwen3_5MoeForCausalLM
def __init__(
self,
config: Qwen3_5Config,
mapping: Mapping,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
is_multimodal_active: bool = True,
mm_attention_backend: str | None = None,
) -> None:
super().__init__(
config=config,
mapping=mapping,
quant_config=quant_config,
prefix=prefix,
is_multimodal_active=is_multimodal_active,
mm_attention_backend=mm_attention_backend,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
stacked_params_mapping = [
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
("gate_up_proj", "gate_proj", 0),
("gate_up_proj", "up_proj", 1),
# GDN (GatedDeltaNet) linear attention projections
# Split checkpoint format (separate qkv/z/b/a files)
("in_proj_qkvzba.", "in_proj_qkv.", (0, 1, 2)),
("in_proj_qkvzba.", "in_proj_z.", 3),
("in_proj_qkvzba.", "in_proj_b.", 4),
("in_proj_qkvzba.", "in_proj_a.", 5),
# Pre-packed checkpoint format (already merged qkvz and ba)
("in_proj_qkvzba.", "in_proj_qkvz.", (0, 1, 2, 3)),
("in_proj_qkvzba.", "in_proj_ba.", (4, 5)),
]
ignore_suffixes = (
".bias",
"_bias",
".k_scale",
"_k_scale",
".v_scale",
"_v_scale",
".weight_scale",
"_weight_scale",
".input_scale",
"_input_scale",
)
loaded_params: set[str] = set()
params_dict = dict(self.named_parameters(remove_duplicate=False))
# MoE expert weights, scales, and activation scales are handled
# by the checkpoint loader.
moe_loader = build_moe_checkpoint_loader(
params_dict=params_dict,
expert_schema=ExpertCheckpointSchema(
gate_proj_name="gate_proj",
down_proj_name="down_proj",
up_proj_name="up_proj",
),
fused_schema=ExpertCheckpointSchema(
gate_up_fused_name="gate_up_proj",
down_proj_name="down_proj",
),
num_experts=self.config.num_experts,
ep_rank=self.mapping.moe.ep_rank,
ep_size=self.mapping.moe.ep_size,
)
for name, loaded_weight in weights:
if "rotary_emb.inv_freq" in name:
continue
if "mtp" in name:
continue
if not self.is_multimodal_active and "visual" in name:
continue
# Vision-only role: drop every non-visual (LM / lm_head / norm /
# embed / expert) weight up front, before any rename, params_dict
# lookup, or moe_loader.load (which would KeyError on a missing
# expert param). self.model is None here, so named_parameters()
# exposes only visual params.
if getattr(self, "encoder_only", False) and "visual" not in name:
continue
if "language_model" in name:
name = name.replace(r"model.language_model.", r"model.")
if ".self_attn." in name:
name = name.replace(".self_attn", "")
if "visual" in name:
name = name.replace(r"attn.qkv.", r"attn.qkv_proj.")
name = name.replace(r"model.visual.", r"visual.")
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
if "visual" in name:
continue
if "mlp.experts" in name:
continue
name = name.replace(weight_name, param_name)
# Skip loading extra parameters for GPTQ/nvfp4 models.
if name.endswith(ignore_suffixes) and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
break
else:
# Skip loading extra bias for GPTQ models.
if name.endswith((".bias", "_bias")) and name not in params_dict:
continue
if moe_loader.matches(name):
mapped_name = moe_loader.load(name, loaded_weight)
loaded_params.add(mapped_name)
continue
if moe_loader.is_expert_checkpoint_weight(name):
continue
# Skip loading extra parameters for GPTQ/nvfp4 models.
if name.endswith(ignore_suffixes) and name not in params_dict:
continue
if name in params_dict.keys():
param = params_dict[name]
weight_loader = getattr(
param, "weight_loader", default_weight_loader
)
weight_loader(param, loaded_weight)
else:
logger.warning("Parameter %s not found in params_dict", name)
loaded_params.add(name)
return loaded_params
@classmethod
def get_model_config_for_expert_location(cls, config):
text_config = getattr(config, "text_config", config)
return ModelConfigForExpertLocation(
num_layers=text_config.num_hidden_layers,
num_logical_experts=text_config.num_experts,
num_groups=None,
)
@triton.jit
def fused_qkvzba_split_reshape_cat_contiguous_kernel(
mixed_qkv,
z,
b,
a,
mixed_qkvz,
mixed_ba,
stride_qkvz,
stride_ba,
NUM_HEADS_QK: tl.constexpr,
NUM_HEADS_V: tl.constexpr,
HEAD_QK: tl.constexpr,
HEAD_V: tl.constexpr,
):
i_bs, i_qk = tl.program_id(0), tl.program_id(1)
V_PER_GROUP: tl.constexpr = NUM_HEADS_V // NUM_HEADS_QK
# ── Input dimensions ──
TOTAL_Q: tl.constexpr = NUM_HEADS_QK * HEAD_QK
TOTAL_K: tl.constexpr = NUM_HEADS_QK * HEAD_QK
TOTAL_V: tl.constexpr = NUM_HEADS_V * HEAD_V
# ── Output dimensions ──
QKV_DIM_T: tl.constexpr = TOTAL_Q + TOTAL_K + TOTAL_V
# ── Read from input (supports non-contiguous stride) ──
# q for head group i_qk: in the all_q region, offset i_qk * HEAD_QK
blk_q_ptr = mixed_qkvz + i_bs * stride_qkvz + i_qk * HEAD_QK + tl.arange(0, HEAD_QK)
# k for head group i_qk: in the all_k region
blk_k_ptr = (
mixed_qkvz
+ i_bs * stride_qkvz
+ TOTAL_Q
+ i_qk * HEAD_QK
+ tl.arange(0, HEAD_QK)
)
# v for head group i_qk: in the all_v region
blk_v_ptr = (
mixed_qkvz
+ i_bs * stride_qkvz
+ TOTAL_Q
+ TOTAL_K
+ i_qk * V_PER_GROUP * HEAD_V
+ tl.arange(0, V_PER_GROUP * HEAD_V)
)
# z for head group i_qk: in the all_z region
blk_z_ptr = (
mixed_qkvz
+ i_bs * stride_qkvz
+ TOTAL_Q
+ TOTAL_K
+ TOTAL_V
+ i_qk * V_PER_GROUP * HEAD_V
+ tl.arange(0, V_PER_GROUP * HEAD_V)
)
# ── Write to output (identical layout to the interleaved kernel) ──
blk_q_st_ptr = mixed_qkv + i_bs * QKV_DIM_T + i_qk * HEAD_QK + tl.arange(0, HEAD_QK)
blk_k_st_ptr = (
mixed_qkv
+ i_bs * QKV_DIM_T
+ NUM_HEADS_QK * HEAD_QK
+ i_qk * HEAD_QK
+ tl.arange(0, HEAD_QK)
)
blk_v_st_ptr = (
mixed_qkv
+ i_bs * QKV_DIM_T
+ NUM_HEADS_QK * HEAD_QK * 2
+ i_qk * V_PER_GROUP * HEAD_V
+ tl.arange(0, V_PER_GROUP * HEAD_V)
)
blk_z_st_ptr = (
z
+ i_bs * NUM_HEADS_V * HEAD_V
+ i_qk * V_PER_GROUP * HEAD_V
+ tl.arange(0, V_PER_GROUP * HEAD_V)
)
tl.store(blk_q_st_ptr, tl.load(blk_q_ptr))
tl.store(blk_k_st_ptr, tl.load(blk_k_ptr))
tl.store(blk_v_st_ptr, tl.load(blk_v_ptr))
tl.store(blk_z_st_ptr, tl.load(blk_z_ptr))
# ── b and a ──
for i in tl.static_range(V_PER_GROUP):
blk_b_ptr = mixed_ba + i_bs * stride_ba + i_qk * V_PER_GROUP + i
blk_b_st_ptr = b + i_bs * NUM_HEADS_V + i_qk * V_PER_GROUP + i
tl.store(blk_b_st_ptr, tl.load(blk_b_ptr))
for i in tl.static_range(V_PER_GROUP):
blk_a_ptr = mixed_ba + i_bs * stride_ba + NUM_HEADS_V + i_qk * V_PER_GROUP + i
blk_a_st_ptr = a + i_bs * NUM_HEADS_V + i_qk * V_PER_GROUP + i
tl.store(blk_a_st_ptr, tl.load(blk_a_ptr))
def fused_qkvzba_split_reshape_cat_contiguous(
mixed_qkvz,
mixed_ba,
num_heads_qk,
num_heads_v,
head_qk,
head_v,
):
"""Fused split/reshape/cat for Qwen3.5. Supports non-contiguous inputs.
Input layout (per row):
mixed_qkvz: [all_q | all_k | all_v | all_z]
mixed_ba: [all_b | all_a]
Output layout:
mixed_qkv: [all_q | all_k | all_v] (z stripped)
z: [num_v_heads, head_v]
b: [num_v_heads]
a: [num_v_heads]
"""
batch, seq_len = mixed_qkvz.shape[0], 1
qkv_dim_t = num_heads_qk * head_qk * 2 + num_heads_v * head_v
mixed_qkv = torch.empty(
[batch * seq_len, qkv_dim_t],
dtype=mixed_qkvz.dtype,
device=mixed_qkvz.device,
)
z = torch.empty(
[batch * seq_len, num_heads_v, head_v],
dtype=mixed_qkvz.dtype,
device=mixed_qkvz.device,
)
b = torch.empty(
[batch * seq_len, num_heads_v],
dtype=mixed_ba.dtype,
device=mixed_ba.device,
)
a = torch.empty_like(b)
grid = (batch * seq_len, num_heads_qk)
fused_qkvzba_split_reshape_cat_contiguous_kernel[grid](
mixed_qkv,
z,
b,
a,
mixed_qkvz,
mixed_ba,
mixed_qkvz.stride(0),
mixed_ba.stride(0),
num_heads_qk,
num_heads_v,
head_qk,
head_v,
num_warps=1,
num_stages=3,
)
return mixed_qkv, z, b, a
EntryClass = [Qwen3_5MoeForConditionalGeneration, Qwen3_5ForConditionalGeneration]