Files
vllm-omni/vllm_omni/diffusion/layers/mot/ops/mot_gemm.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

909 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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,
)