[Refactor][Diffusion] Move single-model layers into their model directories (#5908)

Signed-off-by: congw729 <115451386+congw729@users.noreply.github.com>
Signed-off-by: Alicia <115451386+congw729@users.noreply.github.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
AliciaandHongsheng Liu authored and GitHub committed 2026-09-20 18:19:24 +08:00
1 parent 9f5521351e
commit d02681bf01
22 files changed
+271 -231

No files matched your search

@@ -1,6 +1,7 @@
# Copyright 2025 Bytedance Ltd. and/or its affiliates.
# Copyright (c) 2024 The Qwen Team and The HuggingFace Inc. team.
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
#
# This file has been modified by ByteDance Ltd. and/or its affiliates.
#
@@ -48,10 +49,10 @@ from vllm_omni.diffusion.distributed.parallel_state import (
get_sp_group,
)
from vllm_omni.diffusion.forward_context import get_forward_context, is_forward_context_available
from vllm_omni.diffusion.layers.mot.mot_layernorm import MoTRMSNorm
from vllm_omni.diffusion.layers.mot.mot_qkv_parallel_linear import MoTQKVParallelLinear
from vllm_omni.diffusion.layers.mot.mot_row_parallel_linear import MoTRowParallelLinear
from vllm_omni.diffusion.layers.rope import RotaryEmbedding
from vllm_omni.diffusion.models.bagel.mot.mot_layernorm import MoTRMSNorm
from vllm_omni.diffusion.models.bagel.mot.mot_qkv_parallel_linear import MoTQKVParallelLinear
from vllm_omni.diffusion.models.bagel.mot.mot_row_parallel_linear import MoTRowParallelLinear
from vllm_omni.diffusion.utils.kv_utils import left_pad_stack
from vllm_omni.model_executor.layers.timestep_embedding import timestep_embedding
Whitespace-only changes.
@@ -0,0 +1,110 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# ruff: noqa: N803, E741
"""Mixture-of-Tokens (MoT) RMS Normalization layer.
Holds two sets of weights (text / gen) and routes tokens to the
appropriate weight based on indices. When text_indices is None the
layer degrades to a standard RMSNorm using self.weight (und mode).
"""
import torch
import torch.nn as nn
from vllm import ir
from vllm_omni.diffusion.layers.custom_op import CustomOp
class MoTRMSNorm(CustomOp):
"""Mixture-of-Tokens RMS Normalization.
In *und* mode (``text_indices is None``), every token is normalised
with ``self.weight`` – exactly like a vanilla RMSNorm.
In *gen* mode, text tokens are normalised with ``self.weight`` and
gen tokens are normalised with ``self.gen_weight``, using a single
fused Triton kernel that avoids the gather / scatter overhead.
"""
def __init__(
self,
hidden_size: int,
head_norm: bool = False,
eps: float = 1e-6,
) -> None:
super().__init__()
self.hidden_size = hidden_size
self.variance_epsilon = eps
self.weight = nn.Parameter(torch.ones(hidden_size))
self.gen_weight = nn.Parameter(torch.ones(hidden_size))
self.head_norm = head_norm
# ------------------------------------------------------------------
# Native (pure-PyTorch) fallback
# ------------------------------------------------------------------
def forward_native(
self,
x: torch.Tensor,
text_indices: torch.Tensor | None = None,
vae_indices: torch.Tensor | None = None,
) -> torch.Tensor:
if text_indices is None:
return self._rms_norm_native(x, self.weight)
output = torch.empty_like(x)
output[text_indices] = self._rms_norm_native(x[text_indices], self.weight)
output[vae_indices] = self._rms_norm_native(x[vae_indices], self.gen_weight)
return output
# ------------------------------------------------------------------
# CUDA fast-path
# ------------------------------------------------------------------
def forward_cuda(
self,
x: torch.Tensor,
text_indices: torch.Tensor | None = None,
vae_indices: torch.Tensor | None = None,
) -> torch.Tensor:
if text_indices is None:
# und mode – delegate to vllm's highly-optimised CUDA kernel
return ir.ops.rms_norm(x, self.weight.data, self.variance_epsilon)
# gen mode – fused MoT Triton kernel
from vllm_omni.diffusion.models.bagel.mot.ops.mot_rms_norm import (
mot_rms_norm,
)
return mot_rms_norm(
x,
self.weight.data,
self.gen_weight.data,
text_indices,
vae_indices,
head_norm=self.head_norm,
eps=self.variance_epsilon,
)
# ------------------------------------------------------------------
# NPU fast-path (not implemented yet; reserved for future optimization)
# ------------------------------------------------------------------
def forward_npu(
self,
x: torch.Tensor,
text_indices: torch.Tensor | None = None,
vae_indices: torch.Tensor | None = None,
) -> torch.Tensor:
return self.forward_native(x, text_indices, vae_indices)
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _rms_norm_native(self, x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
orig_dtype = x.dtype
x = x.float()
variance = x.pow(2).mean(dim=-1, keepdim=True)
x = x * torch.rsqrt(variance + self.variance_epsilon)
return (x * weight.float()).to(orig_dtype)
def extra_repr(self) -> str:
return f"hidden_size={self.hidden_size}, eps={self.variance_epsilon}"
@@ -0,0 +1,367 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
import torch
from torch.nn.parameter import Parameter
from vllm.distributed import (
tensor_model_parallel_all_gather,
)
from vllm.logger import init_logger
from vllm.model_executor.layers.linear import (
WEIGHT_LOADER_V2_SUPPORTED,
QKVParallelLinear,
)
from vllm.model_executor.layers.quantization.base_config import (
QuantizationConfig,
)
from vllm.model_executor.utils import set_weight_attrs
from vllm.platforms import current_platform
from vllm_omni.diffusion.models.bagel.mot.ops.mot_gemm import invoke_mot_gemm
logger = init_logger(__name__)
class MoTQKVParallelLinear(QKVParallelLinear):
"""QKVParallelLinear with Mixture-of-Tokens routing.
Text weights: stored directly on self (self.weight, self.weight_scale, ...),
created through the standard QKVParallelLinear.__init__ process.
VAE weights: stored in the permanent submodule self.gen_exp
(self.gen_exp.weight, ...),
created via quant_method.create_weights(self.gen_exp, ...).
gen_exp.quant_method points to the same quant_method, so that
the vLLM framework’s process_weights_after_loading
can automatically detect and process it.
Forward behavior:
- und mode (text_indices is None): fully reuse super().forward()
- gen mode: call the MoT fused GEMM kernel
"""
def __init__(
self,
hidden_size: int,
head_size: int,
total_num_heads: int,
total_num_kv_heads: int | None = None,
bias: bool = True,
vae_bias: bool = False,
skip_bias_add: bool = False,
params_dtype: torch.dtype | None = None,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
*,
return_bias: bool = True,
disable_tp: bool = False,
v_head_size: int | None = None,
):
# ---- 1) Parent class creates text weights ----
# super().__init__ will do the following:
# quant_method.create_weights(self, ...) → self.weight, self.weight_scale, ...
# QKVParallelLinear hardcodes gather_output=False
super().__init__(
hidden_size,
head_size,
total_num_heads,
total_num_kv_heads,
bias,
skip_bias_add,
params_dtype,
quant_config,
prefix,
return_bias=return_bias,
disable_tp=disable_tp,
v_head_size=v_head_size,
)
# ---- 2) Create vae weights (permanent submodule) ----
if self.quant_method is None:
raise ValueError(
f"quant_method must not be None for MoTQKVParallelLinear (prefix={prefix!r}). "
"Ensure a valid QuantizationConfig is provided or the default UnquantizedLinearMethod is set."
)
# NOTE: We instantiate a bare torch.nn.Module() here as a lightweight namespace
# container to hold the secondary VAE weights.
# This is a design choice for dynamically attaching `quant_method` to the bare module.
# This is slightly unconventional but necessary. It allows vLLM's framework
# (specifically `process_weights_after_loading`) to automatically discover
# and process these secondary weights without requiring a dedicated,
# boilerplate Module subclass just for the VAE pathway.
self.gen_exp = torch.nn.Module()
# ``gen_exp`` participates in the same tensor-parallel layout as this
# layer. vLLM's online quantizers inspect these attributes while
# processing weights (for example, FP8 uses them to share scales across
# TP ranks), so the lightweight container must expose the parallel
# linear metadata rather than only holding parameters.
for attr in (
"tp_rank",
"tp_size",
"input_size",
"input_size_per_partition",
"output_size",
"output_size_per_partition",
):
setattr(self.gen_exp, attr, getattr(self, attr))
# Use the same weight_loader as text
vae_weight_loader = (
self.weight_loader_v2
if self.quant_method.__class__.__name__ in WEIGHT_LOADER_V2_SUPPORTED
else self.weight_loader
)
self.quant_method.create_weights(
layer=self.gen_exp,
input_size_per_partition=self.input_size_per_partition,
output_partition_sizes=self.output_partition_sizes,
input_size=self.input_size,
output_size=self.output_size,
params_dtype=self.params_dtype,
weight_loader=vae_weight_loader,
)
# Make gen_exp discoverable by vLLM framework's process_weights_after_loading
self.gen_exp.quant_method = self.quant_method
# ---- 3) vae bias ----
if vae_bias:
self.gen_exp.bias = Parameter(torch.empty(self.output_size_per_partition, dtype=self.params_dtype))
set_weight_attrs(
self.gen_exp.bias,
{"output_dim": 0, "weight_loader": self.weight_loader},
)
else:
self.gen_exp.register_parameter("bias", None)
self.update_param_tp_status()
# ==================================================================
# Forward
# ==================================================================
def forward(
self,
input_: torch.Tensor,
text_indices: torch.Tensor | None = None,
vae_indices: torch.Tensor | None = None,
) -> torch.Tensor | tuple[torch.Tensor, Parameter | None]:
# ---- und mode: fully reuse parent class (only text path) ----
if text_indices is None:
return super().forward(input_)
# ---- gen mode: fuse MoT GEMM ----
output_parallel = self._mot_gemm_dispatch(input_, text_indices, vae_indices)
# QKVParallelLinear hardcodes gather_output=False, this branch never enters;
# retained for future subclass changes
if self.gather_output and self.tp_size > 1:
output = tensor_model_parallel_all_gather(output_parallel)
else:
output = output_parallel
if not self.return_bias:
return output
if self.skip_bias_add and text_indices is not None and (self.bias is not None or self.gen_exp.bias is not None):
merged_bias = torch.zeros(
output.size(0),
self.output_size_per_partition,
dtype=output.dtype,
device=output.device,
)
if self.bias is not None:
merged_bias[text_indices] = self.bias
if self.gen_exp.bias is not None:
merged_bias[vae_indices] = self.gen_exp.bias
return output, merged_bias
output_bias = self.bias if self.skip_bias_add else None
return output, output_bias
# ==================================================================
# MoT GEMM dispatcher
# ==================================================================
def _mot_gemm_dispatch(
self,
x: torch.Tensor,
text_indices: torch.Tensor,
vae_indices: torch.Tensor,
) -> torch.Tensor:
# Any other backend (ROCm, XPU, TPU, CPU) uses the safe vllm fallback.
if not current_platform.is_cuda():
return self._mot_fallback(x, text_indices, vae_indices)
"""Dispatch to different MoT kernel paths based on weight dtype / quant attributes."""
bias_text = self.bias if not self.skip_bias_add else None
bias_vae = self.gen_exp.bias if not self.skip_bias_add else None
w_text = self.weight
w_vae = self.gen_exp.weight
assert w_text.dtype.is_floating_point == w_vae.dtype.is_floating_point, (
"weight of text expert and image expert should be the same dtype."
)
# w_text.dtype.itemsize >= 2 means bytes_per_element >= 2 (16bits or 32bits)
if w_text.dtype.is_floating_point and w_text.dtype.itemsize >= 2:
return self._mot_gemm_unquantized(
x,
text_indices,
vae_indices,
bias_text,
bias_vae,
)
elif w_text.dtype == torch.float8_e4m3fn:
return self._mot_gemm_fp8_w8a8(
x,
text_indices,
vae_indices,
bias_text,
bias_vae,
)
elif w_text.dtype == torch.int8:
return self._mot_gemm_weight_only(
x,
text_indices,
vae_indices,
bias_text,
bias_vae,
)
else:
return self._mot_fallback(
x,
text_indices,
vae_indices,
)
# ==================================================================
# Implementation of each quantization path
# ==================================================================
def _mot_gemm_unquantized(self, x, text_idx, vae_idx, bias_t, bias_v):
"""BF16/FP16 path."""
N = self.output_size_per_partition
C = torch.zeros(x.size(0), N, dtype=x.dtype, device=x.device)
invoke_mot_gemm(
A=x,
B_text=self.weight.data.t(),
B_vae=self.gen_exp.weight.data.t(),
C=C,
bias_text=bias_t,
bias_vae=bias_v,
text_indices=text_idx,
vae_indices=vae_idx,
A_scale=None,
B_text_scale=None,
B_vae_scale=None,
use_fp8_w8a8=False,
use_int8_w8a8=False,
use_int8_w8a16=False,
use_int4_w4a16=False,
A_per_channel_quant=False,
B_per_channel_quant=False,
)
return C
def _mot_gemm_fp8_w8a8(self, x, text_idx, vae_idx, bias_t, bias_v):
"""FP8 W8A8 path.
1) Activation quantization: reuse vllm's ops.scaled_fp8_quant
2) MoT GEMM: text/vae use different fp8 weights and weight_scale
3) De-quantization: done by MoT kernel's epilogue
"""
from vllm import _custom_ops as ops
x_2d = x.view(-1, x.shape[-1])
input_scale = getattr(self, "input_scale", None)
x_fp8, x_scale = ops.scaled_fp8_quant(
x_2d,
input_scale,
use_per_token_if_dynamic=True,
)
N = self.output_size_per_partition
C = torch.zeros(x.size(0), N, dtype=x.dtype, device=x.device)
invoke_mot_gemm(
A=x_fp8,
B_text=self.weight.data,
B_vae=self.gen_exp.weight.data,
C=C,
bias_text=bias_t,
bias_vae=bias_v,
text_indices=text_idx,
vae_indices=vae_idx,
A_scale=x_scale,
B_text_scale=self.weight_scale.data,
B_vae_scale=self.gen_exp.weight_scale.data,
use_fp8_w8a8=True,
use_int8_w8a8=False,
use_int8_w8a16=False,
use_int4_w4a16=False,
A_per_channel_quant=True,
B_per_channel_quant=False,
)
return C
def _mot_gemm_weight_only(self, x, text_idx, vae_idx, bias_t, bias_v):
"""Weight-Only W8A16 path.
Activation values kept as bf16/fp16, weights are int8 + per-channel scale.
De-quantization done by MoT kernel's epilogue.
"""
N = self.output_size_per_partition
C = torch.zeros(x.size(0), N, dtype=x.dtype, device=x.device)
invoke_mot_gemm(
A=x,
B_text=self.weight.data.t(),
B_vae=self.gen_exp.weight.data.t(),
C=C,
bias_text=bias_t,
bias_vae=bias_v,
text_indices=text_idx,
vae_indices=vae_idx,
A_scale=None,
B_text_scale=self.weight_scale.data,
B_vae_scale=self.gen_exp.weight_scale.data,
use_fp8_w8a8=False,
use_int8_w8a8=False,
use_int8_w8a16=True,
use_int4_w4a16=False,
A_per_channel_quant=False,
B_per_channel_quant=True,
)
return C
def _mot_fallback(self, x, text_idx, vae_idx):
"""Fallback: fall back to gather/scatter + quant_method.apply.
For unsupported quantization types, call standard forward for text/vae tokens.
"""
if self.quant_method is None:
raise ValueError(
"quant_method must not be None in MoTQKVParallelLinear._mot_fallback. "
"This indicates an initialization error."
)
bias_text = self.bias if not self.skip_bias_add else None
bias_vae = self.gen_exp.bias if not self.skip_bias_add else None
output = torch.zeros(
x.size(0),
self.output_size_per_partition,
dtype=x.dtype,
device=x.device,
)
output_text = self.quant_method.apply(self, x[text_idx], bias_text)
output_vae = self.quant_method.apply(
self.gen_exp,
x[vae_idx],
bias_vae,
)
output[text_idx] = output_text
output[vae_idx] = output_vae
return output
@@ -0,0 +1,379 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
import torch
from torch.nn.parameter import Parameter
from vllm.distributed import (
split_tensor_along_last_dim,
tensor_model_parallel_all_reduce,
)
from vllm.model_executor.layers.linear import (
WEIGHT_LOADER_V2_SUPPORTED,
RowParallelLinear,
)
from vllm.model_executor.layers.quantization.base_config import (
QuantizationConfig,
)
from vllm.model_executor.utils import set_weight_attrs
from vllm.platforms import current_platform
from vllm_omni.diffusion.models.bagel.mot.ops.mot_gemm import invoke_mot_gemm
class MoTRowParallelLinear(RowParallelLinear):
"""RowParallelLinear with Mixture-of-Tokens routing.
text weights: directly on self (self.weight, self.weight_scale, ...),
created by RowParallelLinear.__init__ standard process.
vae weights: on permanent submodule self.gen_exp (self.gen_exp.weight, ...),
created by quant_method.create_weights(self.gen_exp, ...).
gen_exp.quant_method points to the same quant_method, enabling
vLLM framework's process_weights_after_loading to automatically
discover and process it.
Forward behavior:
- und mode (text_indices is None): fully reuse super().forward()
- gen mode: call MoT fused GEMM kernel, then execute TP all-reduce
"""
def __init__(
self,
input_size: int,
output_size: int,
bias: bool = True,
vae_bias: bool = False,
input_is_parallel: bool = True,
skip_bias_add: bool = False,
params_dtype: torch.dtype | None = None,
reduce_results: bool = True,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
*,
return_bias: bool = True,
disable_tp: bool = False,
):
# ---- Step 1: Parent class creates text weights ----
# super().__init__ internally calls:
# quant_method.create_weights(self, ...) → self.weight, self.weight_scale, ...
super().__init__(
input_size,
output_size,
bias,
input_is_parallel,
skip_bias_add,
params_dtype,
reduce_results,
quant_config,
prefix,
return_bias=return_bias,
disable_tp=disable_tp,
)
# ---- Step 2: Create vae weights (permanent submodule) ----
if self.quant_method is None:
raise ValueError(
f"quant_method must not be None for MoTRowParallelLinear (prefix={prefix!r}). "
"Ensure a valid QuantizationConfig is provided or the default UnquantizedLinearMethod is set."
)
# NOTE: We instantiate a bare torch.nn.Module() here as a lightweight namespace
# container to hold the secondary VAE weights.
# This is a design choice for dynamically attaching `quant_method` to the bare module.
# This is slightly unconventional but necessary. It allows vLLM's framework
# (specifically `process_weights_after_loading`) to automatically discover
# and process these secondary weights without requiring a dedicated,
# boilerplate Module subclass just for the VAE pathway.
self.gen_exp = torch.nn.Module()
# ``gen_exp`` participates in the same tensor-parallel layout as this
# layer. vLLM's online quantizers inspect these attributes while
# processing weights (for example, FP8 uses them to share scales across
# TP ranks), so the lightweight container must expose the parallel
# linear metadata rather than only holding parameters.
for attr in (
"tp_rank",
"tp_size",
"input_size",
"input_size_per_partition",
"output_size",
"output_size_per_partition",
):
setattr(self.gen_exp, attr, getattr(self, attr))
# Select weight_loader consistent with text
vae_weight_loader = (
self.weight_loader_v2
if self.quant_method.__class__.__name__ in WEIGHT_LOADER_V2_SUPPORTED
else self.weight_loader
)
self.quant_method.create_weights(
layer=self.gen_exp,
input_size_per_partition=self.input_size_per_partition,
output_partition_sizes=self.output_partition_sizes,
input_size=self.input_size,
output_size=self.output_size,
params_dtype=self.params_dtype,
weight_loader=vae_weight_loader,
)
# Enable gen_exp to be automatically discovered by vLLM framework's process_weights_after_loading
self.gen_exp.quant_method = self.quant_method
# ---- Step 3: vae bias ----
# RowParallelLinear's bias is full output_size (not sharded)
if vae_bias:
self.gen_exp.bias = Parameter(torch.empty(self.output_size, dtype=self.params_dtype))
set_weight_attrs(
self.gen_exp.bias,
{"output_dim": 0, "weight_loader": self.weight_loader},
)
else:
self.gen_exp.register_parameter("bias", None)
self.update_param_tp_status()
# ==================================================================
# Forward
# ==================================================================
def forward(
self,
input_: torch.Tensor,
text_indices: torch.Tensor | None = None,
vae_indices: torch.Tensor | None = None,
) -> torch.Tensor | tuple[torch.Tensor, Parameter | None]:
# ---- und mode: fully reuse parent class (only text path) ----
if text_indices is None:
return super().forward(input_)
# ---- gen mode ----
# Handle input_is_parallel (consistent with parent class logic)
if self.input_is_parallel:
input_parallel = input_
else:
split_input = split_tensor_along_last_dim(input_, num_partitions=self.tp_size)
input_parallel = split_input[self.tp_rank].contiguous()
# Fused MoT GEMM
output_parallel = self._mot_gemm_dispatch(input_parallel, text_indices, vae_indices)
# ---- TP communication: all-reduce (consistent with parent class) ----
if self.reduce_results and self.tp_size > 1:
output = tensor_model_parallel_all_reduce(output_parallel)
else:
output = output_parallel
if not self.return_bias:
return output
if self.skip_bias_add and text_indices is not None and (self.bias is not None or self.gen_exp.bias is not None):
# Construct per-token mixed bias
merged_bias = torch.zeros(
output.size(0),
self.output_size_per_partition,
dtype=output.dtype,
device=output.device,
)
if self.bias is not None:
merged_bias[text_indices] = self.bias
if self.gen_exp.bias is not None:
merged_bias[vae_indices] = self.gen_exp.bias
return output, merged_bias
output_bias = self.bias if self.skip_bias_add else None
return output, output_bias
# ==================================================================
# MoT GEMM Dispatcher
# ==================================================================
def _mot_gemm_dispatch(
self,
x: torch.Tensor,
text_indices: torch.Tensor,
vae_indices: torch.Tensor,
) -> torch.Tensor:
"""Dispatch to different MoT kernel paths based on weight dtype / quant attributes."""
# Any other backend (ROCm, XPU, TPU, CPU) uses the safe vllm fallback.
if not current_platform.is_cuda():
return self._mot_fallback(x, text_indices, vae_indices)
# RowParallelLinear: bias only fused into GEMM at rank 0,
# other ranks pass None to avoid duplicate accumulation
bias_text = None if (self.tp_rank > 0 or self.skip_bias_add) else self.bias
bias_vae = None if (self.tp_rank > 0 or self.skip_bias_add) else self.gen_exp.bias
# Determine quantization type by weight dtype, avoid isinstance coupling to specific quant_method
# TODO: Currently does not support online quantization fp8, does not support quantization types involving int4
w_text = self.weight
w_vae = self.gen_exp.weight
assert w_text.dtype.is_floating_point == w_vae.dtype.is_floating_point, (
"weight of text expert and image expert should be the same dtype."
)
# w_text.dtype.itemsize >= 2 means bytes_per_element >= 2 (16bits or 32bits)
if w_text.dtype.is_floating_point and w_text.dtype.itemsize >= 2:
# ---- Path 0: BF16 / FP16 (unquantized) ----
return self._mot_gemm_unquantized(
x,
text_indices,
vae_indices,
bias_text,
bias_vae,
)
elif w_text.dtype == torch.float8_e4m3fn:
# ---- Path 1: FP8 W8A8 ----
return self._mot_gemm_fp8_w8a8(
x,
text_indices,
vae_indices,
bias_text,
bias_vae,
)
elif w_text.dtype == torch.int8:
# ---- Path 2: Weight-Only INT8 W8A16 ----
return self._mot_gemm_weight_only(
x,
text_indices,
vae_indices,
bias_text,
bias_vae,
)
else:
# ---- Fallback: gather/scatter + quant_method.apply ----
return self._mot_fallback(
x,
text_indices,
vae_indices,
)
# ==================================================================
# Specific implementations for each quantization path
# ==================================================================
def _mot_gemm_unquantized(self, x, text_idx, vae_idx, bias_t, bias_v):
"""BF16/FP16 path."""
N = self.output_size_per_partition
C = torch.empty(x.size(0), N, dtype=x.dtype, device=x.device)
invoke_mot_gemm(
A=x,
B_text=self.weight.data.t(),
B_vae=self.gen_exp.weight.data.t(),
C=C,
bias_text=bias_t,
bias_vae=bias_v,
text_indices=text_idx,
vae_indices=vae_idx,
A_scale=None,
B_text_scale=None,
B_vae_scale=None,
use_fp8_w8a8=False,
use_int8_w8a8=False,
use_int8_w8a16=False,
use_int4_w4a16=False,
A_per_channel_quant=False,
B_per_channel_quant=False,
)
return C
def _mot_gemm_fp8_w8a8(self, x, text_idx, vae_idx, bias_t, bias_v):
"""FP8 W8A8 path.
1) Activation quantization: reuse vllm's ops.scaled_fp8_quant
2) MoT GEMM: text/vae each use different fp8 weights and weight_scale
3) Dequantization: completed by MoT kernel internal epilogue
"""
from vllm import _custom_ops as ops
x_2d = x.view(-1, x.shape[-1])
input_scale = getattr(self, "input_scale", None)
x_fp8, x_scale = ops.scaled_fp8_quant(
x_2d,
input_scale,
use_per_token_if_dynamic=True,
)
N = self.output_size_per_partition
C = torch.empty(x.size(0), N, dtype=x.dtype, device=x.device)
# weight has been transposed to (K, N) in process_weights_after_loading
invoke_mot_gemm(
A=x_fp8,
B_text=self.weight.data,
B_vae=self.gen_exp.weight.data,
C=C,
bias_text=bias_t,
bias_vae=bias_v,
text_indices=text_idx,
vae_indices=vae_idx,
A_scale=x_scale,
B_text_scale=self.weight_scale.data,
B_vae_scale=self.gen_exp.weight_scale.data,
use_fp8_w8a8=True,
use_int8_w8a8=False,
use_int8_w8a16=False,
use_int4_w4a16=False,
A_per_channel_quant=True,
B_per_channel_quant=False,
)
return C
def _mot_gemm_weight_only(self, x, text_idx, vae_idx, bias_t, bias_v):
"""Weight-Only W8A16 path.
Activations remain bf16/fp16, weights are int8 + per-channel scale.
MoT kernel internally performs immediate dequantization.
"""
N = self.output_size_per_partition
C = torch.empty(x.size(0), N, dtype=x.dtype, device=x.device)
invoke_mot_gemm(
A=x,
B_text=self.weight.data.t(),
B_vae=self.gen_exp.weight.data.t(),
C=C,
bias_text=bias_t,
bias_vae=bias_v,
text_indices=text_idx,
vae_indices=vae_idx,
A_scale=None,
B_text_scale=self.weight_scale.data,
B_vae_scale=self.gen_exp.weight_scale.data,
use_fp8_w8a8=False,
use_int8_w8a8=False,
use_int8_w8a16=True,
use_int4_w4a16=False,
A_per_channel_quant=False,
B_per_channel_quant=True,
)
return C
def _mot_fallback(self, x, text_idx, vae_idx):
"""Fallback: degrade to gather/scatter + quant_method.apply.
For unsupported quantization types, call standard forward separately for text/vae tokens.
"""
if self.quant_method is None:
raise ValueError(
"quant_method must not be None in MoTRowParallelLinear._mot_fallback. "
"This indicates an initialization error."
)
# RowParallelLinear: bias only fused at rank 0
bias_text = None if (self.tp_rank > 0 or self.skip_bias_add) else self.bias
bias_vae = None if (self.tp_rank > 0 or self.skip_bias_add) else self.gen_exp.bias
output = torch.empty(
x.size(0),
self.output_size_per_partition,
dtype=x.dtype,
device=x.device,
)
# text tokens → standard quant_method (operate on weights on self)
output[text_idx] = self.quant_method.apply(self, x[text_idx], bias_text)
# vae tokens → same quant_method (operate on weights on gen_exp)
output[vae_idx] = self.quant_method.apply(
self.gen_exp,
x[vae_idx],
bias_vae,
)
return output
Whitespace-only changes.
@@ -0,0 +1,911 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# ruff: noqa: N803, E741
import functools
import json
import logging
import os
import pathlib
import re
from typing import Any
import torch
from vllm.triton_utils import tl, triton
logger = logging.getLogger(__name__)
# =====================================================================
# MoT GEMM Config Loading (3-tier: env → built-in → default)
#
# Usage pattern (mirrors vLLM fused_moe):
# 1. Layer invokes invoke_mot_gemm(...) without extra config context.
# 2. invoke_mot_gemm lazily calls
# get_mot_configs(K, N, dtype_str)
# which is @lru_cache'd and lazily loads the JSON on first hit.
# 3. If get_mot_configs returns None, fall back to
# get_mot_default_config(M, N, K, ...)
# =====================================================================
_CONFIGS_DIR = pathlib.Path(__file__).resolve().parent.parent / "configs"
_ENV_CONFIG_FOLDER = "VLLM_TUNED_CONFIG_FOLDER"
def get_device_name() -> str:
"""Sanitized GPU device name, matching mot_linear_benchmarks.py output."""
raw = torch.cuda.get_device_name(0)
name = re.sub(r"[^a-zA-Z0-9]", "", raw.replace(" ", ""))
for prefix in ("NVIDIA", "AMD"):
if name.startswith(prefix):
name = name[len(prefix) :]
# --- Device Aliasing Patch ---
alias_map = {
"A800": "A100",
"H800": "H100",
}
# map A800/H800 to A100/H100 for chinese market
for key, target in alias_map.items():
if key in name:
name = name.replace(key, target)
break
return name
def build_config_filename(device_name: str, dtype_str: str) -> str:
return f"device_name={device_name},dtype={dtype_str}.json"
def _try_load_json(filepath: str) -> dict | None:
if os.path.isfile(filepath):
with open(filepath) as f:
return json.load(f)
return None
@functools.lru_cache
def _load_mot_config_file(dtype_str: str) -> dict | None:
"""Load and cache the full MoT config JSON (one file per device/dtype).
Search order:
1. ``$VLLM_TUNED_CONFIG_FOLDER/device_name=...,dtype=....json``
2. ``vllm_omni/.../mot/configs/device_name=...,dtype=....json``
3. Return ``None`` (caller falls back to ``get_mot_default_config``).
"""
device_name = get_device_name()
filename = build_config_filename(device_name, dtype_str)
config_file_paths: list[str] = []
env_dir = os.environ.get(_ENV_CONFIG_FOLDER)
if env_dir:
config_file_paths.append(str(pathlib.Path(env_dir) / filename))
config_file_paths.append(str(_CONFIGS_DIR / filename))
for path in config_file_paths:
data = _try_load_json(path)
if data is not None:
logger.info("MoT config loaded from %s", path)
return data
logger.warning(
f"\n{'=' * 80}\n"
f" ⚠️ [WARNING] No tuned MoT config found.\n"
f" Searched paths: {', '.join(config_file_paths)}\n"
f" Using conservative default configs which are NOT optimal.\n"
f" Run `python benchmarks/kernels/mot_linear_benchmarks.py --tune` \n"
f" to generate hardware-specific optimal configs.\n"
f"{'=' * 80}\n"
)
return None
@functools.lru_cache
def get_mot_configs(
K: int,
N: int,
dtype_str: str | None = None,
) -> dict[int, dict[str, int]] | None:
"""Return ``{M: tile_config}`` for a given (K, N) shape, or ``None``.
The return value maps an irregular grid of batch sizes (M) to Triton
tile configurations. The caller should pick the entry whose M is
closest to the actual batch size.
Config file is selected by ``device_name + dtype``.
"""
file_data = _load_mot_config_file(dtype_str or "w16a16")
if file_data is None:
return None
shape_entry = file_data.get(f"{K}_{N}")
if shape_entry is None:
logger.warning(
f"\n{'=' * 80}\n"
f" ⚠️ [WARNING] MoT config file found, but NO tuned entry for shape K={K}, N={N}.\n"
f" Using conservative default configs which are NOT optimal for this specific shape.\n"
f" Run `python benchmarks/kernels/mot_linear_benchmarks.py --tune` \n"
f" to generate hardware-specific optimal configs.\n"
f"{'=' * 80}\n"
)
return None
return {int(k): dict(v) for k, v in shape_entry.items() if k != "_comment"}
def get_mot_default_config(
M: int,
N: int,
K: int,
dtype: str | None = None,
block_quant_shape: list[int] | None = None,
) -> dict[str, int]:
"""Conservative fallback config guaranteed to compile on all hardware.
Trades peak performance for universal compatibility (T4 / V100 / A100 /
H100, CUDA & ROCm).
"""
# FP8 block-wise quantization requires strict alignment
if dtype == "fp8_w8a8" and block_quant_shape is not None:
return {
"BLOCK_SIZE_M": 64,
"BLOCK_SIZE_N": block_quant_shape[0],
"BLOCK_SIZE_K": block_quant_shape[1],
"GROUP_SIZE_M": 8,
"num_warps": 4,
"num_stages": 2,
}
# Very small M (tail batches, final feature concat, etc.)
# NOTE If M is not a multiple of the block size (e.g., M=3 with BLOCK_SIZE_M=16),
# the kernel will allocate a full tile. Correctness is guaranteed by boundary masks.
# We tolerate this minor compute/SRAM padding to avoid generating excessive
# Triton compiled variants for extremely small sequence lengths.
if M <= 64:
return {
"BLOCK_SIZE_M": 16 if M <= 16 else 32,
"BLOCK_SIZE_N": 64,
"BLOCK_SIZE_K": 32,
"GROUP_SIZE_M": 8,
"num_warps": 4,
"num_stages": 2,
}
# Standard fallback for typical image-generation M (2048 / 4096 / …)
# SRAM usage: (64*32 + 64*32) * 2 * 2 = 16 KB — safe on decade-old GPUs
return {
"BLOCK_SIZE_M": 64,
"BLOCK_SIZE_N": 64,
"BLOCK_SIZE_K": 32,
"GROUP_SIZE_M": 8,
"num_warps": 4,
"num_stages": 2,
}
def get_best_mot_config(M: int, N: int, K: int, dtype_str: str | None = None) -> tuple[int, dict[str, int]]:
configs = get_mot_configs(K, N, dtype_str)
if configs:
loaded_m_key = min(configs.keys(), key=lambda x: abs(x - M))
return loaded_m_key, configs[loaded_m_key]
else:
return -1, get_mot_default_config(M, N, K, dtype=dtype_str)
# =================================================================
# Part 1: The Router (Routing Component)
# Responsibilities: Handle PID mapping, Text/VAE distribution, indirect index loading, pointer calculation
# =================================================================
@triton.jit
def _get_mot_pointers(
# System Inputs
pid,
# Matrix Pointers
a_ptr,
b_text_ptr,
b_vae_ptr,
bias_text_ptr,
bias_vae_ptr,
scale_a_ptr,
scale_b_text_ptr,
scale_b_vae_ptr,
# Indices & Meta
text_indices_ptr,
vae_indices_ptr,
M_text,
M_vae,
N,
# Strides (need to select based on Text/VAE)
stride_bk_text,
stride_bn_text,
stride_bk_vae,
stride_bn_vae,
# Block Config
BLOCK_SIZE_M: tl.constexpr,
BLOCK_SIZE_N: tl.constexpr,
GROUP_SIZE_M: tl.constexpr,
):
# 1. Calculate Text/VAE task boundaries
num_pid_m_text = tl.cdiv(M_text, BLOCK_SIZE_M)
num_pid_n = tl.cdiv(N, BLOCK_SIZE_N)
# 2. MoT Routing
# Initialize VAE variables first and overwrite for Text
# VAE Path
cur_pid = pid - (num_pid_m_text * num_pid_n)
# Select VAE Pointers
cur_b_ptr = b_vae_ptr
cur_bias_ptr = bias_vae_ptr
cur_scale_b_ptr = scale_b_vae_ptr
cur_indices_ptr = vae_indices_ptr
# Select VAE Strides / Limits
cur_stride_bk = stride_bk_vae
cur_stride_bn = stride_bn_vae
M_limit = M_vae
# Text Path
if pid < num_pid_m_text * num_pid_n:
cur_pid = pid
# Select Text Pointers
cur_b_ptr = b_text_ptr
cur_bias_ptr = bias_text_ptr
cur_scale_b_ptr = scale_b_text_ptr
cur_indices_ptr = text_indices_ptr
# Select Text Strides / Limits
cur_stride_bk = stride_bk_text
cur_stride_bn = stride_bn_text
M_limit = M_text
# 3. Calculate Grid coordinates(grouping)
cur_num_pid_m = tl.cdiv(M_limit, BLOCK_SIZE_M)
num_pid_in_group = GROUP_SIZE_M * num_pid_n
group_id = cur_pid // num_pid_in_group
first_pid_m = group_id * GROUP_SIZE_M
group_size_m_adj = tl.minimum(cur_num_pid_m - first_pid_m, GROUP_SIZE_M)
pid_m = first_pid_m + (cur_pid % group_size_m_adj)
pid_n = (cur_pid % num_pid_in_group) // group_size_m_adj
# 4. Load indirect indices (Indirect Indexing for A)
# Calculate the M range covered by current Block [0, BLOCK_SIZE_M]
offs_m_idx = pid_m * BLOCK_SIZE_M + tl.arange(0, BLOCK_SIZE_M).to(tl.int64)
m_mask = offs_m_idx < M_limit
# Load real row indices from the index array
# for very big K (eg. ffn down proj*TP=1,K=16k), if M is also huge(M>130K)
# loading int32 indices may result in integer overflow when we compute offs_m
real_row_idxs = tl.load(cur_indices_ptr + offs_m_idx, mask=m_mask, other=0).to(tl.int64)
# 5. Calculate N-dimension Offsets
offs_n = pid_n * BLOCK_SIZE_N + tl.arange(0, BLOCK_SIZE_N).to(tl.int64)
n_mask = offs_n < N
return (
pid_m,
pid_n, # Grid Coordinates
real_row_idxs,
m_mask, # M-dim info (Indirect)
offs_n,
n_mask, # N-dim info
M_limit, # Boundary
cur_b_ptr,
cur_bias_ptr, # Selected Pointers
cur_scale_b_ptr, # Selected Scale Pointer
cur_stride_bk,
cur_stride_bn, # Selected Strides
)
# =================================================================
# Part 2: Compute Cores
# Responsibilities: Execute specific Loop structures based on QUANT_TYPE
# =================================================================
# Core A: Standard GEMM (for BF16/FP16 and W8A8)
# Feature: No dequantization inside Loop, Scale is applied after Loop ends
@triton.jit
def _core_standard_gemm(
# Pointers
a_ptr,
b_ptr,
# Offsets & Masks
real_row_idxs,
m_mask,
offs_n,
n_mask,
offs_k,
# Strides
stride_am,
stride_ak,
stride_bk,
stride_bn,
# Quant-Related
scale_a_ptr,
scale_b_ptr,
stride_scale_a,
stride_scale_b,
# Loop Info
K,
BLOCK_SIZE_K: tl.constexpr,
# Configs
BLOCK_SIZE_M: tl.constexpr,
BLOCK_SIZE_N: tl.constexpr,
ACCUMULATOR_DTYPE: tl.constexpr,
IS_W8A8: tl.constexpr,
# Accelerate Configs
EVEN_K: tl.constexpr,
EVEN_N: tl.constexpr,
STRIDE_AK_IS_1: tl.constexpr,
STRIDE_BK_IS_1: tl.constexpr,
STRIDE_BN_IS_1: tl.constexpr,
):
# 1. Stride optimizations (Bypassing compiler limitations for unit strides)
_stride_ak = 1 if STRIDE_AK_IS_1 else stride_ak
_stride_bk = 1 if STRIDE_BK_IS_1 else stride_bk
_stride_bn = 1 if STRIDE_BN_IS_1 else stride_bn
# Pointer initialization (A uses indirect indexing, B uses standard striding)
a_ptrs = a_ptr + (stride_am * real_row_idxs[:, None] + _stride_ak * offs_k[None, :])
b_ptrs = b_ptr + (_stride_bk * offs_k[:, None] + _stride_bn * offs_n[None, :])
accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=ACCUMULATOR_DTYPE)
# 2. Unified Main Loop
# Triton evaluates `constexpr` conditions at compile time, ensuring zero runtime overhead
for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
# Compute K-mask only if needed
if not EVEN_K:
mask_k = offs_k < K - k * BLOCK_SIZE_K
# Load A
if EVEN_K:
a = tl.load(a_ptrs, mask=m_mask[:, None], other=0.0)
else:
a = tl.load(a_ptrs, mask=m_mask[:, None] & mask_k[None, :], other=0.0)
# Load B
if EVEN_K and EVEN_N:
b = tl.load(b_ptrs)
elif not EVEN_K and EVEN_N:
b = tl.load(b_ptrs, mask=mask_k[:, None], other=0.0)
elif EVEN_K and not EVEN_N:
b = tl.load(b_ptrs, mask=n_mask[None, :], other=0.0)
else:
b = tl.load(b_ptrs, mask=mask_k[:, None] & n_mask[None, :], other=0.0)
# Compute & Advance
accumulator = tl.dot(a, b, accumulator, out_dtype=ACCUMULATOR_DTYPE)
a_ptrs += BLOCK_SIZE_K * _stride_ak
b_ptrs += BLOCK_SIZE_K * _stride_bk
# 3. Epilogue (only needed for W8A8)
if IS_W8A8:
accumulator = accumulator.to(tl.float32)
# Load Scale A
scale_a_ptrs = scale_a_ptr + real_row_idxs * stride_scale_a
sa = tl.load(scale_a_ptrs, mask=m_mask, other=1.0)
accumulator = accumulator * sa[:, None]
# Load Scale B
scale_b_ptrs = scale_b_ptr + offs_n * stride_scale_b
sb = tl.load(scale_b_ptrs, mask=n_mask, other=1.0)
accumulator = accumulator * sb[None, :]
return accumulator
# Core B: Weight Only GEMM (for W4A16 / W8A16)
# Feature: Dequantization inside Loop (Dequantize-on-the-fly)
# Currently only supports W8A16
@triton.jit
def _core_weight_only_gemm(
# Pointers
a_ptr,
b_ptr,
# Offsets & Masks
real_row_idxs,
m_mask,
offs_n,
n_mask,
offs_k,
# Strides
stride_am,
stride_ak,
stride_bk,
stride_bn,
# Quant-Related
scale_b_ptr,
stride_scale_b, # 1(per-channel) or 0(per-tensor)
# Loop Info
K,
BLOCK_SIZE_K: tl.constexpr,
# Configs
BLOCK_SIZE_M: tl.constexpr,
BLOCK_SIZE_N: tl.constexpr,
ACCUMULATOR_DTYPE: tl.constexpr,
COMPUTE_DTYPE: tl.constexpr, # bf16 or fp16
WEIGHT_BITS: tl.constexpr, # 4 or 8
# Accelerate Configs
EVEN_K: tl.constexpr,
EVEN_N: tl.constexpr,
STRIDE_AK_IS_1: tl.constexpr,
STRIDE_BK_IS_1: tl.constexpr,
STRIDE_BN_IS_1: tl.constexpr,
):
# Compile-time Check
tl.static_assert(WEIGHT_BITS == 8, "For weight-only, we only support W8A16 at this point")
# 1. Stride optimizations
_stride_ak = 1 if STRIDE_AK_IS_1 else stride_ak
_stride_bk = 1 if STRIDE_BK_IS_1 else stride_bk
_stride_bn = 1 if STRIDE_BN_IS_1 else stride_bn
a_ptrs = a_ptr + (stride_am * real_row_idxs[:, None] + _stride_ak * offs_k[None, :])
b_ptrs = b_ptr + (_stride_bk * offs_k[:, None] + _stride_bn * offs_n[None, :])
accumulator = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_N), dtype=ACCUMULATOR_DTYPE)
# 2. Unified Main Loop
for k in range(0, tl.cdiv(K, BLOCK_SIZE_K)):
if not EVEN_K:
mask_k = offs_k < K - k * BLOCK_SIZE_K
# Load A
if EVEN_K:
a = tl.load(a_ptrs, mask=m_mask[:, None], other=0.0)
else:
a = tl.load(a_ptrs, mask=m_mask[:, None] & mask_k[None, :], other=0.0)
# Load B (Int)
if EVEN_K and EVEN_N:
b_int = tl.load(b_ptrs)
elif not EVEN_K and EVEN_N:
b_int = tl.load(b_ptrs, mask=mask_k[:, None], other=0.0)
elif EVEN_K and not EVEN_N:
b_int = tl.load(b_ptrs, mask=n_mask[None, :], other=0.0)
else:
b_int = tl.load(b_ptrs, mask=mask_k[:, None] & n_mask[None, :], other=0.0)
# --- Dequantize Logic (Type Cast Only) ---
# No multiplication here, correct as long as per-tensor/per-channel scaling (scale_b.shape=(N,))
b_compute = b_int.to(COMPUTE_DTYPE)
# Compute & Advance
accumulator = tl.dot(a, b_compute, accumulator, out_dtype=ACCUMULATOR_DTYPE)
a_ptrs += BLOCK_SIZE_K * _stride_ak
b_ptrs += BLOCK_SIZE_K * _stride_bk
# 3. Epilogue: Apply Scale B safely outside the loop
# Load Scale B
scale_b_ptrs = scale_b_ptr + offs_n * stride_scale_b
sb = tl.load(scale_b_ptrs, mask=n_mask, other=1.0) # shape=(N,)
accumulator = accumulator * sb[None, :]
return accumulator
# =================================================================
# Part 3: Unified Entry Kernel
# Responsibilities: Call Router, statically dispatch Core, store results
# =================================================================
@triton.jit
def mot_unified_gemm_kernel(
# Inputs
a_ptr,
b_text_ptr,
b_vae_ptr,
c_ptr,
bias_text_ptr,
bias_vae_ptr,
text_indices_ptr,
vae_indices_ptr,
# Dimensions & Strides
M_text,
M_vae,
N,
K,
stride_am,
stride_ak,
stride_bk_text,
stride_bn_text,
stride_bk_vae,
stride_bn_vae,
stride_cm,
stride_cn,
# Scales (pass 0 if None)
scale_a_ptr,
scale_b_text_ptr,
scale_b_vae_ptr,
stride_scale_a,
stride_scale_b, # 1(per-channel) or 0(per-tensor)
# Metas
BLOCK_SIZE_M: tl.constexpr,
BLOCK_SIZE_N: tl.constexpr,
BLOCK_SIZE_K: tl.constexpr,
GROUP_SIZE_M: tl.constexpr,
# accelerate config
EVEN_K: tl.constexpr,
EVEN_N: tl.constexpr,
STRIDE_AK_IS_1: tl.constexpr,
STRIDE_BK_IS_1: tl.constexpr,
STRIDE_BN_IS_1: tl.constexpr,
# Quant-related dtypes
ACCUMULATOR_DTYPE: tl.constexpr,
COMPUTE_DTYPE: tl.constexpr,
OUTPUT_DTYPE: tl.constexpr,
# Quant Control
# 0=None, 1=W8A8, 2=W8A16, 3=W4A16
QUANT_TYPE: tl.constexpr,
HAS_BIAS: tl.constexpr = False,
):
pid = tl.program_id(axis=0)
# -----------------------------------------------------------
# 1. Routing Phase (General)
# -----------------------------------------------------------
(
pid_m,
pid_n,
real_row_idxs,
m_mask,
offs_n,
n_mask,
M_limit,
cur_b_ptr,
cur_bias_ptr,
cur_scale_b_ptr,
cur_stride_bk,
cur_stride_bn,
) = _get_mot_pointers(
pid,
a_ptr,
b_text_ptr,
b_vae_ptr,
bias_text_ptr,
bias_vae_ptr,
scale_a_ptr,
scale_b_text_ptr,
scale_b_vae_ptr,
text_indices_ptr,
vae_indices_ptr,
M_text,
M_vae,
N,
stride_bk_text,
stride_bn_text,
stride_bk_vae,
stride_bn_vae,
BLOCK_SIZE_M,
BLOCK_SIZE_N,
GROUP_SIZE_M,
)
offs_k = tl.arange(0, BLOCK_SIZE_K)
# -----------------------------------------------------------
# 2. Compute Phase (Static Dispatch)
# -----------------------------------------------------------
if QUANT_TYPE == 0: # FP16 / BF16 Standard
c = _core_standard_gemm(
a_ptr,
cur_b_ptr,
real_row_idxs,
m_mask,
offs_n,
n_mask,
offs_k,
stride_am,
stride_ak,
cur_stride_bk,
cur_stride_bn,
0,
0,
0,
0,
K,
BLOCK_SIZE_K,
BLOCK_SIZE_M,
BLOCK_SIZE_N,
ACCUMULATOR_DTYPE,
IS_W8A8=False,
EVEN_K=EVEN_K,
EVEN_N=EVEN_N,
STRIDE_AK_IS_1=STRIDE_AK_IS_1,
STRIDE_BK_IS_1=STRIDE_BK_IS_1,
STRIDE_BN_IS_1=STRIDE_BN_IS_1,
)
elif QUANT_TYPE == 1: # W8A8
c = _core_standard_gemm(
a_ptr,
cur_b_ptr,
real_row_idxs,
m_mask,
offs_n,
n_mask,
offs_k,
stride_am,
stride_ak,
cur_stride_bk,
cur_stride_bn,
scale_a_ptr,
cur_scale_b_ptr,
stride_scale_a,
stride_scale_b,
K,
BLOCK_SIZE_K,
BLOCK_SIZE_M,
BLOCK_SIZE_N,
ACCUMULATOR_DTYPE,
IS_W8A8=True,
EVEN_K=EVEN_K,
EVEN_N=EVEN_N,
STRIDE_AK_IS_1=STRIDE_AK_IS_1,
STRIDE_BK_IS_1=STRIDE_BK_IS_1,
STRIDE_BN_IS_1=STRIDE_BN_IS_1,
)
elif QUANT_TYPE == 2 or QUANT_TYPE == 3: # Weight Only
bits = 8 if QUANT_TYPE == 2 else 4
c = _core_weight_only_gemm(
a_ptr,
cur_b_ptr,
real_row_idxs,
m_mask,
offs_n,
n_mask,
offs_k,
stride_am,
stride_ak,
cur_stride_bk,
cur_stride_bn,
cur_scale_b_ptr,
stride_scale_b,
K,
BLOCK_SIZE_K,
BLOCK_SIZE_M,
BLOCK_SIZE_N,
ACCUMULATOR_DTYPE,
COMPUTE_DTYPE,
WEIGHT_BITS=bits,
EVEN_K=EVEN_K,
EVEN_N=EVEN_N,
STRIDE_AK_IS_1=STRIDE_AK_IS_1,
STRIDE_BK_IS_1=STRIDE_BK_IS_1,
STRIDE_BN_IS_1=STRIDE_BN_IS_1,
)
# -----------------------------------------------------------
# 3. Store Phase (General)
# -----------------------------------------------------------
# Bias Add
if HAS_BIAS:
bias = tl.load(cur_bias_ptr + offs_n, mask=n_mask, other=0.0)
c = c + bias[None, :] # reshape to (1, BLOCK_SIZE_N)
# Cast C into the output dtype
c = c.to(OUTPUT_DTYPE)
# Store
offs_cm = real_row_idxs
c_ptrs = c_ptr + stride_cm * offs_cm[:, None] + stride_cn * offs_n[None, :]
# Need to be careful with the Mask here: real_row_idxs may contain 0
# (out-of-bounds padding), but c_mask needs the real boundary
store_mask = m_mask[:, None] & n_mask[None, :]
tl.store(c_ptrs, c, mask=store_mask)
# Define is_weak_contiguous
def is_weak_contiguous(x: torch.Tensor):
strides = x.stride()
sizes = x.shape
is_not_transpose = strides[0] == 1 and (strides[1] >= max(1, sizes[0]))
is_transpose = strides[1] == 1 and (strides[0] >= max(1, sizes[1]))
return is_transpose or is_not_transpose
def invoke_mot_gemm(
# Inputs
A: torch.Tensor,
B_text: torch.Tensor,
B_vae: torch.Tensor,
C: torch.Tensor,
bias_text: torch.Tensor | None,
bias_vae: torch.Tensor | None,
# Indices
text_indices: torch.Tensor,
vae_indices: torch.Tensor,
# Quant Scales (None if disabled)
A_scale: torch.Tensor | None,
B_text_scale: torch.Tensor | None,
B_vae_scale: torch.Tensor | None,
# Quant Flags
use_fp8_w8a8: bool,
use_int8_w8a8: bool,
use_int8_w8a16: bool,
use_int4_w4a16: bool,
# Quant Type
A_per_channel_quant: bool,
B_per_channel_quant: bool,
# Config
config: dict[str, Any] | None = None,
):
# ------ 1. Basic Assertions ------
M = A.size(0)
K = B_text.size(0)
N = B_text.size(1)
if config is None:
if use_fp8_w8a8:
_dtype_str: str | None = "fp8_w8a8"
elif use_int8_w8a16:
_dtype_str = "int8_w8a16"
else:
_dtype_str = None
loaded_m_key, config = get_best_mot_config(M, N, K, _dtype_str)
assert len(A.shape) == 2 and len(C.shape) == 2, (
"The input tensor and output tensor should be flattened to (batch_size*seq_len, hidden_dim)"
)
assert K == A.size(1), "the weights' first dimension should matchinputtensor's last dimension (hidden_dim)"
assert K == B_vae.size(0) and N == B_vae.size(1), (
"the weights dimension for text expert andimage expert should be the same"
)
assert C.size(0) == M and C.size(1) == N, "the output tensor shape is not correct"
M_text = text_indices.size(0)
M_vae = vae_indices.size(0)
assert M_text + M_vae == M, "the length sum of text and image indices should match input tensor's first dimension"
if bias_text is not None:
assert bias_text.dtype == C.dtype, "the bias tensor dtype should match the output tensor dtype"
if bias_vae is not None:
assert bias_vae.dtype == C.dtype, "the bias tensor dtype should match the output tensor dtype"
assert is_weak_contiguous(A)
assert is_weak_contiguous(B_text)
assert is_weak_contiguous(B_vae)
# --- 2. Quantization Logic Translation ---
def triton_dtype(torch_dtype):
if torch_dtype == torch.float8_e4m3fn:
return getattr(tl, "float8e4m3fn", tl.float8e4nv)
elif torch_dtype == torch.float8_e5m2:
return tl.float8e5
return {
torch.float16: tl.float16,
torch.bfloat16: tl.bfloat16,
torch.float32: tl.float32,
torch.int8: tl.int8,
torch.float8_e4m3fn: tl.float8e4nv,
torch.float8_e5m2: tl.float8e5,
}[torch_dtype]
# Determine QUANT_TYPE
# 0=None, 1=W8A8, 2=W8A16, 3=W4A16
quant_type = 0
ACCUMULATOR_DTYPE = tl.float32
COMPUTE_DTYPE = triton_dtype(A.dtype)
OUTPUT_DTYPE = triton_dtype(C.dtype)
if use_int8_w8a8 or use_fp8_w8a8:
quant_type = 1
assert A_scale is not None, "W8A8 requires A_scale"
assert B_text_scale is not None and B_vae_scale is not None, "W8A8 requires B_text_scale and B_vae_scale"
if use_int8_w8a8:
ACCUMULATOR_DTYPE = tl.int32
assert (
A.dtype == torch.int8
and B_text.dtype == torch.int8
and B_vae.dtype == torch.int8
and C.dtype in [torch.float16, torch.bfloat16]
), "if you want to use INT8_W8A8, A should be INT8, B should be INT8, C should be FP16/BF16"
else:
ACCUMULATOR_DTYPE = tl.float32
assert (
A.dtype in [torch.float8_e4m3fn, torch.float8_e5m2]
and B_text.dtype in [torch.float8_e4m3fn, torch.float8_e5m2]
and B_vae.dtype in [torch.float8_e4m3fn, torch.float8_e5m2]
and C.dtype in [torch.float16, torch.bfloat16]
), "if you want to use FP8_W8A8, A should be FP8, B should be FP8, C should be FP16/BF16"
elif use_int8_w8a16:
quant_type = 2
assert B_text_scale is not None and B_vae_scale is not None, "W8A16 requires B_text_scale and B_vae_scale"
ACCUMULATOR_DTYPE = tl.float32
assert (
A.dtype in [torch.float16, torch.bfloat16]
and B_text.dtype == torch.int8
and B_vae.dtype == torch.int8
and C.dtype in [torch.float16, torch.bfloat16]
), "if you want to use INT8_W8A16, A should be FP16/BF16, B should be INT8, C should be FP16/BF16"
elif use_int4_w4a16:
raise NotImplementedError("For weight-only, we only support W8A16 at this point")
# quant_type = 3
# ACCUMULATOR_DTYPE=tl.float32
# accelerate config
EVEN_K = K % config["BLOCK_SIZE_K"] == 0
EVEN_N = N % config["BLOCK_SIZE_N"] == 0
STRIDE_AK_IS_1 = A.stride(1) == 1
STRIDE_BK_IS_1 = (B_text.stride(0) == 1) and (B_vae.stride(0) == 1)
STRIDE_BN_IS_1 = (B_text.stride(1) == 1) and (B_vae.stride(1) == 1)
# bias check
assert (bias_text is None) == (bias_vae is None), (
"Bias must be provided for both Text and VAE simultaneously, or neither."
)
has_bias = bias_text is not None
# --- 3. Grid Calculation ---
def grid(META):
return (
(triton.cdiv(M_text, META["BLOCK_SIZE_M"]) + triton.cdiv(M_vae, META["BLOCK_SIZE_M"]))
* triton.cdiv(N, META["BLOCK_SIZE_N"]),
)
# --- 4. Launch ---
run_config = config.copy()
run_config.update(
{
"QUANT_TYPE": quant_type,
"ACCUMULATOR_DTYPE": ACCUMULATOR_DTYPE,
"COMPUTE_DTYPE": COMPUTE_DTYPE,
"OUTPUT_DTYPE": OUTPUT_DTYPE,
"HAS_BIAS": has_bias,
"EVEN_K": EVEN_K,
"EVEN_N": EVEN_N,
"STRIDE_AK_IS_1": STRIDE_AK_IS_1,
"STRIDE_BK_IS_1": STRIDE_BK_IS_1,
"STRIDE_BN_IS_1": STRIDE_BN_IS_1,
}
)
# Pointers (Handle None -> 0)
p_a_scale = A_scale if A_scale is not None else 0
p_b_text_scale = B_text_scale if B_text_scale is not None else 0
p_b_vae_scale = B_vae_scale if B_vae_scale is not None else 0
p_bias_text = bias_text if bias_text is not None else 0
p_bias_vae = bias_vae if bias_vae is not None else 0
# Quantization granularity
stride_scale_a = 1 if A_per_channel_quant else 0
stride_scale_b = 1 if B_per_channel_quant else 0
mot_unified_gemm_kernel[grid](
# Inputs
A,
B_text,
B_vae,
C,
p_bias_text,
p_bias_vae,
text_indices,
vae_indices,
# Dimensions
M_text,
M_vae,
N,
K,
# Strides
A.stride(0),
A.stride(1),
B_text.stride(0),
B_text.stride(1),
B_vae.stride(0),
B_vae.stride(1),
C.stride(0),
C.stride(1),
# Scales
p_a_scale,
p_b_text_scale,
p_b_vae_scale,
stride_scale_a,
stride_scale_b,
# Config
**run_config,
)
@@ -0,0 +1,286 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# ruff: noqa: N803, E741
import torch
from vllm.triton_utils import tl, triton
@triton.jit
def _mot_rms_norm_kernel(
input_ptr,
text_weight_ptr,
vae_weight_ptr,
text_indices_ptr, # MoT Routing Info
vae_indices_ptr, # MoT Routing Info
M_text, # MoT Routing Info
output_ptr,
input_row_stride,
output_row_stride,
n_cols,
eps,
BLOCK_SIZE: tl.constexpr,
):
"""
input shape: (batch_size*seq_len, hidden_size)
RMSNorm: y = x / sqrt(mean(x^2) + eps) * weight
"""
# Step 0: MoT Routing
pid = tl.program_id(0).to(tl.int64)
# dummy init (must be scalar int64[] to match tl.load return type)
row_idx = tl.cast(0, tl.int64)
weight_ptr = text_weight_ptr
if pid < M_text:
# --- Text Path ---
row_idx = tl.load(text_indices_ptr + pid)
weight_ptr = text_weight_ptr
else:
# --- VAE Path ---
vae_pid = pid - M_text
row_idx = tl.load(vae_indices_ptr + vae_pid)
weight_ptr = vae_weight_ptr
row_start_ptr = input_ptr + row_idx * input_row_stride
output_row_start_ptr = output_ptr + row_idx * output_row_stride
# Step 1: Compute sum of squares in float32 to avoid overflow
sum_sq = tl.zeros([1], dtype=tl.float32)
for col_offset in range(0, n_cols, BLOCK_SIZE):
col_idx = col_offset + tl.arange(0, BLOCK_SIZE)
mask = col_idx < n_cols
vals = tl.load(row_start_ptr + col_idx, mask=mask, other=0.0)
# Convert to float32 for accumulation to prevent overflow
vals_f32 = vals.to(tl.float32)
sq_vals = vals_f32 * vals_f32
sum_sq += tl.sum(tl.where(mask, sq_vals, 0.0))
# Step 2: Compute RMS (root mean square) in float32
mean_sq = sum_sq / n_cols
rms = tl.sqrt(mean_sq + eps)
inv_rms = 1.0 / rms
# Step 3: Normalize and apply weight
for col_offset in range(0, n_cols, BLOCK_SIZE):
col_idx = col_offset + tl.arange(0, BLOCK_SIZE)
mask = col_idx < n_cols
vals = tl.load(row_start_ptr + col_idx, mask=mask, other=0.0)
weight = tl.load(weight_ptr + col_idx, mask=mask, other=1.0)
# Compute in float32 then convert back to input dtype
vals_f32 = vals.to(tl.float32)
weight_f32 = weight.to(tl.float32)
output_f32 = vals_f32 * inv_rms * weight_f32
output = output_f32.to(vals.dtype)
tl.store(output_row_start_ptr + col_idx, output, mask=mask)
@triton.jit
def _mot_rms_norm_qk_kernel(
input_ptr,
text_weight_ptr,
vae_weight_ptr,
text_indices_ptr,
vae_indices_ptr,
M_text,
output_ptr,
stride_tok,
stride_head,
out_stride_tok,
out_stride_head,
num_heads,
head_dim,
eps,
BLOCK_DIM: tl.constexpr, # for head_dim=128,should be 128
SHARED_WEIGHT: tl.constexpr, # weights are shared across heads
):
pid_tok = tl.program_id(0).to(tl.int64)
# MoT Routing
if pid_tok < M_text:
row_idx = tl.load(text_indices_ptr + pid_tok)
weight_ptr = text_weight_ptr
else:
vae_pid = pid_tok - M_text
row_idx = tl.load(vae_indices_ptr + vae_pid)
weight_ptr = vae_weight_ptr
dim_offsets = tl.arange(0, BLOCK_DIM)
mask = dim_offsets < head_dim
# input.shape=(4098,28,128)=(seq_len,num_heads,head_dim)
# weight.shape=(28,128)=(num_heads,head_dim) or (128,)=(head_dim)
tok_in_ptr = input_ptr + row_idx * stride_tok
tok_out_ptr = output_ptr + row_idx * out_stride_tok
# if weight.shape=(128,)
if SHARED_WEIGHT:
weight = tl.load(weight_ptr + dim_offsets, mask=mask, other=1.0)
weight_f32 = weight.to(tl.float32)
# one program loop over all heads
for h in range(num_heads):
head_in_ptr = tok_in_ptr + h * stride_head
head_out_ptr = tok_out_ptr + h * out_stride_head
# if weight.shape=(28,128)
if not SHARED_WEIGHT:
weight_offset = h * head_dim + dim_offsets
weight = tl.load(weight_ptr + weight_offset, mask=mask, other=1.0)
weight_f32 = weight.to(tl.float32)
vals = tl.load(head_in_ptr + dim_offsets, mask=mask, other=0.0)
vals_f32 = vals.to(tl.float32)
sq_vals = vals_f32 * vals_f32
sum_sq = tl.sum(tl.where(mask, sq_vals, 0.0))
mean_sq = sum_sq / head_dim
rms = tl.sqrt(mean_sq + eps)
inv_rms = 1.0 / rms
output_f32 = vals_f32 * inv_rms * weight_f32
tl.store(head_out_ptr + dim_offsets, output_f32.to(vals.dtype), mask=mask)
def mot_rms_norm(
input: torch.Tensor,
text_weight: torch.Tensor,
vae_weight: torch.Tensor,
text_indices: torch.Tensor,
vae_indices: torch.Tensor,
head_norm: bool = False,
eps: float = 1e-6,
block_size: int | None = None,
) -> torch.Tensor:
"""
Compute RMS normalization using Triton kernel.
RMS Norm normalizes the input by the root mean square and scales by weight:
output = input / sqrt(mean(input^2) + eps) * weight
Args:
input: Input tensor of shape (batch_size*seq_len, hidden_size) or (batch_size,seq_len, hidden_size)
text_weight: Weight for text tokens tensor of shape (hidden_size,)
vae_weight: Weight for vae tokens tensor of shape (hidden_size,)
text_indices: indices of text tokens, (batch_size*2,)
vae_indices: indices of vae tokens, (batch_size*(seq_len-2),)
eps: Small constant for numerical stability
Returns:
Tensor with RMS normalization applied along the last dimension
"""
assert input.shape[-1] == text_weight.shape[-1], (
f"Input last dimension ({input.shape[-1]}) must match Text weight dimension ({text_weight.shape[-1]})"
)
assert input.shape[-1] == vae_weight.shape[-1], (
f"Input last dimension ({input.shape[-1]}) must match VAE weight dimension ({vae_weight.shape[-1]})"
)
original_shape = input.shape
text_indices = text_indices.reshape(-1)
vae_indices = vae_indices.reshape(-1)
M_text = text_indices.shape[0]
M_vae = vae_indices.shape[0]
num_tokens = M_text + M_vae
if not head_norm:
input_2d = input.reshape(-1, input.shape[-1])
assert input_2d.shape[0] == num_tokens, (
f"batch_size={input_2d.shape[0]}, len(text_indices)={M_text}, len(vae_indices)={M_vae}"
f"for layer norm, batched_token_length should match the sum of indices_length"
)
input_2d = input_2d.contiguous()
text_indices = text_indices.contiguous()
vae_indices = vae_indices.contiguous()
text_weight = text_weight.contiguous()
vae_weight = vae_weight.contiguous()
n_rows, n_cols = input_2d.shape
output = torch.empty_like(input_2d)
if block_size is None:
block_size = triton.next_power_of_2(n_cols)
block_size = min(block_size, 4096)
num_warps = 4
if block_size >= 2048:
num_warps = 8
elif block_size >= 1024:
num_warps = 4
else:
num_warps = 2
grid = (n_rows,)
_mot_rms_norm_kernel[grid](
input_2d,
text_weight,
vae_weight,
text_indices,
vae_indices,
M_text,
output,
input_2d.stride(0),
output.stride(0),
n_cols,
eps,
BLOCK_SIZE=block_size,
num_warps=num_warps,
)
return output.reshape(original_shape)
else:
# qk norm scenarios:
# input.shape=(batch_size, seq_len,head_num, head_dim) or
# input.shape=(batch_size*seq_len,head_num, head_dim)
assert len(original_shape) > 2, (
"If head_norm=True,input shape be 3D or 3D, last 2 dimensions should be head_num and head_dim"
)
num_heads = input.shape[-2]
head_dim = input.shape[-1]
is_shared_weight = text_weight.dim() == 1
if not is_shared_weight:
assert num_heads == text_weight.shape[0] and num_heads == vae_weight.shape[0], (
"when weights are not shared across heads, the first dimension of "
"weights should be num of heads and match input.shape[-2]"
)
# reshape to 3D
input_3d = input.view(-1, num_heads, head_dim)
input_3d = input_3d.contiguous()
text_indices = text_indices.contiguous()
vae_indices = vae_indices.contiguous()
text_weight = text_weight.contiguous()
vae_weight = vae_weight.contiguous()
output_3d = torch.empty_like(input_3d)
block_dim = triton.next_power_of_2(head_dim)
num_warps = 4 if block_dim > 128 else 2
num_stages = 3
_mot_rms_norm_qk_kernel[(num_tokens,)](
input_3d,
text_weight,
vae_weight,
text_indices,
vae_indices,
M_text,
output_3d,
input_3d.stride(0),
input_3d.stride(1),
output_3d.stride(0),
output_3d.stride(1),
num_heads,
head_dim,
eps,
BLOCK_DIM=block_dim,
SHARED_WEIGHT=is_shared_weight,
num_warps=num_warps,
num_stages=num_stages,
)
return output_3d.view(original_shape)
+81
View File
@@ -0,0 +1,81 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Normalization layer used by the Wan VAE patch."""
import torch
import torch.nn as nn
import torch.nn.functional as F
from vllm_omni.diffusion.layers.custom_op import CustomOp
class RMSNormVAE(CustomOp):
"""Root Mean Square Layer Normalization for Channel-First or Last"""
def __init__(
self,
dim: int,
channel_first: bool = True,
images: bool = True,
bias: bool = False,
epsilon: float = 1e-6,
) -> None:
super().__init__()
broadcastable_dims = (1, 1, 1) if not images else (1, 1)
shape = (dim, *broadcastable_dims) if channel_first else (dim,)
self.channel_first = channel_first
self.scale = dim**0.5
self.gamma = nn.Parameter(torch.ones(shape))
self.bias = nn.Parameter(torch.zeros(shape)) if bias else None
self.epsilon = epsilon
self.gamma_rmsnorm = None
def forward_cuda(
self,
x: torch.Tensor,
) -> torch.Tensor:
return self.forward_native(x)
def forward_hip(
self,
x: torch.Tensor,
) -> torch.Tensor:
return self.forward_native(x)
def forward_npu(
self,
x: torch.Tensor,
) -> torch.Tensor:
import torch_npu
if self.gamma_rmsnorm is None:
self.gamma_rmsnorm = self.gamma.reshape(-1)
if self.channel_first:
x = x.transpose(1, -1)
out = torch_npu.npu_rms_norm(x, self.gamma_rmsnorm, epsilon=self.epsilon)[0].transpose(1, -1)
else:
out = torch_npu.npu_rms_norm(x, self.gamma_rmsnorm, epsilon=self.epsilon)[0]
if self.bias is not None:
out = out + self.bias
return out
def forward_native(
self,
x: torch.Tensor,
) -> torch.Tensor:
out = (
F.normalize(
x,
dim=(1 if self.channel_first else -1),
eps=self.epsilon,
)
* self.scale
* self.gamma
)
if self.bias is not None:
out = out + self.bias
return out
@@ -1,9 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import sys
from vllm_omni.diffusion.layers.norm import RMSNormVAE
from vllm_omni.diffusion.models.wan2_2.norm import RMSNormVAE
def patch_wan_rms_norm():
+142
View File
@@ -0,0 +1,142 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Wan S2V rotary positional embedding layers."""
import torch
from vllm_omni.diffusion.layers.rope import RotaryEmbeddingWan
from vllm_omni.platforms import current_omni_platform
class WanS2VRotaryPosEmbed(torch.nn.Module):
"""Precompute complex-valued RoPE embeddings for S2V multi-grid positions.
Owns the base frequency buffer and provides forward() to compute position
embeddings given hidden_states (for shape) and grid_sizes.
"""
def __init__(self, num_heads: int, head_dim: int, max_seq_len: int = 1024):
super().__init__()
self.num_heads = num_heads
self.head_dim = head_dim
d = head_dim
freqs = torch.cat(
[
self._rope_params(max_seq_len, d - 4 * (d // 6)),
self._rope_params(max_seq_len, 2 * (d // 6)),
self._rope_params(max_seq_len, 2 * (d // 6)),
],
dim=1,
)
self.register_buffer("freqs", freqs.to(torch.complex64), persistent=False)
@staticmethod
@torch.amp.autocast(current_omni_platform.device_type, enabled=False)
def _rope_params(max_seq_len, dim, theta=10000):
if dim % 2 != 0:
raise ValueError(f"dim ({dim}) must be even")
freqs = torch.outer(
torch.arange(max_seq_len), 1.0 / torch.pow(theta, torch.arange(0, dim, 2).to(torch.float64).div(dim))
)
return torch.polar(torch.ones_like(freqs), freqs)
def forward(
self,
hidden_states: torch.Tensor,
grid_sizes: list,
trainable_freqs: torch.Tensor | None = None,
) -> torch.Tensor:
"""Precompute RoPE embeddings for the given grid layout.
Args:
hidden_states: Tensor [B, S, ...] (used for batch/seq shape and device)
grid_sizes: Grid specification (list of [offsets, sizes, totals])
trainable_freqs: Optional trainable frequency overrides for t_f < 0
Returns:
Complex tensor [B, S, 1, head_dim//2] of precomputed position embeddings
"""
b, s = hidden_states.shape[0], hidden_states.shape[1]
c = self.head_dim // 2
device = hidden_states.device
freqs = self.freqs.to(device)
if trainable_freqs is not None:
freqs_input = [freqs, trainable_freqs]
else:
freqs_input = freqs
if isinstance(freqs_input, list):
trainable_f = freqs_input[1]
freqs_split = freqs_input[0]
else:
trainable_f = None
freqs_split = freqs_input
freqs_split = freqs_split.split([c - 2 * (c // 3), c // 3, c // 3], dim=1)
output = torch.empty((b, s, 1, c), device=device, dtype=torch.complex64)
seq_bucket = [0]
if not isinstance(grid_sizes, list):
grid_sizes = [grid_sizes]
for g in grid_sizes:
if not isinstance(g, list):
g = [torch.zeros_like(g), g]
batch_size = g[0].shape[0]
for i in range(batch_size):
f_o, h_o, w_o = g[0][i]
f, h, w = g[1][i]
t_f, t_h, t_w = g[2][i]
seq_f, seq_h, seq_w = f - f_o, h - h_o, w - w_o
seq_len = int(seq_f * seq_h * seq_w)
if seq_len > 0:
if t_f > 0:
assert f_o * f >= 0 and h_o * h >= 0 and w_o * w >= 0
seq_f_int = int(seq_f)
seq_h_int = int(seq_h)
seq_w_int = int(seq_w)
if f_o >= 0:
f_sam = torch.linspace(int(f_o), int(t_f + f_o) - 1, seq_f_int, device=device).long()
else:
f_sam = torch.linspace(int(-f_o), int(-t_f - f_o) + 1, seq_f_int, device=device).long()
h_sam = torch.linspace(int(h_o), int(t_h + h_o) - 1, seq_h_int, device=device).long()
w_sam = torch.linspace(int(w_o), int(t_w + w_o) - 1, seq_w_int, device=device).long()
freqs_0 = torch.index_select(freqs_split[0] if f_o >= 0 else freqs_split[0].conj(), 0, f_sam)
freqs_0 = freqs_0.view(seq_f_int, 1, 1, -1)
freqs_i = torch.cat(
[
freqs_0.expand(seq_f_int, seq_h_int, seq_w_int, -1),
torch.index_select(freqs_split[1], 0, h_sam)
.view(1, seq_h_int, 1, -1)
.expand(seq_f_int, seq_h_int, seq_w_int, -1),
torch.index_select(freqs_split[2], 0, w_sam)
.view(1, 1, seq_w_int, -1)
.expand(seq_f_int, seq_h_int, seq_w_int, -1),
],
dim=-1,
).reshape(seq_len, 1, -1)
elif t_f < 0:
freqs_i = trainable_f.unsqueeze(1)
output[i, seq_bucket[-1] : seq_bucket[-1] + seq_len] = freqs_i
seq_bucket.append(seq_bucket[-1] + seq_len)
return output
class RotaryEmbeddingWanS2V(RotaryEmbeddingWan):
"""Apply RoPE using precomputed complex freqs for Wan S2V main transformer.
Converts complex freqs (from WanS2VRotaryPosEmbed) to cos/sin and delegates
to RotaryEmbeddingWan for platform-optimized application (float32 kernel).
Under TP, freqs has 1 head — broadcasts automatically via cos/sin.
"""
def __init__(self) -> None:
super().__init__(is_neox_style=False, half_head_dim=True)
def forward(self, x: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
freqs_sliced = freqs[:, : x.size(1)]
cos = freqs_sliced.real.to(x.dtype)
sin = freqs_sliced.imag.to(x.dtype)
return super().forward(x, cos, sin)
@@ -32,7 +32,8 @@ from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm_omni.diffusion.attention.layer import Attention
from vllm_omni.diffusion.distributed.sp_plan import SequenceParallelInput, SequenceParallelOutput
from vllm_omni.diffusion.layers.rope import RotaryEmbeddingS2VGrid, RotaryEmbeddingWanS2V, WanS2VRotaryPosEmbed
from vllm_omni.diffusion.layers.rope import RotaryEmbeddingS2VGrid
from vllm_omni.diffusion.models.wan2_2.rope import RotaryEmbeddingWanS2V, WanS2VRotaryPosEmbed
from vllm_omni.platforms import current_omni_platform
from .wan2_2_transformer import DistributedRMSNorm, WanFeedForward