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>
362 lines
14 KiB
Python
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
|