Files
vllm-omni/vllm_omni/diffusion/layers/mot/mot_row_parallel_linear.py
T
46e0b71423 [Enhancement] Introduce High-Performance MoT (Mixture-of-Tokens) Kernels: Triton Implementation (#3960)
Signed-off-by: Yufeng Zhu <yzhu802@gatech.edu>
Signed-off-by: 汪志鹏 <wangzhipeng628@gmail.com>
Signed-off-by: princepride <wangzhipeng628@gmail.com>
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
Co-authored-by: Yufeng Zhu <yzhu802@gatech.edu>
Co-authored-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 03:08:02 +00:00

362 lines
14 KiB
Python

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.layers.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()
# 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