chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
@@ -0,0 +1,178 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/model_executor/layers/activation.py
"""Custom activation functions."""
import math
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
from sglang.multimodal_gen.runtime.platforms import current_platform
_is_cuda = current_platform.is_cuda()
_is_hip = current_platform.is_hip()
_is_npu = current_platform.is_npu()
_is_xpu = current_platform.is_xpu()
if _is_cuda:
from sglang.jit_kernel.activation import silu_and_mul
elif _is_hip or _is_xpu:
from sgl_kernel import silu_and_mul
if _is_npu:
import torch_npu
# TODO (will): remove this dependency
from sglang.multimodal_gen.runtime.layers.custom_op import CustomOp
@CustomOp.register("silu_and_mul")
class SiluAndMul(CustomOp):
"""An activation function for SwiGLU.
The function computes x -> silu(x[:d]) * x[d:] where d = x.shape[-1] // 2.
Shapes:
x: (num_tokens, 2 * d) or (batch_size, seq_len, 2 * d)
return: (num_tokens, d) or (batch_size, seq_len, d)
"""
def __init__(self) -> None:
super().__init__()
def forward_cuda(self, x: torch.Tensor) -> torch.Tensor:
d = x.shape[-1] // 2
output_shape = x.shape[:-1] + (d,)
out = torch.empty(output_shape, dtype=x.dtype, device=x.device)
silu_and_mul(x, out)
return out
def forward_native(self, x: torch.Tensor) -> torch.Tensor:
"""PyTorch-native implementation equivalent to forward()."""
d = x.shape[-1] // 2
return F.silu(x[..., :d]) * x[..., d:]
def forward_npu(self, x: torch.Tensor) -> torch.Tensor:
out = torch_npu.npu_swiglu(x)
return out
def forward_musa(self, x: torch.Tensor) -> torch.Tensor:
return nn.SwishGLU()(x)
def forward_xpu(self, x: torch.Tensor) -> torch.Tensor:
d = x.shape[-1] // 2
output_shape = x.shape[:-1] + (d,)
out = torch.empty(output_shape, dtype=x.dtype, device=x.device)
silu_and_mul(x, out)
return out
@CustomOp.register("gelu_and_mul")
class GeluAndMul(CustomOp):
"""An activation function for GeGLU.
The function computes x -> GELU(x[:d]) * x[d:] where d = x.shape[-1] // 2.
Shapes:
x: (batch_size, seq_len, 2 * d) or (num_tokens, 2 * d)
return: (batch_size, seq_len, d) or (num_tokens, d)
"""
def __init__(self, approximate: str = "none"):
super().__init__()
self.approximate = approximate
if approximate not in ("none", "tanh"):
raise ValueError(f"Unknown approximate mode: {approximate}")
def forward_cuda(self, *args, **kwargs) -> Any:
return self.forward_native(*args, **kwargs)
def forward_npu(self, x: torch.Tensor) -> torch.Tensor:
y_npu, _ = torch_npu.npu_geglu(
x,
dim=-1,
approximate=1 if self.approximate == "tanh" else 0,
activate_left=True,
)
return y_npu
def forward_native(self, x: torch.Tensor) -> torch.Tensor:
"""PyTorch-native implementation equivalent to forward()."""
d = x.shape[-1] // 2
return F.gelu(x[..., :d], approximate=self.approximate) * x[..., d:]
def extra_repr(self) -> str:
return f"approximate={repr(self.approximate)}"
@CustomOp.register("gelu_new")
class NewGELU(CustomOp):
def __init__(self):
super().__init__()
def forward_cuda(self, *args, **kwargs) -> Any:
return self.forward_native(*args, **kwargs)
def forward_xpu(self, *args, **kwargs) -> Any:
return self.forward_native(*args, **kwargs)
def forward_native(self, x: torch.Tensor) -> torch.Tensor:
"""PyTorch-native implementation equivalent to forward()."""
c = math.sqrt(2.0 / math.pi)
return 0.5 * x * (1.0 + torch.tanh(c * (x + 0.044715 * torch.pow(x, 3.0))))
@CustomOp.register("quick_gelu")
class QuickGELU(CustomOp):
# https://github.com/huggingface/transformers/blob/main/src/transformers/activations.py#L90
def __init__(self):
super().__init__()
def forward_cuda(self, *args, **kwargs) -> Any:
return self.forward_native(*args, **kwargs)
def forward_xpu(self, *args, **kwargs) -> Any:
return self.forward_native(*args, **kwargs)
def forward_native(self, x: torch.Tensor) -> torch.Tensor:
"""PyTorch-native implementation equivalent to forward()."""
return x * torch.sigmoid(1.702 * x)
_ACTIVATION_REGISTRY = {
"gelu": nn.GELU,
"gelu_new": NewGELU,
"gelu_pytorch_tanh": lambda: nn.GELU(approximate="tanh"),
"relu": nn.ReLU,
"silu": nn.SiLU,
"quick_gelu": QuickGELU,
}
def get_act_fn(act_fn_name: str) -> nn.Module:
"""Get an activation function by name."""
act_fn_name = act_fn_name.lower()
if act_fn_name not in _ACTIVATION_REGISTRY:
raise ValueError(f"Activation function {act_fn_name!r} is not supported.")
return _ACTIVATION_REGISTRY[act_fn_name]()
_ACTIVATION_AND_MUL_REGISTRY = {
"gelu": GeluAndMul,
"silu": SiluAndMul,
}
def get_act_and_mul_fn(act_fn_name: str) -> nn.Module:
"""Get an activation-and-mul (i.e. SiluAndMul) function by name."""
act_fn_name = act_fn_name.lower()
if act_fn_name not in _ACTIVATION_AND_MUL_REGISTRY:
raise ValueError(f"Activation function {act_fn_name!r} is not supported.")
return _ACTIVATION_AND_MUL_REGISTRY[act_fn_name]()
@@ -0,0 +1,414 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import json
import os
from collections import defaultdict
from typing import Any
import numpy as np
from sglang.multimodal_gen.utils import dict_to_3d_list
def configure_sta(
mode: str = "STA_searching",
layer_num: int = 40,
time_step_num: int = 50,
head_num: int = 40,
**kwargs,
) -> list[list[list[Any]]]:
"""
Configure Sliding Tile Attention (STA) parameters based on the specified mode.
Parameters:
----------
mode : str
The STA mode to use. Options are:
- 'STA_searching': Generate a set of mask candidates for initial search
- 'STA_tuning': Select best mask strategy based on previously saved results
- 'STA_inference': Load and use a previously tuned mask strategy
layer_num: int, number of layers
time_step_num: int, number of timesteps
head_num: int, number of heads
**kwargs : dict
Mode-specific parameters:
For 'STA_searching':
- mask_candidates: list of str, optional, mask candidates to use
- mask_selected: list of int, optional, indices of selected masks
For 'STA_tuning':
- mask_search_files_path: str, required, path to mask search results
- mask_candidates: list of str, optional, mask candidates to use
- mask_selected: list of int, optional, indices of selected masks
- skip_time_steps: int, optional, number of time steps to use full attention (default 12)
- save_dir: str, optional, directory to save mask strategy (default "mask_candidates")
For 'STA_inference':
- load_path: str, optional, path to load mask strategy (default "mask_candidates/mask_strategy.json")
"""
valid_modes = ["STA_searching", "STA_tuning", "STA_inference", "STA_tuning_cfg"]
if mode not in valid_modes:
raise ValueError(f"Mode must be one of {valid_modes}, got {mode}")
if mode == "STA_searching":
# Get parameters with defaults
mask_candidates: list[str] | None = kwargs.get("mask_candidates")
if mask_candidates is None:
raise ValueError("mask_candidates is required for STA_searching mode")
mask_selected: list[int] = kwargs.get(
"mask_selected", list(range(len(mask_candidates)))
)
# Parse selected masks
selected_masks: list[list[int]] = []
for index in mask_selected:
mask = mask_candidates[index]
masks_list = [int(x) for x in mask.split(",")]
selected_masks.append(masks_list)
# Create 3D mask structure with fixed dimensions (t=50, l=60)
masks_3d: list[list[list[list[int]]]] = []
for i in range(time_step_num): # Fixed t dimension = 50
row = []
for j in range(layer_num): # Fixed l dimension = 60
row.append(selected_masks) # Add all masks at each position
masks_3d.append(row)
return masks_3d
elif mode == "STA_tuning":
# Get required parameters
mask_search_files_path: str | None = kwargs.get("mask_search_files_path")
if not mask_search_files_path:
raise ValueError("mask_search_files_path is required for STA_tuning mode")
# Get optional parameters with defaults
mask_candidates_tuning: list[str] | None = kwargs.get("mask_candidates")
if mask_candidates_tuning is None:
raise ValueError("mask_candidates is required for STA_tuning mode")
mask_selected_tuning: list[int] = kwargs.get(
"mask_selected", list(range(len(mask_candidates_tuning)))
)
skip_time_steps_tuning: int | None = kwargs.get("skip_time_steps")
save_dir_tuning: str | None = kwargs.get("save_dir", "mask_candidates")
# Parse selected masks
selected_masks_tuning: list[list[int]] = []
for index in mask_selected_tuning:
mask = mask_candidates_tuning[index]
masks_list = [int(x) for x in mask.split(",")]
selected_masks_tuning.append(masks_list)
# Read JSON results
results = read_specific_json_files(mask_search_files_path)
averaged_results = average_head_losses(results, selected_masks_tuning)
# Add full attention mask for specific cases
full_attention_mask_tuning: list[int] | None = kwargs.get("full_attention_mask")
if full_attention_mask_tuning is not None:
selected_masks_tuning.append(full_attention_mask_tuning)
# Select best mask strategy
timesteps_tuning: int = kwargs.get("timesteps", time_step_num)
if skip_time_steps_tuning is None:
skip_time_steps_tuning = 12
mask_strategy, sparsity, strategy_counts = select_best_mask_strategy(
averaged_results,
selected_masks_tuning,
skip_time_steps_tuning,
timesteps_tuning,
head_num,
)
# Save mask strategy
if save_dir_tuning is not None:
os.makedirs(save_dir_tuning, exist_ok=True)
file_path = os.path.join(
save_dir_tuning, f"mask_strategy_s{skip_time_steps_tuning}.json"
)
with open(file_path, "w") as f:
json.dump(mask_strategy, f, indent=4)
print(f"Successfully saved mask_strategy to {file_path}")
# Print sparsity and strategy counts for information
print(f"Overall sparsity: {sparsity:.4f}")
print("\nStrategy usage counts:")
total_heads = time_step_num * layer_num * head_num # Fixed dimensions
for strategy, count in strategy_counts.items():
print(f"Strategy {strategy}: {count} heads ({count/total_heads*100:.2f}%)")
# Convert dictionary to 3D list with fixed dimensions
mask_strategy_3d = dict_to_3d_list(
mask_strategy, t_max=time_step_num, l_max=layer_num, h_max=head_num
)
return mask_strategy_3d
elif mode == "STA_tuning_cfg":
# Get required parameters for both positive and negative paths
mask_search_files_path_pos: str | None = kwargs.get(
"mask_search_files_path_pos"
)
mask_search_files_path_neg: str | None = kwargs.get(
"mask_search_files_path_neg"
)
save_dir_cfg: str | None = kwargs.get("save_dir")
if (
not mask_search_files_path_pos
or not mask_search_files_path_neg
or not save_dir_cfg
):
raise ValueError(
"mask_search_files_path_pos, mask_search_files_path_neg, and save_dir are required for STA_tuning_cfg mode"
)
# Get optional parameters with defaults
mask_candidates_cfg: list[str] | None = kwargs.get("mask_candidates")
if mask_candidates_cfg is None:
raise ValueError("mask_candidates is required for STA_tuning_cfg mode")
mask_selected_cfg: list[int] = kwargs.get(
"mask_selected", list(range(len(mask_candidates_cfg)))
)
skip_time_steps_cfg: int | None = kwargs.get("skip_time_steps")
# Parse selected masks
selected_masks_cfg: list[list[int]] = []
for index in mask_selected_cfg:
mask = mask_candidates_cfg[index]
masks_list = [int(x) for x in mask.split(",")]
selected_masks_cfg.append(masks_list)
# Read JSON results for both positive and negative paths
pos_results = read_specific_json_files(mask_search_files_path_pos)
neg_results = read_specific_json_files(mask_search_files_path_neg)
# Combine positive and negative results into one list
combined_results = pos_results + neg_results
# Average the combined results
averaged_results = average_head_losses(combined_results, selected_masks_cfg)
# Add full attention mask for specific cases
full_attention_mask_cfg: list[int] | None = kwargs.get("full_attention_mask")
if full_attention_mask_cfg is not None:
selected_masks_cfg.append(full_attention_mask_cfg)
timesteps_cfg: int = kwargs.get("timesteps", time_step_num)
if skip_time_steps_cfg is None:
skip_time_steps_cfg = 12
# Select best mask strategy using combined results
mask_strategy, sparsity, strategy_counts = select_best_mask_strategy(
averaged_results,
selected_masks_cfg,
skip_time_steps_cfg,
timesteps_cfg,
head_num,
)
# Save mask strategy
os.makedirs(save_dir_cfg, exist_ok=True)
file_path = os.path.join(
save_dir_cfg, f"mask_strategy_s{skip_time_steps_cfg}.json"
)
with open(file_path, "w") as f:
json.dump(mask_strategy, f, indent=4)
print(f"Successfully saved mask_strategy to {file_path}")
# Print sparsity and strategy counts for information
print(f"Overall sparsity: {sparsity:.4f}")
print("\nStrategy usage counts:")
total_heads = time_step_num * layer_num * head_num # Fixed dimensions
for strategy, count in strategy_counts.items():
print(f"Strategy {strategy}: {count} heads ({count/total_heads*100:.2f}%)")
# Convert dictionary to 3D list with fixed dimensions
mask_strategy_3d = dict_to_3d_list(
mask_strategy, t_max=time_step_num, l_max=layer_num, h_max=head_num
)
return mask_strategy_3d
else: # STA_inference
# Get parameters with defaults
load_path: str | None = kwargs.get(
"load_path", "mask_candidates/mask_strategy.json"
)
if load_path is None:
raise ValueError("load_path is required for STA_inference mode")
# Load previously saved mask strategy
with open(load_path) as f:
mask_strategy = json.load(f)
# Convert dictionary to 3D list with fixed dimensions
mask_strategy_3d = dict_to_3d_list(
mask_strategy, t_max=time_step_num, l_max=layer_num, h_max=head_num
)
return mask_strategy_3d
# Helper functions
def read_specific_json_files(folder_path: str) -> list[dict[str, Any]]:
"""Read and parse JSON files containing mask search results."""
json_contents: list[dict[str, Any]] = []
# List files only in the current directory (no walk)
files = os.listdir(folder_path)
# Filter files
matching_files = [f for f in files if "mask" in f and f.endswith(".json")]
print(f"Found {len(matching_files)} matching files: {matching_files}")
for file_name in matching_files:
file_path = os.path.join(folder_path, file_name)
with open(file_path) as file:
data = json.load(file)
json_contents.append(data)
return json_contents
def average_head_losses(
results: list[dict[str, Any]], selected_masks: list[list[int]]
) -> dict[str, dict[str, np.ndarray]]:
"""Average losses across all prompts for each mask strategy."""
# Initialize a dictionary to store the averaged results
averaged_losses: dict[str, dict[str, np.ndarray]] = {}
loss_type = "L2_loss"
# Get all loss types (e.g., 'L2_loss')
averaged_losses[loss_type] = {}
for mask in selected_masks:
mask_str = str(mask)
data_shape = np.array(results[0][loss_type][mask_str]).shape
accumulated_data = np.zeros(data_shape)
# Sum across all prompts
for prompt_result in results:
accumulated_data += np.array(prompt_result[loss_type][mask_str])
# Average by dividing by number of prompts
averaged_data = accumulated_data / len(results)
averaged_losses[loss_type][mask_str] = averaged_data
return averaged_losses
def select_best_mask_strategy(
averaged_results: dict[str, dict[str, np.ndarray]],
selected_masks: list[list[int]],
skip_time_steps: int = 12,
timesteps: int = 50,
head_num: int = 40,
) -> tuple[dict[str, list[int]], float, dict[str, int]]:
"""Select the best mask strategy for each head based on loss minimization."""
best_mask_strategy: dict[str, list[int]] = {}
loss_type = "L2_loss"
# Get the shape of time steps and layers
layers = len(averaged_results[loss_type][str(selected_masks[0])][0])
# Counter for sparsity calculation
total_tokens = 0 # total number of masked tokens
total_length = 0 # total sequence length
strategy_counts: dict[str, int] = {str(strategy): 0 for strategy in selected_masks}
full_attn_strategy = selected_masks[-1] # Last strategy is full attention
print(f"Strategy {full_attn_strategy}, skip first {skip_time_steps} steps ")
for t in range(timesteps):
for layer_idx in range(layers):
for h in range(head_num):
if t < skip_time_steps: # First steps use full attention
strategy = full_attn_strategy
else:
# Get losses for this head across all strategies
head_losses = []
for strategy in selected_masks[:-1]: # Exclude full attention
head_losses.append(
averaged_results[loss_type][str(strategy)][t][layer_idx][h]
)
# Find which strategy gives minimum loss
best_strategy_idx = np.argmin(head_losses)
strategy = selected_masks[best_strategy_idx]
best_mask_strategy[f"{t}_{layer_idx}_{h}"] = strategy
# Calculate sparsity
nums = strategy # strategy is already a list of numbers
total_tokens += (
nums[0] * nums[1] * nums[2]
) # masked tokens for chosen strategy
total_length += (
full_attn_strategy[0]
* full_attn_strategy[1]
* full_attn_strategy[2]
)
# Count strategy usage
strategy_counts[str(strategy)] += 1
overall_sparsity = 1 - total_tokens / total_length
return best_mask_strategy, overall_sparsity, strategy_counts
def save_mask_search_results(
mask_search_final_result: list[dict[str, list[float]]],
prompt: str,
mask_strategies: list[str],
output_dir: str = "output/mask_search_result/",
) -> str | None:
if not mask_search_final_result:
print("No mask search results to save")
return None
# Create result dictionary with defaultdict for nested lists
mask_search_dict: dict[str, dict[str, list[list[float]]]] = {
"L2_loss": defaultdict(list),
"L1_loss": defaultdict(list),
}
mask_selected = list(range(len(mask_strategies)))
selected_masks: list[list[int]] = []
for index in mask_selected:
mask = mask_strategies[index]
masks_list = [int(x) for x in mask.split(",")]
selected_masks.append(masks_list)
# Process each mask strategy
for i, mask_strategy in enumerate(selected_masks):
mask_strategy_str = str(mask_strategy)
# Process L2 loss
step_results: list[list[float]] = []
for step_data in mask_search_final_result:
if isinstance(step_data, dict) and "L2_loss" in step_data:
layer_losses = [float(loss) for loss in step_data["L2_loss"]]
step_results.append(layer_losses)
mask_search_dict["L2_loss"][mask_strategy_str] = step_results
step_results = []
for step_data in mask_search_final_result:
if isinstance(step_data, dict) and "L1_loss" in step_data:
layer_losses = [float(loss) for loss in step_data["L1_loss"]]
step_results.append(layer_losses)
mask_search_dict["L1_loss"][mask_strategy_str] = step_results
# Create the output directory if it doesn't exist
os.makedirs(output_dir, exist_ok=True)
# Create a filename based on the first 20 characters of the prompt
filename = prompt[:50].replace(" ", "_")
filepath = os.path.join(output_dir, f"mask_search_{filename}.json")
# Save the results to a JSON file
with open(filepath, "w") as f:
json.dump(mask_search_dict, f, indent=4)
print(f"Successfully saved mask research results to {filepath}")
return filepath
@@ -0,0 +1,38 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.layers.attention.layer import (
DynamicVarlenMaskMeta,
LocalAttention,
UlyssesAttention,
UlyssesAttention_VSA,
USPAttention,
build_varlen_mask_meta,
build_varlen_mask_meta_from_lengths,
build_varlen_mask_meta_from_ranges,
)
from sglang.multimodal_gen.runtime.layers.attention.selector import get_attn_backend
from sglang.multimodal_gen.runtime.layers.attention.turbo_layer import MinimalA2AAttnOp
__all__ = [
"USPAttention",
"LocalAttention",
"DynamicVarlenMaskMeta",
"UlyssesAttention",
"UlyssesAttention_VSA",
"MinimalA2AAttnOp",
"AttentionBackend",
"AttentionMetadata",
"AttentionMetadataBuilder",
# "AttentionState",
"get_attn_backend",
"build_varlen_mask_meta",
"build_varlen_mask_meta_from_lengths",
"build_varlen_mask_meta_from_ranges",
]
@@ -0,0 +1 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
@@ -0,0 +1,207 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import logging
import os
import aiter
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.platforms.aiter import USE_AITER_GFX95
logger = logging.getLogger(__name__)
_use_fp8_attn = os.environ.get("SGLANG_DIFFUSION_AITER_FP8_ATTN", "0") == "1"
_fp8_dtype = torch.float8_e4m3fn
# fmha_fwd_hd128_fp8_gfx950 ASM kernel. Support full MHA with q/k/v head_dim == 128 -- e.g., Wan 2.2 self- and cross-attention.
_FMHA_FP8_HEAD_DIM = 128
if _use_fp8_attn:
logger.info("DiT FP8 attention enabled via SGLANG_DIFFUSION_AITER_FP8_ATTN=1")
def _can_use_fmha_fp8_prefill(
q_head_dim: int,
k_head_dim: int,
v_head_dim: int,
num_heads: int,
num_kv_heads: int,
) -> bool:
"""True if MHA q/k/v head_dim==128 on a gfx950-class arch."""
if not USE_AITER_GFX95:
return False
if num_kv_heads != num_heads:
return False
return q_head_dim == k_head_dim == v_head_dim == _FMHA_FP8_HEAD_DIM
def _fmha_fp8_prefill_attention(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
softmax_scale: float,
is_causal: bool,
q_scale: torch.Tensor,
k_scale: torch.Tensor,
v_scale: torch.Tensor,
) -> torch.Tensor:
"""
FP8 FMHA prefill via aiter.flash_attn_fp8_pertensor_func.
Expects q, k, v as (batch, seqlen, nheads, 128) FP8, contiguous.
"""
def _ensure_fp8_descale(scale: torch.Tensor) -> torch.Tensor:
"""Per-tensor descale as shape (1,) float32 for flash_attn_fp8_pertensor_func."""
return scale.to(dtype=torch.float32).reshape(1).contiguous()
q = q.contiguous()
k = k.contiguous()
v = v.contiguous()
q_descale = _ensure_fp8_descale(q_scale)
k_descale = _ensure_fp8_descale(k_scale)
v_descale = _ensure_fp8_descale(v_scale)
return aiter.flash_attn_fp8_pertensor_func(
q,
k,
v,
q_descale,
k_descale,
v_descale,
causal=is_causal,
softmax_scale=softmax_scale,
window_size=(-1, -1, 0),
)
class AITerBackend(AttentionBackend):
"""
Backend for AITemplate attention implementation.
"""
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.AITER
@staticmethod
def get_impl_cls() -> type["AITerImpl"]:
return AITerImpl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
# AITer backend does not require special metadata.
return AttentionMetadata
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
raise NotImplementedError("AITer backend does not have a metadata builder.")
class AITerImpl(AttentionImpl):
"""
Implementation of attention using AITemplate.
"""
def __init__(
self,
num_heads: int,
head_size: int,
softmax_scale: float,
causal: bool = False,
num_kv_heads: int | None = None,
prefix: str = "",
dropout_p: float = 0.0,
**extra_impl_args,
) -> None:
if num_kv_heads is not None and num_kv_heads != num_heads:
raise NotImplementedError(
"AITer backend does not support Grouped Query Attention yet."
)
self.causal = causal
self.dropout_p = dropout_p
self.softmax_scale = softmax_scale
@torch.compiler.disable
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
"""
Performs attention using one of:
- _fmha_fp8_prefill_attention (FP8, SGLANG_DIFFUSION_AITER_FP8_ATTN=1 when eligible)
- flash_attn_func (BF16, default or FP8 fallback for unsupported shapes)
Args:
query: Query tensor of shape [batch_size, seq_len, num_heads, head_dim]
key: Key tensor of shape [batch_size, seq_len, num_heads, head_dim]
value: Value tensor of shape [batch_size, seq_len, num_heads, head_dim]
attn_metadata: Metadata for the attention operation (unused).
Returns:
Output tensor of shape [batch_size, seq_len, num_heads, head_dim]
"""
if _use_fp8_attn:
if query.dtype != _fp8_dtype:
q_fp8, q_scale = aiter.per_tensor_quant(query, quant_dtype=_fp8_dtype)
k_fp8, k_scale = aiter.per_tensor_quant(key, quant_dtype=_fp8_dtype)
v_fp8, v_scale = aiter.per_tensor_quant(value, quant_dtype=_fp8_dtype)
else:
q_fp8, k_fp8, v_fp8 = query, key, value
one = torch.tensor(1.0, dtype=torch.float32, device=query.device)
q_scale = k_scale = v_scale = one
d_q = q_fp8.shape[-1]
d_k = k_fp8.shape[-1]
d_v = v_fp8.shape[-1]
h_q = q_fp8.shape[2]
h_kv = k_fp8.shape[2]
if _can_use_fmha_fp8_prefill(d_q, d_k, d_v, h_q, h_kv):
return _fmha_fp8_prefill_attention(
q_fp8,
k_fp8,
v_fp8,
softmax_scale=self.softmax_scale,
is_causal=self.causal,
q_scale=q_scale,
k_scale=k_scale,
v_scale=v_scale,
)
logger.warning_once(
"FP8 FMHA prefill unsupported for this shape (need gfx950-class AITER, "
"full MHA, q/k/v head_dim=%d; got q=%d, k=%d, v=%d, num_heads=%d, "
"num_kv_heads=%d). Falling back to BF16.",
_FMHA_FP8_HEAD_DIM,
d_q,
d_k,
d_v,
h_q,
h_kv,
)
# BF16 path
output, _ = aiter.flash_attn_func(
query,
key,
value,
dropout_p=self.dropout_p,
causal=self.causal,
return_attn_probs=False,
return_lse=True,
)
return output
@@ -0,0 +1,81 @@
# SPDX-License-Identifier: Apache-2.0
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
class AITERSageBackend(AttentionBackend):
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.AITER_SAGE
@staticmethod
def get_impl_cls() -> type["AITERSageImpl"]:
return AITERSageImpl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
# AITER Sage backend does not require special metadata.
return AttentionMetadata
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
raise NotImplementedError(
"AITER Sage backend does not have a metadata builder."
)
class AITERSageImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
softmax_scale: float,
causal: bool = False,
num_kv_heads: int | None = None,
prefix: str = "",
dropout_p: float = 0.0,
**extra_impl_args,
) -> None:
try:
from aiter.ops.triton.attention.fav3_sage import fav3_sage_wrapper_func
self.aiter_sage_attn_fn = fav3_sage_wrapper_func
except ImportError:
raise ImportError(
"AITER Sage attention is not available, please update AITER version."
)
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
"""
Performs attention using aiter sage backend.
Args:
query: Query tensor of shape [batch_size, seq_len, head_num, head_dim]
key: Key tensor of shape [batch_size, seq_len, head_num, head_dim]
value: Value tensor of shape [batch_size, seq_len, head_num, head_dim]
attn_metadata: Metadata for the attention operation (unused).
Returns:
Output tensor of shape [batch_size, seq_len, head_num, head_dim]
"""
output = self.aiter_sage_attn_fn(query, key, value)
return output
@@ -0,0 +1,104 @@
from dataclasses import dataclass
from typing import Any
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
@dataclass
class AscendFAMetadata:
pass
class AscendFAMetadataBuilder(AttentionMetadataBuilder):
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build(
self,
**kwargs: dict[str, Any],
) -> AttentionMetadata:
return AscendFAMetadata()
class AscendFABackend(AttentionBackend):
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.FA
@staticmethod
def get_impl_cls() -> type["AscendFAImpl"]:
return AscendFAImpl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
raise NotImplementedError
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
return AscendFAMetadataBuilder
class AscendFAImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
return_softmax_lse: bool = False,
) -> torch.Tensor:
mask = None
num_heads, num_key_value_heads = query.shape[2], key.shape[2]
if self.causal:
seq_len = query.shape[1]
mask = torch.triu(
torch.ones(seq_len, seq_len, device=query.device), diagonal=1
).bool()
# transpose to bs, heads, seq_len, head_dim
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
output, lse = torch.ops.npu.npu_fused_infer_attention_score(
query,
key,
value,
num_heads=num_heads,
num_key_value_heads=num_key_value_heads,
scale=self.softmax_scale,
input_layout="BNSD",
softmax_lse_flag=return_softmax_lse,
atten_mask=mask,
)
output = output.transpose(1, 2)
if return_softmax_lse:
return output, lse
return output
@@ -0,0 +1,179 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/attention/backends/abstract.py
from abc import ABC, abstractmethod
from dataclasses import dataclass, fields
from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar
if TYPE_CHECKING:
pass
import torch
from sglang.kernel_api_logging import wrap_method_with_debug_kernel_once
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
class AttentionBackend(ABC):
"""Abstract class for attention backends."""
# For some attention backends, we allocate an output tensor before
# calling the custom op. When piecewise cudagraph is enabled, this
# makes sure the output tensor is allocated inside the cudagraph.
accept_output_buffer: bool = False
@staticmethod
@abstractmethod
def get_enum() -> AttentionBackendEnum:
raise NotImplementedError
@staticmethod
@abstractmethod
def get_impl_cls() -> type["AttentionImpl"]:
raise NotImplementedError
@staticmethod
@abstractmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
raise NotImplementedError
# @staticmethod
# @abstractmethod
# def get_state_cls() -> Type["AttentionState"]:
# raise NotImplementedError
# @classmethod
# def make_metadata(cls, *args, **kwargs) -> "AttentionMetadata":
# return cls.get_metadata_cls()(*args, **kwargs)
@staticmethod
@abstractmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
return None
@dataclass
class AttentionMetadata:
"""Attention metadata for prefill and decode batched together."""
# Current step of diffusion process
current_timestep: int
def asdict_zerocopy(self, skip_fields: set[str] | None = None) -> dict[str, Any]:
"""Similar to dataclasses.asdict, but avoids deepcopying."""
if skip_fields is None:
skip_fields = set()
# Note that if we add dataclasses as fields, they will need
# similar handling.
return {
field.name: getattr(self, field.name)
for field in fields(self)
if field.name not in skip_fields
}
T = TypeVar("T", bound=AttentionMetadata)
class AttentionMetadataBuilder(ABC, Generic[T]):
"""Abstract class for attention metadata builders."""
@abstractmethod
def __init__(self) -> None:
"""Create the builder, remember some configuration and parameters."""
raise NotImplementedError
@abstractmethod
def prepare(self) -> None:
"""Prepare for one batch."""
raise NotImplementedError
@abstractmethod
def build(
self,
**kwargs: dict[str, Any],
) -> AttentionMetadata:
"""Build attention metadata with on-device tensors."""
raise NotImplementedError
class AttentionLayer(Protocol):
_k_scale: torch.Tensor
_v_scale: torch.Tensor
_k_scale_float: float
_v_scale_float: float
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
kv_cache: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor: ...
class AttentionImpl(ABC, Generic[T]):
@abstractmethod
def __init__(
self,
num_heads: int,
head_size: int,
softmax_scale: float,
causal: bool = False,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
raise NotImplementedError
def preprocess_qkv(self, qkv: torch.Tensor, attn_metadata: T) -> torch.Tensor:
"""Preprocess QKV tensor before performing attention operation.
Default implementation returns the tensor unchanged.
Subclasses can override this to implement custom preprocessing
like reshaping, tiling, scaling, or other transformations.
Called AFTER all_to_all for distributed attention
"""
return qkv
def postprocess_output(
self,
output: torch.Tensor,
attn_metadata: T,
) -> torch.Tensor:
"""Postprocess the output tensor after the attention operation.
Default implementation returns the tensor unchanged.
Subclasses can override this to implement custom postprocessing
like untiling, scaling, or other transformations.
Called BEFORE all_to_all for distributed attention
"""
return output
@abstractmethod
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: T,
) -> torch.Tensor:
raise NotImplementedError
def wrap_attention_impl_forward(attn_impl: AttentionImpl) -> AttentionImpl:
return wrap_method_with_debug_kernel_once(
attn_impl,
"forward",
op_name=f"diffusion.attn_impl.{attn_impl.__class__.__name__}.forward",
)
@@ -0,0 +1,279 @@
from dataclasses import dataclass
from typing import Any
import attentions # noqa: F401
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.laser_attn import (
LaserAttentionBackend,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
BSA_BLOCK_SIZE = 128
class BlockSparseAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.BLOCK_SPARSE_ATTN
@staticmethod
def get_impl_cls() -> type["BlockSparseAttentionImpl"]:
return BlockSparseAttentionImpl
@staticmethod
def get_metadata_cls() -> type["BlockSparseAttentionMetadata"]:
return BlockSparseAttentionMetadata
@staticmethod
def get_builder_cls() -> type["BlockSparseAttentionMetadataBuilder"]:
return BlockSparseAttentionMetadataBuilder
@dataclass
class BlockSparseAttentionMetadata(AttentionMetadata):
current_timestep: int
skip_first_steps: int
sparsity: float
block_frame_stride: int
class BlockSparseAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build(
self,
current_timestep: int,
skip_first_steps: int,
sparsity: float,
raw_latent_shape: list[int],
patch_size: tuple[int, int, int],
**kwargs: dict[str, Any],
) -> BlockSparseAttentionMetadata:
"""
Builds BlockSparseAttention metadata.
Args:
current_timestep: The current diffusion timestep.
skip_first_steps: Number of initial timesteps to skip before applying
sparsity. Must be nonnegative.
sparsity: Fraction of tokens to drop (blockwise) in the block sparse
attention mechanism. Must be in the range [0.0, 1.0).
raw_latent_shape: Shape of the latent tensor before patching.
patch_size: Patch size as (T, height, width). Only the height
and width components are used to divide the latent dimensions.
**kwargs: Additional keyword arguments (ignored, but accepted for
compatibility with base class or calling conventions).
Returns:
BlockSparseAttentionMetadata
Note:
The `block_frame_stride` is needed to set the first blocks to be nonsparse.
"""
if not (skip_first_steps >= 0 and 0.0 <= sparsity < 1.0):
raise ValueError(
(
"Invalid attention metadata values."
f"Sparsity should be in [0, 1), skip_first_steps should be non-negative."
f"Got sparsity={sparsity}, skip_first_steps={skip_first_steps}"
)
)
if sparsity == 0.0:
logger.warning(
(
"Sparsity is set to 0.0, which means no tokens will be dropped."
"For better performance use Laser Attention or increase sparsity."
)
)
if len(raw_latent_shape) >= 5:
latent_height, latent_width = raw_latent_shape[3:5]
else:
latent_height, latent_width = raw_latent_shape[-2:]
latent_height //= patch_size[1]
latent_width //= patch_size[2]
frame_stride = latent_height * latent_width
block_frame_stride = (frame_stride + BSA_BLOCK_SIZE - 1) // BSA_BLOCK_SIZE
return BlockSparseAttentionMetadata(
current_timestep=current_timestep,
skip_first_steps=skip_first_steps,
sparsity=sparsity,
block_frame_stride=block_frame_stride,
)
class BlockSparseAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
self.num_heads = num_heads
self.num_kv_heads = num_kv_heads or num_heads
self.block_size = BSA_BLOCK_SIZE
self.stride = 8
self.default_tokens = 214748647
self.laser_attn_impl = LaserAttentionBackend.get_impl_cls()(
num_heads,
head_size,
causal,
softmax_scale,
num_kv_heads,
prefix,
**extra_impl_args,
)
def _get_estimate_mask(
self,
query: torch.Tensor,
key: torch.Tensor,
sparsity: float,
) -> tuple[torch.Tensor, torch.Tensor]:
return torch.ops.attentions.sparse_block_estimate(
query=query,
key=key,
actual_seq_lengths=None,
actual_seq_lengths_kv=None,
input_layout="BNSD",
stride=self.stride,
sparse_size=self.block_size,
num_heads=query.shape[1],
num_key_value_heads=key.shape[1],
scale_value=self.softmax_scale / self.stride,
threshold=1.0,
causal=self.causal,
keep_sink=True,
keep_recent=True,
row_sparse=1.0 - sparsity,
)
def _block_sparse_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
smask: torch.Tensor,
sct: torch.Tensor,
) -> torch.Tensor:
return torch.ops.attentions.block_sparse_attention(
query=query,
key=key,
value=value,
sparse_mask=smask,
sparse_count_table=sct,
input_layout="BNSD",
sparse_size=self.block_size,
num_heads=query.shape[1],
num_key_value_heads=key.shape[1],
scale_value=self.softmax_scale,
causal=self.causal,
inner_precise=1,
pre_tokens=self.default_tokens,
next_tokens=self.default_tokens,
actual_seq_lengths=None,
actual_seq_lengths_kv=None,
)
def _get_smask(
self,
query: torch.Tensor,
key: torch.Tensor,
block_frame_stride: int,
sparsity: float,
) -> tuple[torch.Tensor, torch.Tensor]:
smask, sct = self._get_estimate_mask(
query,
key,
sparsity,
)
seq_len = smask.shape[2]
# Set the first blocks to be non-sparse to ensure the quality of the first few steps
smask[:, :, :block_frame_stride, :seq_len] = 1
smask[:, :, :seq_len, :block_frame_stride] = 1
smask = smask.to(torch.int8)
sct = smask.sum(dim=-1, dtype=torch.int32)
return smask, sct
def _adaptive_block_sparse_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
block_frame_stride: int,
sparsity: float,
) -> torch.Tensor:
# TODO Currently implementation for BSND input layout has quality issues
# When the implementation is improved, transposes can be removed
q = query.permute(0, 2, 1, 3).contiguous()
k = key.permute(0, 2, 1, 3).contiguous()
v = value.permute(0, 2, 1, 3).contiguous()
smask, sct = self._get_smask(
q,
k,
block_frame_stride,
sparsity,
)
output = self._block_sparse_attention(q, k, v, smask, sct)
output = output.permute(0, 2, 1, 3).contiguous()
return output
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
if attn_metadata.current_timestep < attn_metadata.skip_first_steps:
output = self.laser_attn_impl.forward(
query,
key,
value,
attn_metadata,
)
else:
output = self._adaptive_block_sparse_attention(
query,
key,
value,
attn_metadata.block_frame_stride,
attn_metadata.sparsity,
)
return output
@@ -0,0 +1,445 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple
import torch
from sglang.jit_kernel.flash_attention import flash_attn_varlen_func
from sglang.multimodal_gen.runtime.layers.utils import register_custom_op
from sglang.multimodal_gen.runtime.platforms import (
AttentionBackendEnum,
)
def maybe_contiguous(x: Optional[torch.Tensor]) -> Optional[torch.Tensor]:
return x.contiguous() if x is not None and x.stride(-1) != 1 else x
# -----------------------------
# Fake implementations for schema / tracing
# custom op schema requires FIXED return structure.
# We provide TWO ops:
# 1) out-only op: always returns Tensor
# 2) out+lse op: always returns Tuple[Tensor, Tensor]
# -----------------------------
def flash_attn_varlen_func_fake_out(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k: Optional[torch.Tensor] = None,
max_seqlen_q: Optional[int] = None,
max_seqlen_k: Optional[int] = None,
seqused_q: Optional[torch.Tensor] = None,
seqused_k: Optional[torch.Tensor] = None,
page_table: Optional[torch.Tensor] = None,
softmax_scale: Optional[float] = None,
causal: bool = False,
qv: Optional[torch.Tensor] = None,
q_descale: Optional[torch.Tensor] = None,
k_descale: Optional[torch.Tensor] = None,
v_descale: Optional[torch.Tensor] = None,
window_size: Optional[List[int]] = None,
attention_chunk: int = 0,
softcap: float = 0.0,
num_splits: int = 1,
pack_gqa: Optional[bool] = None,
sm_margin: int = 0,
return_softmax_lse: bool = False,
sinks: Optional[torch.Tensor] = None,
ver: int = 4,
) -> torch.Tensor:
assert ver == 4, "only support flash attention v4"
q, k, v = [maybe_contiguous(t) for t in (q, k, v)]
num_head, head_dim = q.shape[-2:]
if cu_seqlens_q is None:
batch_size, seqlen_q = q.shape[:2]
else:
batch_size = cu_seqlens_q.shape[0] - 1
seqlen_q = None
head_dim_v = v.shape[-1]
if cu_seqlens_q is not None:
assert cu_seqlens_q.shape == (
batch_size + 1,
), "cu_seqlens_q must have shape (batch_size + 1,)"
assert cu_seqlens_q.dtype == torch.int32, "cu_seqlens_q must be int32"
assert cu_seqlens_q.stride(0) == 1, "cu_seqlens_q must be contiguous"
assert q.dtype in [
torch.float16,
torch.bfloat16,
], "inputs must be float16 or bfloat16"
assert q.dtype == k.dtype == v.dtype, "inputs must have the same dtype"
assert head_dim <= 256, "head_dim must be less than or equal to 256"
alignment = 16 // q.element_size()
assert head_dim_v % alignment == 0, f"head_dim_v must be divisible by {alignment}"
q_batch_seqlen_shape = (
(batch_size, seqlen_q) if cu_seqlens_q is None else (q.shape[0],)
)
out = q.new_empty(*q_batch_seqlen_shape, num_head, head_dim_v)
return out
def flash_attn_varlen_func_fake_out_lse(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k: Optional[torch.Tensor] = None,
max_seqlen_q: Optional[int] = None,
max_seqlen_k: Optional[int] = None,
seqused_q: Optional[torch.Tensor] = None,
seqused_k: Optional[torch.Tensor] = None,
page_table: Optional[torch.Tensor] = None,
softmax_scale: Optional[float] = None,
causal: bool = False,
qv: Optional[torch.Tensor] = None,
q_descale: Optional[torch.Tensor] = None,
k_descale: Optional[torch.Tensor] = None,
v_descale: Optional[torch.Tensor] = None,
window_size: Optional[List[int]] = None,
attention_chunk: int = 0,
softcap: float = 0.0,
num_splits: int = 1,
pack_gqa: Optional[bool] = None,
sm_margin: int = 0,
return_softmax_lse: bool = True,
sinks: Optional[torch.Tensor] = None,
ver: int = 4,
) -> Tuple[torch.Tensor, torch.Tensor]:
assert ver == 4, "only support flash attention v4"
q, k, v = [maybe_contiguous(t) for t in (q, k, v)]
num_head, head_dim = q.shape[-2:]
if cu_seqlens_q is None:
batch_size, seqlen_q = q.shape[:2]
total_q = batch_size * seqlen_q
else:
batch_size = cu_seqlens_q.shape[0] - 1
seqlen_q = None
total_q = q.shape[0]
head_dim_v = v.shape[-1]
if cu_seqlens_q is not None:
assert cu_seqlens_q.shape == (
batch_size + 1,
), "cu_seqlens_q must have shape (batch_size + 1,)"
assert cu_seqlens_q.dtype == torch.int32, "cu_seqlens_q must be int32"
assert cu_seqlens_q.stride(0) == 1, "cu_seqlens_q must be contiguous"
assert q.dtype in [
torch.float16,
torch.bfloat16,
], "inputs must be float16 or bfloat16"
assert q.dtype == k.dtype == v.dtype, "inputs must have the same dtype"
assert head_dim <= 256, "head_dim must be less than or equal to 256"
alignment = 16 // q.element_size()
assert head_dim_v % alignment == 0, f"head_dim_v must be divisible by {alignment}"
q_batch_seqlen_shape = (
(batch_size, seqlen_q) if cu_seqlens_q is None else (total_q,)
)
lse_shape = (
(batch_size, num_head, seqlen_q)
if cu_seqlens_q is None
else (num_head, total_q)
)
out = q.new_empty(*q_batch_seqlen_shape, num_head, head_dim_v)
lse = q.new_empty(lse_shape, dtype=torch.float32)
return out, lse
# -----------------------------
# Registered custom ops
# NOTE: fixed return schemas to avoid:
# "Object of type 'Tensor' is not an instance of 'sequence'"
# -----------------------------
@register_custom_op(fake_impl=flash_attn_varlen_func_fake_out)
def flash_attn_varlen_func_op(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k: Optional[torch.Tensor] = None,
max_seqlen_q: Optional[int] = None,
max_seqlen_k: Optional[int] = None,
seqused_q: Optional[torch.Tensor] = None,
seqused_k: Optional[torch.Tensor] = None,
page_table: Optional[torch.Tensor] = None,
softmax_scale: Optional[float] = None,
causal: bool = False,
qv: Optional[torch.Tensor] = None,
q_descale: Optional[torch.Tensor] = None,
k_descale: Optional[torch.Tensor] = None,
v_descale: Optional[torch.Tensor] = None,
window_size: Optional[List[int]] = None,
attention_chunk: int = 0,
softcap: float = 0.0,
num_splits: int = 1,
pack_gqa: Optional[bool] = None,
sm_margin: int = 0,
return_softmax_lse: bool = False,
sinks: Optional[torch.Tensor] = None,
ver: int = 4,
) -> torch.Tensor:
if window_size is None:
window_size = [-1, -1]
if return_softmax_lse:
raise ValueError(
"flash_attn_varlen_func_op is out-only op; return_softmax_lse must be False. "
"Use flash_attn_varlen_func_op_lse for (out, lse)."
)
return flash_attn_varlen_func(
q,
k,
v,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
seqused_q=seqused_q,
seqused_k=seqused_k,
page_table=page_table,
softmax_scale=softmax_scale,
causal=causal,
qv=qv,
q_descale=q_descale,
k_descale=k_descale,
v_descale=v_descale,
window_size=tuple(window_size),
attention_chunk=attention_chunk,
softcap=softcap,
num_splits=num_splits,
pack_gqa=pack_gqa,
sm_margin=sm_margin,
return_softmax_lse=False,
sinks=sinks,
ver=ver,
)
@register_custom_op(fake_impl=flash_attn_varlen_func_fake_out_lse)
def flash_attn_varlen_func_op_lse(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k: Optional[torch.Tensor] = None,
max_seqlen_q: Optional[int] = None,
max_seqlen_k: Optional[int] = None,
seqused_q: Optional[torch.Tensor] = None,
seqused_k: Optional[torch.Tensor] = None,
page_table: Optional[torch.Tensor] = None,
softmax_scale: Optional[float] = None,
causal: bool = False,
qv: Optional[torch.Tensor] = None,
q_descale: Optional[torch.Tensor] = None,
k_descale: Optional[torch.Tensor] = None,
v_descale: Optional[torch.Tensor] = None,
window_size: Optional[List[int]] = None,
attention_chunk: int = 0,
softcap: float = 0.0,
num_splits: int = 1,
pack_gqa: Optional[bool] = None,
sm_margin: int = 0,
return_softmax_lse: bool = True,
sinks: Optional[torch.Tensor] = None,
ver: int = 4,
) -> Tuple[torch.Tensor, torch.Tensor]:
if window_size is None:
window_size = [-1, -1]
if not return_softmax_lse:
raise ValueError(
"flash_attn_varlen_func_op_lse is out+lse op; return_softmax_lse must be True. "
"Use flash_attn_varlen_func_op for out-only."
)
return flash_attn_varlen_func(
q,
k,
v,
cu_seqlens_q=cu_seqlens_q,
cu_seqlens_k=cu_seqlens_k,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
seqused_q=seqused_q,
seqused_k=seqused_k,
page_table=page_table,
softmax_scale=softmax_scale,
causal=causal,
qv=qv,
q_descale=q_descale,
k_descale=k_descale,
v_descale=v_descale,
window_size=tuple(window_size),
attention_chunk=attention_chunk,
softcap=softcap,
num_splits=num_splits,
pack_gqa=pack_gqa,
sm_margin=sm_margin,
return_softmax_lse=True,
sinks=sinks,
ver=ver,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
fa_ver = 3
def set_fa_ver(ver: int) -> None:
global fa_ver
fa_ver = ver
@dataclass
class FlashAttentionMetadata:
# Sequence lengths for the forward batch
# Maximum sequence length for query
max_seqlen_q: int = 1
# Maximum sequence length for key
max_seqlen_k: int = 0
# Cumulative sequence lengths for query
cu_seqlens_q: torch.Tensor = None
# Cumulative sequence lengths for key
cu_seqlens_k: torch.Tensor = None
class FlashAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build( # type: ignore
self,
raw_latent_shape=list,
**kwargs: dict[str, Any],
) -> FlashAttentionMetadata:
# TODO: put empty values here to be set at first-run, since the q_len calculation can be complicated
return FlashAttentionMetadata(max_seqlen_q=None, max_seqlen_k=None)
class FlashAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.FA
@staticmethod
def get_impl_cls() -> type["FlashAttentionImpl"]:
return FlashAttentionImpl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
raise NotImplementedError
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
return FlashAttentionMetadataBuilder
class FlashAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.num_heads = num_heads
self.num_kv_heads = num_kv_heads
self.head_size = head_size
self.causal = causal
self.softmax_scale = softmax_scale
self.attention_metadata = FlashAttentionMetadata()
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata = None,
*,
return_softmax_lse: bool = False,
):
if attn_metadata is not None:
if attn_metadata.max_seqlen_q is None:
attn_metadata.max_seqlen_q = query.shape[1]
if attn_metadata.max_seqlen_k is None:
attn_metadata.max_seqlen_k = key.shape[1]
max_seqlen_q = attn_metadata.max_seqlen_q
max_seqlen_k = attn_metadata.max_seqlen_k
else:
max_seqlen_q = query.shape[1]
max_seqlen_k = key.shape[1]
# FA version selection:
# - fa_ver == 3: call python function (can return Tensor or (Tensor, Tensor) depending on flag)
# - fa_ver == 4: call custom ops with FIXED return schema
if fa_ver == 3:
flash_attn_op = flash_attn_varlen_func
output = flash_attn_op(
q=query,
k=key,
v=value,
cu_seqlens_q=None,
cu_seqlens_k=None,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
softmax_scale=self.softmax_scale,
causal=self.causal,
return_softmax_lse=return_softmax_lse,
ver=fa_ver,
)
return output
if fa_ver == 4:
if return_softmax_lse:
out_tensor, softmax_lse = flash_attn_varlen_func_op_lse(
q=query,
k=key,
v=value,
cu_seqlens_q=None,
cu_seqlens_k=None,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
softmax_scale=self.softmax_scale,
causal=self.causal,
return_softmax_lse=True,
ver=fa_ver,
)
return out_tensor, softmax_lse
out_tensor = flash_attn_varlen_func_op(
q=query,
k=key,
v=value,
cu_seqlens_q=None,
cu_seqlens_k=None,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
softmax_scale=self.softmax_scale,
causal=self.causal,
return_softmax_lse=False,
ver=fa_ver,
)
return out_tensor
raise ValueError(f"flash attention version {fa_ver} is not supported.")
@@ -0,0 +1,79 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.flash_attn import (
flash_attn_func,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class FlashAttention2Backend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.FA2
@staticmethod
def get_impl_cls() -> type["FlashAttention2Impl"]:
return FlashAttention2Impl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
raise NotImplementedError
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
raise NotImplementedError
class FlashAttention2Impl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
):
output = flash_attn_func(
q=query, # type: ignore[no-untyped-call]
k=key,
v=value,
cu_seqlens_q=None,
cu_seqlens_k=None,
max_seqlen_q=None,
max_seqlen_k=None,
softmax_scale=self.softmax_scale,
causal=self.causal,
)
return output
@@ -0,0 +1,191 @@
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.sdpa import SDPABackend
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
# Import to use torch.ops.attentions, install package with sgl_kernel_npu
try:
import attentions # noqa: F401
except ImportError as e:
raise ImportError(
(
"The required 'attentions' package is not installed."
"The package can be installed with sgl_kernel_npu"
)
) from e
logger = init_logger(__name__)
class LaserAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.LASER_ATTN
@staticmethod
def get_impl_cls() -> type["LaserAttentionImpl"]:
return LaserAttentionImpl
class LaserAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.softmax_scale = softmax_scale
# After preprocess input layout should be BNSD.
self.seqlen_base = 256
self.seqlen_index = 2
self.dim_index = 3
self.dim_base = 128
self.max_token = 2**31 - 1
self.seq_len_pad_base = 256
# the laser attention operator has issues with small seq_len
self.min_seqlen = 2048
self.sdpa_impl = SDPABackend.get_impl_cls()(
num_heads,
head_size,
causal,
softmax_scale,
num_kv_heads,
prefix,
**extra_impl_args,
)
def _pad(self, input_tensor: torch.Tensor) -> torch.Tensor:
"""
Pad the input tensor along the sequence length and head dimension.
to multiples of base values. self.seqlen_index and self.dim_index should be positive integers.
"""
seq_len = input_tensor.size(self.seqlen_index)
head_dim = input_tensor.size(self.dim_index)
pad_seq = 0
if seq_len % self.seqlen_base != 0:
pad_seq = ((seq_len // self.seqlen_base) + 1) * self.seqlen_base - seq_len
pad_dim = 0
if head_dim % self.dim_base != 0:
pad_dim = ((head_dim // self.dim_base) + 1) * self.dim_base - head_dim
if pad_seq == 0 and pad_dim == 0:
return input_tensor
pad_list = [0] * (2 * input_tensor.ndim)
pad_list[len(pad_list) - 2 * self.seqlen_index - 1] = pad_seq
pad_list[len(pad_list) - 2 * self.dim_index - 1] = pad_dim
return torch.nn.functional.pad(input_tensor, pad_list)
def _la_preprocess_input(
self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
# Currently BSND input layout is not supported
q = query.transpose(1, 2)
k = key.transpose(1, 2)
v = value.transpose(1, 2)
if q.dtype != torch.float16:
q = q.to(torch.float16)
k = k.to(torch.float16)
v = v.to(torch.float16)
q = self._pad(q)
k = self._pad(k)
v = self._pad(v)
return q, k, v
def _la_postprocess_output(
self,
attention_out: torch.Tensor,
dtype: torch.dtype,
qseqlen: int,
head_dim: int,
) -> torch.Tensor:
if dtype != attention_out.dtype:
attention_out = attention_out.to(dtype)
attention_out = attention_out[:, :, :qseqlen, :head_dim]
attention_out = attention_out.transpose(1, 2).contiguous()
return attention_out
def _laser_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
head_num: int,
pre_tokens: int,
) -> tuple[torch.Tensor, torch.Tensor]:
return torch.ops.attentions.la(
query=query,
key=key,
value=value,
atten_mask=None,
alibi_mask=None,
drop_mask=None,
scale_value=self.softmax_scale,
head_num=head_num,
input_layout="BNSD",
keep_prob=1.0,
pre_tokens=pre_tokens,
next_tokens=1,
is_highPrecision=True,
)
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
q_seqlen, head_dim = query.shape[1], query.shape[3]
kv_seqlen = key.shape[1]
if q_seqlen < self.min_seqlen or kv_seqlen != q_seqlen:
output = self.sdpa_impl.forward(
query,
key,
value,
attn_metadata,
)
else:
pre_tokens = self.max_token
if kv_seqlen % self.seq_len_pad_base != 0:
pre_tokens = (
kv_seqlen // self.seq_len_pad_base + 1
) * self.seq_len_pad_base - kv_seqlen
q, k, v = self._la_preprocess_input(query, key, value)
_, la_output = self._laser_attention(q, k, v, q.shape[1], pre_tokens)
output = self._la_postprocess_output(
la_output, query.dtype, q_seqlen, head_dim
)
return output
@@ -0,0 +1,414 @@
import math
from dataclasses import dataclass
from typing import Any, List, Optional
import attentions # noqa: F401
import torch
from einops import rearrange
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.laser_attn import (
LaserAttentionBackend,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class RainFusionAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.RAIN_FUSION_ATTN
@staticmethod
def get_impl_cls() -> type["RainFusionAttentionImpl"]:
return RainFusionAttentionImpl
@staticmethod
def get_metadata_cls() -> type["RainFusionAttentionMetadata"]:
return RainFusionAttentionMetadata
@staticmethod
def get_builder_cls() -> type["RainFusionAttentionMetadataBuilder"]:
return RainFusionAttentionMetadataBuilder
@dataclass
class RainFusionAttentionMetadata(AttentionMetadata):
current_timestep: int
skip_first_steps: int
sparsity: float
latent_shape: list[int]
class RainFusionAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build(
self,
current_timestep: int,
skip_first_steps: int,
sparsity: float,
raw_latent_shape: list[int],
patch_size: tuple[int, int, int],
**kwargs: dict[str, Any],
) -> RainFusionAttentionMetadata:
if not (skip_first_steps >= 0 and 0.0 <= sparsity < 1.0):
raise ValueError(
(
"Invalid attention metadata values."
f"Sparsity should be in [0, 1), skip_first_steps should be non-negative."
f"Got sparsity={sparsity}, skip_first_steps={skip_first_steps}"
)
)
if sparsity == 0.0:
logger.warning(
(
"Sparsity is set to 0.0, which means no tokens will be dropped."
"For better performance use Laser Attention or increase sparsity."
)
)
latent_shape = raw_latent_shape[-3:]
latent_shape = [latent_shape[i] // patch_size[i] for i in range(3)]
return RainFusionAttentionMetadata(
current_timestep=current_timestep,
skip_first_steps=skip_first_steps,
sparsity=sparsity,
latent_shape=latent_shape,
)
class RainFusionAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
self.block_size = 128
self.inner_precise = 0
self.laser_attn_impl = LaserAttentionBackend.get_impl_cls()(
num_heads,
head_size,
causal,
softmax_scale,
num_kv_heads,
prefix,
**extra_impl_args,
)
def _avgpool(
self, input_tensor: torch.Tensor, pool_size: int = 128
) -> torch.Tensor:
batch, seqlen, heads, dim = input_tensor.shape
x = input_tensor.permute(0, 2, 3, 1).reshape(batch * heads, dim, seqlen)
pooled = torch.nn.functional.avg_pool1d(
x, kernel_size=pool_size, stride=pool_size, ceil_mode=True
)
out = pooled.reshape(batch, heads, dim, -1).permute(0, 3, 1, 2).contiguous()
return out
def _get_mask_index(self, mask: torch.Tensor) -> torch.Tensor:
batch_size, num_heads, seq_len, _ = mask.shape
mask_reshaped = mask.reshape(-1, seq_len)
row_indices = torch.arange(
seq_len, device=mask.device, dtype=torch.float32
).unsqueeze(0)
sorted_vals = torch.where(mask_reshaped, row_indices, seq_len)
sorted_vals, _ = torch.sort(sorted_vals, dim=-1)
valid_count = mask_reshaped.sum(dim=-1, keepdim=True)
keep_mask = row_indices < valid_count
result = torch.where(keep_mask, sorted_vals, -1)
pos_matrix = result.reshape(batch_size, num_heads, seq_len, seq_len).to(
torch.int64
)
return pos_matrix
def _get_blockwise_mask(
self,
qkv_pool: torch.Tensor,
sparsity: float,
scale: float,
pool_size: int,
latent_shape: tuple,
) -> tuple[torch.Tensor, torch.Tensor]:
first_frame_len = latent_shape[1] * latent_shape[2]
query_pool, key_pool, value_pool = torch.chunk(qkv_pool, 3, dim=0)
attn_scores = (
query_pool.permute(0, 2, 1, 3) @ key_pool.permute(0, 2, 3, 1) * scale
)
keep_len = math.ceil(attn_scores.shape[-1] * (1 - sparsity))
topk_values, _ = torch.topk(attn_scores, k=keep_len, dim=-1)
mask = attn_scores >= topk_values[..., -1:]
firstframe_block_num = (first_frame_len + pool_size - 1) // pool_size
if firstframe_block_num > 0:
mask[:, :, :firstframe_block_num, :] = True
mask[:, :, :, :firstframe_block_num] = True
select_idx = self._get_mask_index(mask)
select_idx = select_idx[0].transpose(0, 1)
select_num_idx = mask[0].transpose(0, 1).sum(dim=-1)
return select_idx, select_num_idx
def _rearrange_with_remaining(
self, tensor: torch.Tensor, latent_shape: tuple[int, int, int]
) -> torch.Tensor:
"""
b (f hn hb wn wb) n d -> b (f hn wn hb wb) n d
or
b n (f hn hb wn wb) d -> b n (f hn wn hb wb) d
"""
tq, hq, wq = latent_shape
first_frame_len, frame_num = hq * wq, tq
b, s, n, d = tensor.shape
if (hq % 8 != 0) or (wq % 8 != 0):
tensor_first = tensor[:, :first_frame_len, :, :]
tensor = tensor[:, first_frame_len:, :, :]
tensor_hwt = rearrange(
tensor, "b (f h w) n d -> b f h w n d", f=frame_num - 1, h=hq, w=wq
)
if hq % 8 != 0:
tensor_hwt, tensor_h_r = torch.split(tensor_hwt, hq - (hq % 8), dim=2)
tensor_h_r = tensor_h_r.reshape(b, frame_num - 1, -1, n, d)
if wq % 8 != 0:
tensor_hwt, tensor_w_r = torch.split(tensor_hwt, wq - (wq % 8), dim=3)
tensor_w_r = tensor_w_r.reshape(b, frame_num - 1, -1, n, d)
tensor_hwt = rearrange(
tensor_hwt,
"b f (hn hb) (wn wb) n d -> b f (hn wn hb wb) n d",
f=frame_num - 1,
hb=8,
wb=8,
hn=hq // 8,
wn=wq // 8,
)
if hq % 8 != 0:
tensor_hwt = torch.cat((tensor_hwt, tensor_h_r), dim=2)
if wq % 8 != 0:
tensor_hwt = torch.cat((tensor_hwt, tensor_w_r), dim=2)
tensor_hwt = tensor_hwt.reshape(b, -1, n, d)
tensor_hwt = torch.cat([tensor_first, tensor_hwt], dim=1)
else:
tensor_hwt = rearrange(
tensor,
"b (f hn hb wn wb) n d -> b (f hn wn hb wb) n d",
f=frame_num,
hb=8,
wb=8,
hn=hq // 8,
wn=wq // 8,
)
return tensor_hwt
def _inv_rearrange_with_remaining(
self, tensor: torch.Tensor, latent_shape: tuple[int, int, int]
) -> torch.Tensor:
tq, hq, wq = latent_shape
first_frame_len, frame_num = hq * wq, tq
b, s, n, d = tensor.shape
if (hq % 8 != 0) or (wq % 8 != 0):
tensor_first = tensor[:, :first_frame_len, :, :]
tensor = tensor[:, first_frame_len:, :, :]
tensor_hwt = rearrange(
tensor, "b (f h w) n d -> b f h w n d", f=frame_num - 1, h=hq, w=wq
)
if hq % 8 != 0:
tensor_hwt, tensor_h_r = torch.split(tensor_hwt, hq - (hq % 8), dim=2)
if wq % 8 != 0:
tensor_hwt, tensor_w_r = torch.split(tensor_hwt, wq - (wq % 8), dim=3)
tensor_hwt = tensor_hwt.reshape(b, frame_num - 1, -1, n, d)
tensor_hwt = rearrange(
tensor_hwt,
"b f (hn wn hb wb) n d -> b f (hn hb) (wn wb) n d",
f=frame_num - 1,
hb=8,
wb=8,
hn=hq // 8,
wn=wq // 8,
)
if wq % 8 != 0:
tensor_hwt = torch.cat((tensor_hwt, tensor_w_r), dim=3)
if hq % 8 != 0:
tensor_hwt = torch.cat((tensor_hwt, tensor_h_r), dim=2)
tensor_hwt = tensor_hwt.reshape(b, -1, n, d)
tensor_hwt = torch.cat([tensor_first, tensor_hwt], dim=1)
else:
tensor_hwt = rearrange(
tensor,
"b (f hn wn hb wb) n h -> b (f hn hb wn wb) n h",
f=frame_num,
hb=8,
wb=8,
hn=hq // 8,
wn=wq // 8,
)
return tensor_hwt
def _do_tensor_rearrange_pooling(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
pool_size: int,
latent_shape: tuple[int, int, int],
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""
Tensor block rearrangement + pooling operation
"""
tensor = torch.cat((query, key, value), dim=0)
tensor = self._rearrange_with_remaining(tensor, latent_shape)
tensor_pool = self._avgpool(tensor, pool_size)
query_, key_, value_ = torch.chunk(tensor, 3, dim=0)
return query_, key_, value_, tensor_pool
def _rain_fusion_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
select_idx: torch.Tensor,
select_num_idx: torch.Tensor,
blockshape: List[int],
scale: float = 1.0,
head_num: int = 1,
input_layout: str = "TND",
actual_seq_lengths=Optional[torch.Tensor],
actual_seq_lengths_kv=Optional[torch.Tensor],
) -> tuple[torch.Tensor, torch.Tensor]:
return torch.ops.attentions.rainfusionattention(
query=query,
key=key,
value=value,
select_idx=select_idx,
select_num_idx=select_num_idx,
blockshape=blockshape,
attn_mask=None,
actual_seq_qlen=actual_seq_lengths,
actual_seq_kvlen=actual_seq_lengths_kv,
block_table=None,
q_input_layout=input_layout,
kv_input_layout=input_layout,
head_num=head_num,
mask_type=0,
scale=scale,
inner_precise=self.inner_precise,
block_size=0,
)
def _rain_fusion_sparse_attention(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
latent_shape: tuple[int, int, int],
sparsity: float,
):
q, k, v, qkv_pool = self._do_tensor_rearrange_pooling(
query, key, value, self.block_size, latent_shape
)
select_idx, select_num_idx = self._get_blockwise_mask(
qkv_pool,
sparsity,
self.softmax_scale,
self.block_size,
latent_shape,
)
batch_size, seqlen_q, head_num, head_dim = q.shape
seqlen_kv = k.shape[1]
layout = "TND"
q = q.reshape(-1, head_num, head_dim)
k = k.reshape(-1, head_num, head_dim)
v = v.reshape(-1, head_num, head_dim)
actual_seq_lengths = [seqlen_q] * batch_size
actual_seq_lengths_kv = [seqlen_kv] * batch_size
out, _ = self._rain_fusion_attention(
q,
k,
v,
scale=self.softmax_scale,
head_num=head_num,
input_layout=layout,
select_idx=select_idx,
select_num_idx=select_num_idx,
blockshape=[self.block_size, self.block_size],
actual_seq_lengths=actual_seq_lengths,
actual_seq_lengths_kv=actual_seq_lengths_kv,
)
out = out.reshape(batch_size, seqlen_q, head_num, head_dim)
out = self._inv_rearrange_with_remaining(out, latent_shape)
return out
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
if attn_metadata.current_timestep < attn_metadata.skip_first_steps:
output = self.laser_attn_impl.forward(
query,
key,
value,
attn_metadata,
)
else:
output = self._rain_fusion_sparse_attention(
query,
key,
value,
attn_metadata.latent_shape,
attn_metadata.sparsity,
)
return output
@@ -0,0 +1,74 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import torch
from sageattention import sageattn
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import ( # FlashAttentionMetadata,
AttentionBackend,
AttentionImpl,
AttentionMetadata,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class SageAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SAGE_ATTN
@staticmethod
def get_impl_cls() -> type["SageAttentionImpl"]:
return SageAttentionImpl
class SageAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
self.dropout = extra_impl_args.get("dropout_p", 0.0)
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
*,
return_softmax_lse: bool = False,
) -> torch.Tensor:
output = sageattn(
query,
key,
value,
# since input is (batch_size, seq_len, head_num, head_dim)
tensor_layout="NHD",
is_causal=self.causal,
sm_scale=self.softmax_scale,
return_lse=return_softmax_lse,
)
if return_softmax_lse:
output, softmax_lse = output
return output, softmax_lse
return output
@@ -0,0 +1,92 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import torch
import torch.nn.functional as F
from sageattn3 import sageattn3_blackwell
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class SageAttention3Backend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 128, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SAGE_ATTN_3
@staticmethod
def get_impl_cls() -> type["SageAttention3Impl"]:
return SageAttention3Impl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
raise NotImplementedError
class SageAttention3Impl(AttentionImpl):
_warned_gqa_fallback_global: bool = False
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
self.dropout = extra_impl_args.get("dropout_p", 0.0)
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
# SageAttention3's Blackwell kernel assumes MHA (Hq == Hkv). For GQA/MQA
# (Hq != Hkv), fall back to torch SDPA which supports GQA.
if key.shape[1] != query.shape[1]:
if query.shape[1] % key.shape[1] != 0:
raise ValueError(
"GQA/MQA requires query heads to be a multiple of KV heads, "
f"got q_heads={query.shape[1]} and kv_heads={key.shape[1]}"
)
if not type(self)._warned_gqa_fallback_global:
logger.warning(
"SageAttention3 does not support GQA/MQA (Hq != Hkv); falling back to torch SDPA."
)
type(self)._warned_gqa_fallback_global = True
output = F.scaled_dot_product_attention(
query,
key,
value,
is_causal=self.causal,
dropout_p=self.dropout,
scale=self.softmax_scale,
enable_gqa=True,
)
else:
output = sageattn3_blackwell(query, key, value, is_causal=self.causal)
output = output.transpose(1, 2)
return output
@@ -0,0 +1,95 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from contextlib import nullcontext
import torch
from torch.nn.attention import SDPBackend, sdpa_kernel
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import ( # FlashAttentionMetadata,
AttentionBackend,
AttentionImpl,
AttentionMetadata,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS = [
SDPBackend.CUDNN_ATTENTION,
SDPBackend.FLASH_ATTENTION,
SDPBackend.EFFICIENT_ATTENTION,
SDPBackend.MATH,
]
class SDPABackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.TORCH_SDPA
@staticmethod
def get_impl_cls() -> type["SDPAImpl"]:
return SDPAImpl
# @staticmethod
# def get_metadata_cls() -> Type["AttentionMetadata"]:
# return FlashAttentionMetadata
class SDPAImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.causal = causal
self.softmax_scale = softmax_scale
self.dropout = extra_impl_args.get("dropout_p", 0.0)
self.allow_cudnn_sdp = bool(extra_impl_args.get("allow_cudnn_sdp", False))
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
# transpose to bs, heads, seq_len, head_dim
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
attn_kwargs = {
"attn_mask": None,
"dropout_p": self.dropout,
"is_causal": self.causal,
"scale": self.softmax_scale,
}
if query.shape[1] != key.shape[1]:
attn_kwargs["enable_gqa"] = True
sdpa_context = (
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
if self.allow_cudnn_sdp and query.device.type == "cuda"
else nullcontext()
)
with sdpa_context:
output = torch.nn.functional.scaled_dot_product_attention(
query, key, value, **attn_kwargs
)
output = output.transpose(1, 2)
return output
@@ -0,0 +1,316 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import json
from dataclasses import dataclass
from typing import Any
import torch
from einops import rearrange
from sglang.multimodal_gen.runtime.distributed import get_sp_group
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.managers.forward_context import (
ForwardContext,
get_forward_context,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.server_args import get_global_server_args
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.utils import dict_to_3d_list
try:
from st_attn import sliding_tile_attention
st_attn_backend_available = True
except Exception:
st_attn_backend_available = False
logger = init_logger(__name__)
class RangeDict(dict):
def __getitem__(self, item: int) -> str:
for key in self.keys():
if isinstance(key, tuple):
low, high = key
if low <= item <= high:
return str(super().__getitem__(key))
elif key == item:
return str(super().__getitem__(key))
raise KeyError(f"seq_len {item} not supported for STA")
class SlidingTileAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
# TODO(will-refactor): check this
return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SLIDING_TILE_ATTN
@staticmethod
def get_impl_cls() -> type["SlidingTileAttentionImpl"]:
return SlidingTileAttentionImpl
@staticmethod
def get_metadata_cls() -> type["SlidingTileAttentionMetadata"]:
return SlidingTileAttentionMetadata
@staticmethod
def get_builder_cls() -> type["SlidingTileAttentionMetadataBuilder"]:
return SlidingTileAttentionMetadataBuilder
@dataclass
class SlidingTileAttentionMetadata(AttentionMetadata):
current_timestep: int
STA_param: list[
list[Any]
] # each timestep with one metadata, shape [num_layers, num_heads]
class SlidingTileAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self):
pass
def prepare(self):
pass
def build( # type: ignore
self,
STA_param: list[list[Any]],
current_timestep: int,
**kwargs: dict[str, Any],
) -> SlidingTileAttentionMetadata:
param = STA_param
if param is None:
return SlidingTileAttentionMetadata(
current_timestep=current_timestep, STA_param=[]
)
return SlidingTileAttentionMetadata(
current_timestep=current_timestep, STA_param=param[current_timestep]
)
class SlidingTileAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
if not st_attn_backend_available:
raise ValueError("st attn not supported")
# TODO(will-refactor): for now this is the mask strategy, but maybe we should
# have a more general config for STA?
mask_strategy_file_path = (
get_global_server_args().attention_backend_config.mask_strategy_file_path
)
if mask_strategy_file_path is None:
raise ValueError("SGLANG_DIFFUSION_ATTENTION_CONFIG is not set")
# TODO(kevin): get mask strategy for different STA modes
with open(mask_strategy_file_path) as f:
mask_strategy = json.load(f)
self.mask_strategy = dict_to_3d_list(mask_strategy)
self.prefix = prefix
sp_group = get_sp_group()
self.sp_size = sp_group.world_size
# STA config
self.STA_base_tile_size = [6, 8, 8]
self.dit_seq_shape_mapping = RangeDict(
{
(115200, 115456): "30x48x80",
82944: "36x48x48",
69120: "18x48x80",
}
)
self.full_window_mapping = {
"30x48x80": [5, 6, 10],
"36x48x48": [6, 6, 6],
"18x48x80": [3, 6, 10],
}
def tile(self, x: torch.Tensor) -> torch.Tensor:
return rearrange(
x,
"b (n_t ts_t n_h ts_h n_w ts_w) h d -> b (n_t n_h n_w ts_t ts_h ts_w) h d",
n_t=self.full_window_size[0],
n_h=self.full_window_size[1],
n_w=self.full_window_size[2],
ts_t=self.STA_base_tile_size[0],
ts_h=self.STA_base_tile_size[1],
ts_w=self.STA_base_tile_size[2],
)
def untile(self, x: torch.Tensor) -> torch.Tensor:
x = rearrange(
x,
"b (n_t n_h n_w ts_t ts_h ts_w) h d -> b (n_t ts_t n_h ts_h n_w ts_w) h d",
n_t=self.full_window_size[0],
n_h=self.full_window_size[1],
n_w=self.full_window_size[2],
ts_t=self.STA_base_tile_size[0],
ts_h=self.STA_base_tile_size[1],
ts_w=self.STA_base_tile_size[2],
)
return x
def preprocess_qkv(
self,
qkv: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
img_sequence_length = qkv.shape[1]
self.dit_seq_shape_str = self.dit_seq_shape_mapping[img_sequence_length]
self.full_window_size = self.full_window_mapping[self.dit_seq_shape_str]
self.dit_seq_shape_int = list(map(int, self.dit_seq_shape_str.split("x")))
self.img_seq_length = (
self.dit_seq_shape_int[0]
* self.dit_seq_shape_int[1]
* self.dit_seq_shape_int[2]
)
return self.tile(qkv)
def postprocess_output(
self,
output: torch.Tensor,
attn_metadata: SlidingTileAttentionMetadata,
) -> torch.Tensor:
return self.untile(output)
def forward(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
attn_metadata: SlidingTileAttentionMetadata,
) -> torch.Tensor:
if self.mask_strategy is None:
raise ValueError("mask_strategy cannot be None for SlidingTileAttention")
if self.mask_strategy[0] is None:
raise ValueError("mask_strategy[0] cannot be None for SlidingTileAttention")
timestep = attn_metadata.current_timestep
forward_context: ForwardContext = get_forward_context()
forward_batch = forward_context.forward_batch
if forward_batch is None:
raise ValueError("forward_batch cannot be None")
# pattern:'.double_blocks.0.attn.impl' or '.single_blocks.0.attn.impl'
layer_idx = int(self.prefix.split(".")[-3])
if attn_metadata.STA_param is None or len(attn_metadata.STA_param) <= layer_idx:
raise ValueError("Invalid STA_param")
STA_param = attn_metadata.STA_param[layer_idx]
text_length = q.shape[1] - self.img_seq_length
has_text = text_length > 0
query = q.transpose(1, 2).contiguous()
key = k.transpose(1, 2).contiguous()
value = v.transpose(1, 2).contiguous()
head_num = query.size(1)
sp_group = get_sp_group()
current_rank = sp_group.rank_in_group
start_head = current_rank * head_num
# searching or tuning mode
if len(STA_param) < head_num * sp_group.world_size:
sparse_attn_hidden_states_all = []
full_mask_window = STA_param[-1]
for window_size in STA_param[:-1]:
sparse_hidden_states = sliding_tile_attention(
query,
key,
value,
[window_size] * head_num,
text_length,
has_text,
self.dit_seq_shape_str,
).transpose(1, 2)
sparse_attn_hidden_states_all.append(sparse_hidden_states)
hidden_states = sliding_tile_attention(
query,
key,
value,
[full_mask_window] * head_num,
text_length,
has_text,
self.dit_seq_shape_str,
).transpose(1, 2)
attn_L2_loss = []
attn_L1_loss = []
# average loss across all heads
for sparse_attn_hidden_states in sparse_attn_hidden_states_all:
# L2 loss
attn_L2_loss_ = (
torch.mean(
(sparse_attn_hidden_states.float() - hidden_states.float())
** 2,
dim=[0, 1, 3],
)
.cpu()
.numpy()
)
attn_L2_loss_ = [round(float(x), 6) for x in attn_L2_loss_]
attn_L2_loss.append(attn_L2_loss_)
# L1 loss
attn_L1_loss_ = (
torch.mean(
torch.abs(
sparse_attn_hidden_states.float() - hidden_states.float()
),
dim=[0, 1, 3],
)
.cpu()
.numpy()
)
attn_L1_loss_ = [round(float(x), 6) for x in attn_L1_loss_]
attn_L1_loss.append(attn_L1_loss_)
layer_loss_save = {"L2_loss": attn_L2_loss, "L1_loss": attn_L1_loss}
if forward_batch.is_cfg_negative:
if forward_batch.mask_search_final_result_neg is not None:
forward_batch.mask_search_final_result_neg[timestep].append(
layer_loss_save
)
else:
if forward_batch.mask_search_final_result_pos is not None:
forward_batch.mask_search_final_result_pos[timestep].append(
layer_loss_save
)
else:
windows = [STA_param[head_idx + start_head] for head_idx in range(head_num)]
hidden_states = sliding_tile_attention(
query,
key,
value,
windows,
text_length,
has_text,
self.dit_seq_shape_str,
).transpose(1, 2)
return hidden_states
@@ -0,0 +1,695 @@
"""
Copyright (c) 2025 by SLA team.
Licensed under the Apache License, Version 2.0 (the "License");
This implementation is adapted from: from https://github.com/thu-ml/TurboDiffusion/blob/main/turbodiffusion/SLA/core.py and https://github.com/thu-ml/SLA/blob/main/SageSLA/core.py
Citation (please cite if you use this code):
@article{zhang2025sla,
title={SLA: Beyond Sparsity in Diffusion Transformers via Fine-Tunable Sparse-Linear Attention},
author={Jintao Zhang and Haoxu Wang and Kai Jiang and Shuo Yang and Kaiwen Zheng and Haocheng Xi and Ziteng Wang and Hongzhou Zhu and Min Zhao and Ion Stoica and Joseph E. Gonzalez and Jun Zhu and Jianfei Chen},
journal={arXiv preprint arXiv:2509.24006},
year={2025}
}
"""
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
import triton
import triton.language as tl
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
# ==================================SLA Functions===================================
def get_block_map(q, k, topk_ratio, BLKQ=64, BLKK=64):
arg_k = k - torch.mean(
k, dim=-2, keepdim=True
) # smooth-k technique in SageAttention
pooled_qblocks = mean_pool(q, BLKQ)
pooled_kblocks = mean_pool(arg_k, BLKK)
pooled_score = pooled_qblocks @ pooled_kblocks.transpose(-1, -2)
K = pooled_score.shape[-1]
topk = min(K, int(topk_ratio * K))
lut = torch.topk(pooled_score, topk, dim=-1, sorted=False).indices
sparse_map = torch.zeros_like(pooled_score, dtype=torch.int8)
sparse_map.scatter_(-1, lut, 1)
return sparse_map, lut, topk
def mean_pool(x, BLK):
assert x.is_contiguous()
B, H, L, D = x.shape
L_BLOCKS = (L + BLK - 1) // BLK
x_mean = torch.empty((B, H, L_BLOCKS, D), device=x.device, dtype=x.dtype)
grid = (L_BLOCKS, B * H)
compress_kernel[grid](x, x_mean, L, D, BLK)
return x_mean
@triton.jit
def compress_kernel(
X,
XM,
L: tl.constexpr,
D: tl.constexpr,
BLOCK_L: tl.constexpr,
):
idx_l = tl.program_id(0)
idx_bh = tl.program_id(1)
offs_l = idx_l * BLOCK_L + tl.arange(0, BLOCK_L)
offs_d = tl.arange(0, D)
x_offset = idx_bh * L * D
xm_offset = idx_bh * ((L + BLOCK_L - 1) // BLOCK_L) * D
x = tl.load(
X + x_offset + offs_l[:, None] * D + offs_d[None, :], mask=offs_l[:, None] < L
)
nx = min(BLOCK_L, L - idx_l * BLOCK_L)
x_mean = tl.sum(x, axis=0, dtype=tl.float32) / nx
tl.store(XM + xm_offset + idx_l * D + offs_d, x_mean.to(XM.dtype.element_ty))
@triton.jit
def _attn_fwd(
Q,
K,
V,
qk_scale: tl.constexpr,
topk: tl.constexpr,
LUT,
LSE,
OS,
L: tl.constexpr,
M_BLOCKS: tl.constexpr,
D: tl.constexpr,
BLOCK_M: tl.constexpr,
BLOCK_N: tl.constexpr,
):
idx_m = tl.program_id(0).to(tl.int64)
idx_bh = tl.program_id(1).to(tl.int64)
qkv_offset = idx_bh * L * D
lut_offset = (idx_bh * M_BLOCKS + idx_m) * topk
lse_offset = idx_bh * L
offs_m = idx_m * BLOCK_M + tl.arange(0, BLOCK_M)
offs_n = tl.arange(0, BLOCK_N)
offs_d = tl.arange(0, D)
Q_ptrs = Q + qkv_offset + offs_m[:, None] * D + offs_d[None, :]
K_ptrs = K + qkv_offset + offs_n[None, :] * D + offs_d[:, None]
V_ptrs = V + qkv_offset + offs_n[:, None] * D + offs_d[None, :]
OS_ptrs = OS + qkv_offset + offs_m[:, None] * D + offs_d[None, :]
LUT_ptr = LUT + lut_offset
LSE_ptrs = LSE + lse_offset + offs_m
m_i = tl.full([BLOCK_M], -float("inf"), dtype=tl.float32)
l_i = tl.zeros([BLOCK_M], dtype=tl.float32)
o_s = tl.zeros([BLOCK_M, D], dtype=tl.float32)
q = tl.load(Q_ptrs, mask=offs_m[:, None] < L)
for block_idx in tl.range(topk):
idx_n = tl.load(LUT_ptr + block_idx)
n_mask = offs_n < L - idx_n * BLOCK_N
k = tl.load(K_ptrs + idx_n * BLOCK_N * D, mask=n_mask[None, :])
qk = tl.dot(q, k) * (qk_scale * 1.4426950408889634) # = 1 / ln(2)
if L - idx_n * BLOCK_N < BLOCK_N:
qk = tl.where(n_mask[None, :], qk, float("-inf"))
v = tl.load(V_ptrs + idx_n * BLOCK_N * D, mask=n_mask[:, None])
local_m = tl.max(qk, 1)
new_m = tl.maximum(m_i, local_m)
qk = qk - new_m[:, None]
p = tl.math.exp2(qk)
l_ij = tl.sum(p, 1)
alpha = tl.math.exp2(m_i - new_m)
o_s = o_s * alpha[:, None]
o_s += tl.dot(p.to(v.dtype), v)
l_i = l_i * alpha + l_ij
m_i = new_m
o_s = o_s / l_i[:, None]
tl.store(OS_ptrs, o_s.to(OS.type.element_ty), mask=offs_m[:, None] < L)
m_i += tl.math.log2(l_i)
tl.store(LSE_ptrs, m_i, mask=offs_m < L)
def _get_cuda_arch(device_index: int) -> str:
"""Get CUDA architecture string for the given device."""
major, minor = torch.cuda.get_device_capability(device_index)
return f"sm{major}{minor}"
# ==================================SLA Class===================================
class SparseLinearAttentionBackend(AttentionBackend):
"""Sparse Linear Attention Backend for efficient attention computation."""
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SLA_ATTN
@staticmethod
def get_impl_cls() -> type["SparseLinearAttentionImpl"]:
return SparseLinearAttentionImpl
@staticmethod
def get_metadata_cls() -> type["SparseLinearAttentionMetadata"]:
return SparseLinearAttentionMetadata
@staticmethod
def get_builder_cls() -> type["SparseLinearAttentionMetadataBuilder"]:
return SparseLinearAttentionMetadataBuilder
@dataclass
class SparseLinearAttentionMetadata(AttentionMetadata):
"""Metadata for Sparse Linear Attention computation."""
# Basic attention parameters
current_timestep: int
# Sparse attention configuration
topk_ratio: float = 0.1
class SparseLinearAttentionMetadataBuilder(AttentionMetadataBuilder):
"""Builder for SparseLinearAttentionMetadata."""
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build(
self,
current_timestep: int,
topk_ratio: float = 0.1,
**kwargs: dict[str, Any],
) -> SparseLinearAttentionMetadata:
return SparseLinearAttentionMetadata(
current_timestep=current_timestep,
topk_ratio=topk_ratio,
)
class SparseLinearAttentionImpl(AttentionImpl, nn.Module):
"""Implementation of sparse linear attention for the backend."""
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool = False,
softmax_scale: float | None = None,
num_kv_heads: int | None = None,
prefix: str = "",
# SLA-specific parameters - matched to TurboDiffusion defaults
topk_ratio: float = 0.1, # TurboDiffusion uses topk=0.1
feature_map: str = "softmax",
BLKQ: int = 128, # TurboDiffusion uses BLKQ=128
BLKK: int = 64, # TurboDiffusion uses BLKK=64
use_bf16: bool = True,
**extra_impl_args,
) -> None:
nn.Module.__init__(self)
# SLA-specific config
self.topk_ratio = topk_ratio
self.BLKQ = BLKQ
self.BLKK = BLKK
self.dtype = torch.bfloat16 if use_bf16 else torch.float16
# Learnable linear projection for combining sparse + linear attention
self.proj_l = nn.Linear(head_size, head_size, dtype=torch.float32)
# Feature map for linear attention
# Type annotation for callables
self.feature_map_q: Callable[[torch.Tensor], torch.Tensor]
self.feature_map_k: Callable[[torch.Tensor], torch.Tensor]
if feature_map == "elu":
self.feature_map_q = lambda x: F.elu(x) + 1
self.feature_map_k = lambda x: F.elu(x) + 1
elif feature_map == "relu":
self.feature_map_q = F.relu
self.feature_map_k = F.relu
elif feature_map == "softmax":
self.feature_map_q = lambda x: F.softmax(x, dim=-1)
self.feature_map_k = lambda x: F.softmax(x, dim=-1)
else:
raise ValueError(f"Unknown feature map: {feature_map}")
self._init_weights()
def _init_weights(self) -> None:
"""Initialize projection weights to zero for residual-like behavior."""
with torch.no_grad():
nn.init.zeros_(self.proj_l.weight)
nn.init.zeros_(self.proj_l.bias) # type: ignore[arg-type]
def _calc_linear_attention_with_torch(self, q, k, v):
kv = torch.matmul(k.transpose(-1, -2), v)
k_sum = torch.sum(k, dim=-2, keepdim=True)
return torch.matmul(q, kv) / (1e-5 + torch.matmul(q, k_sum.transpose(-1, -2)))
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: SparseLinearAttentionMetadata = None,
) -> torch.Tensor:
"""Forward pass for sparse linear attention.
Args:
query: query tensor of shape (B, H, L, D)
key: key tensor of shape (B, H, L, D)
value: value tensor of shape (B, H, L, D)
attn_metadata: attention metadata containing configuration
Returns:
output tensor of shape (B, H, L, D)
"""
dtype = query.dtype
# Transpose for computation
query = query.transpose(1, 2).contiguous()
key = key.transpose(1, 2).contiguous()
value = value.transpose(1, 2).contiguous()
# Get sparse attention map
sparse_map, lut, real_topk = get_block_map(
query, key, topk_ratio=self.topk_ratio, BLKQ=self.BLKQ, BLKK=self.BLKK
)
# Convert to computation dtype
query = query.to(self.dtype)
key = key.to(self.dtype)
value = value.to(self.dtype)
# Sparse attention computation
o_s = _attention.apply(
query, key, value, sparse_map, lut, real_topk, self.BLKQ, self.BLKK
)
# Apply feature maps
query = self.feature_map_q(query).to(self.dtype) # c_q
key = self.feature_map_k(key).to(self.dtype) # c_k
# Linear attention computation
o_l = self._calc_linear_attention_with_torch(query, key, value)
# Apply projection and combine results
with torch.amp.autocast("cuda", dtype=self.dtype):
o_l = self.proj_l(o_l)
# Combine sparse and linear attention
output = (o_s + o_l).to(dtype).transpose(1, 2)
return output
class _attention(torch.autograd.Function):
@staticmethod
def forward(ctx, q, k, v, k_block_id, lut, topk, BLOCK_M, BLOCK_N, qk_scale=None):
assert q.is_contiguous() and k.is_contiguous() and v.is_contiguous()
assert k_block_id.is_contiguous() and lut.is_contiguous()
# We recommend the following two settings
assert BLOCK_M == 64 or BLOCK_M == 128
assert BLOCK_N == 64
B, H, L, D = q.shape
if qk_scale is None:
qk_scale = D**-0.5
M_BLOCKS = triton.cdiv(L, BLOCK_M)
o_s = torch.empty_like(v)
lse = torch.empty(q.shape[:-1], device=q.device, dtype=torch.float32)
grid = (M_BLOCKS, B * H)
_attn_fwd[grid](
q,
k,
v,
qk_scale,
topk,
lut,
lse,
o_s,
L,
M_BLOCKS,
D,
BLOCK_M,
BLOCK_N,
num_warps=4 if q.shape[-1] == 64 else 8,
num_stages=3,
)
ctx.save_for_backward(q, k, v, k_block_id, lut, lse, o_s)
ctx.qk_scale = qk_scale
ctx.topk = topk
ctx.BLOCK_M = BLOCK_M
ctx.BLOCK_N = BLOCK_N
return o_s
# ==================================SageSLA Class===================================
SAGESLA_ENABLED = True
try:
import spas_sage_attn._fused as fused
import spas_sage_attn._qattn as qattn
from spas_sage_attn.utils import block_map_lut_triton, get_vanilla_qk_quant
except ImportError:
SAGESLA_ENABLED = False
SAGE2PP_ENABLED = True
try:
from spas_sage_attn._qattn import (
qk_int8_sv_f8_accum_f16_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold,
)
except ImportError:
SAGE2PP_ENABLED = False
class SageSparseLinearAttentionBackend(AttentionBackend):
"""Quantized Sparse-Linear Attention backend using SageAttention kernels."""
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SAGE_SLA_ATTN
@staticmethod
def get_impl_cls() -> type["SageSparseLinearAttentionImpl"]:
return SageSparseLinearAttentionImpl
@staticmethod
def get_metadata_cls() -> type["SageSparseLinearAttentionMetadata"]:
return SageSparseLinearAttentionMetadata
@staticmethod
def get_builder_cls() -> type["SageSparseLinearAttentionMetadataBuilder"]:
return SageSparseLinearAttentionMetadataBuilder
@dataclass
class SageSparseLinearAttentionMetadata(AttentionMetadata):
"""Metadata for Sage Sparse Linear Attention computation."""
# Basic attention parameters
current_timestep: int
# Sparse attention configuration
topk_ratio: float = 0.1
class SageSparseLinearAttentionMetadataBuilder(AttentionMetadataBuilder):
"""Builder for SageSparseLinearAttentionMetadata."""
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build(
self,
current_timestep: int,
topk_ratio: float = 0.1,
**kwargs: dict[str, Any],
) -> SageSparseLinearAttentionMetadata:
return SageSparseLinearAttentionMetadata(
current_timestep=current_timestep,
topk_ratio=topk_ratio,
)
class SageSparseLinearAttentionImpl(AttentionImpl, nn.Module):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool = False,
softmax_scale: float | None = None,
num_kv_heads: int | None = None,
prefix: str = "",
topk_ratio: float = 0.5,
feature_map: str = "softmax",
use_bf16: bool = True,
**extra_impl_args,
) -> None:
nn.Module.__init__(self)
assert (
SAGESLA_ENABLED
), "Install spas_sage_attn(pip install git+https://github.com/thu-ml/SpargeAttn.git --no-build-isolation) first to enable SageSLA."
self.num_heads = num_heads
self.head_size = head_size
self.softmax_scale = softmax_scale if softmax_scale else head_size**-0.5
self.causal = causal
self.prefix = prefix
self.topk_ratio = topk_ratio
self.dtype = torch.bfloat16 if use_bf16 else torch.float16
# Learnable linear projection for combining sparse + linear attention
self.proj_l = nn.Linear(head_size, head_size, dtype=torch.float32)
# Feature map for linear attention
# Type annotation for callables
self.feature_map_q: Callable[[torch.Tensor], torch.Tensor]
self.feature_map_k: Callable[[torch.Tensor], torch.Tensor]
if feature_map == "elu":
self.feature_map_q = lambda x: F.elu(x) + 1
self.feature_map_k = lambda x: F.elu(x) + 1
elif feature_map == "relu":
self.feature_map_q = F.relu
self.feature_map_k = F.relu
elif feature_map == "softmax":
self.feature_map_q = lambda x: F.softmax(x, dim=-1)
self.feature_map_k = lambda x: F.softmax(x, dim=-1)
else:
raise ValueError(f"Unknown feature map: {feature_map}")
self._init_weights()
def _init_weights(self) -> None:
"""Initialize projection weights to zero for residual-like behavior."""
with torch.no_grad():
nn.init.zeros_(self.proj_l.weight)
nn.init.zeros_(self.proj_l.bias) # type: ignore[arg-type]
def _calc_linear_attention_with_torch(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
):
kv = torch.matmul(k.transpose(-1, -2), v)
k_sum = torch.sum(k, dim=-2, keepdim=True)
return torch.matmul(q, kv) / (1e-5 + torch.matmul(q, k_sum.transpose(-1, -2)))
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
"""Forward pass for Sage Sparse Linear attention with quantized kernels.
Args:
query: query tensor of shape (B, L, H, D)
key: key tensor of shape (B, L, H, D)
value: value tensor of shape (B, L, H, D)
attn_metadata: attention metadata containing configuration
Returns:
output tensor of shape (B, L, H, D)
"""
dtype = query.dtype
# Transpose from (B, L, H, D) to SLA format (B, H, L, D)
q = query.transpose(1, 2).contiguous()
k = key.transpose(1, 2).contiguous()
v = value.transpose(1, 2).contiguous()
# Determine block sizes based on GPU architecture
arch = _get_cuda_arch(q.device.index)
if arch == "sm90":
BLKQ = 64
BLKK = 128
else:
BLKQ = 128
BLKK = 64
# Compute block-sparse attention pattern
sparse_map, lut, real_topk = get_block_map(
q, k, topk_ratio=self.topk_ratio, BLKQ=BLKQ, BLKK=BLKK
)
# Convert to compute dtype
q = q.to(self.dtype)
k = k.to(self.dtype)
v = v.to(self.dtype)
########## SPARGE BEGIN ##########
km = k.mean(dim=-2, keepdim=True)
headdim = q.size(-1)
assert headdim in [
64,
128,
], "headdim should be in [64, 128]. For other headdim, you can use padding and specify the softmax scale."
# Quantize Q, K to INT8
q_int8, q_scale, k_int8, k_scale = get_vanilla_qk_quant(q, k, km, BLKQ, BLKK)
lut, valid_block_num = block_map_lut_triton(sparse_map)
scale = 1.0 / (headdim**0.5)
o_s = torch.empty_like(q)
if arch in ("sm80", "sm86", "sm87"):
pvthreshold = torch.full(
(q.shape[-3],), 1e6, dtype=torch.float32, device=q.device
)
v_fp16 = v.to(torch.float16)
qattn.qk_int8_sv_f16_accum_f16_block_sparse_attn_inst_buf_with_pv_threshold(
q_int8,
k_int8,
v_fp16,
o_s,
lut,
valid_block_num,
pvthreshold,
q_scale,
k_scale,
1,
False,
1,
scale,
0,
)
else:
b, h_kv, kv_len, head_dim = v.shape
padded_len = (kv_len + 127) // 128 * 128
v_transposed_permutted = torch.empty(
(b, h_kv, head_dim, padded_len), dtype=v.dtype, device=v.device
)
fused.transpose_pad_permute_cuda(v, v_transposed_permutted, 1)
v_fp8 = torch.empty(
v_transposed_permutted.shape, dtype=torch.float8_e4m3fn, device=v.device
)
v_scale = torch.empty(
(b, h_kv, head_dim), dtype=torch.float32, device=v.device
)
fused.scale_fuse_quant_cuda(
v_transposed_permutted, v_fp8, v_scale, kv_len, 2.25, 1
)
if arch == "sm90":
qattn.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_sm90(
q_int8,
k_int8,
v_fp8,
o_s,
lut,
valid_block_num,
q_scale,
k_scale,
v_scale,
1,
False,
1,
scale,
)
else:
pvthreshold = torch.full(
(q.shape[-3],), 1e6, dtype=torch.float32, device=q.device
)
if SAGE2PP_ENABLED:
qk_int8_sv_f8_accum_f16_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold(
q_int8,
k_int8,
v_fp8,
o_s,
lut,
valid_block_num,
pvthreshold,
q_scale,
k_scale,
v_scale,
1,
False,
1,
scale,
0,
)
else:
qattn.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold(
q_int8,
k_int8,
v_fp8,
o_s,
lut,
valid_block_num,
pvthreshold,
q_scale,
k_scale,
v_scale,
1,
False,
1,
scale,
0,
)
########## SPARGE END ##########
# Linear attention with feature maps
q_linear = self.feature_map_q(q).to(self.dtype)
k_linear = self.feature_map_k(k).to(self.dtype)
o_l = self._calc_linear_attention_with_torch(q_linear, k_linear, v)
# Project linear attention output and combine
with torch.amp.autocast("cuda", dtype=self.dtype):
o_l = self.proj_l(o_l)
# Combine sparse and linear outputs
output = (o_s + o_l).to(dtype).transpose(1, 2)
return output
@@ -0,0 +1,562 @@
"""
Sparse Video Gen 2 (SAP) attention backend.
This is a baseline integration that wires the backend into the
attention framework.
Adapted from https://github.com/svg-project/Sparse-VideoGen/blob/main/svg/models/wan/attention.py
"""
from dataclasses import dataclass, field
from typing import Any
import torch
import torch.nn.functional as F
from torch.nn.attention import SDPBackend, sdpa_kernel
try:
from svg.kernels.triton.permute import (
apply_inverse_permutation_triton,
permute_tensor_by_labels_triton,
)
from svg.kmeans_utils import (
batch_kmeans_Euclid,
dynamic_block_sparse_fwd_flashinfer,
identify_dynamic_map,
)
svg2_available = True
except ImportError:
svg2_available = False
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class SparseVideoGen2AttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 128, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.SPARSE_VIDEO_GEN_2_ATTN
@staticmethod
def get_impl_cls() -> type["SparseVideoGen2AttentionImpl"]:
return SparseVideoGen2AttentionImpl
@staticmethod
def get_metadata_cls() -> type["SparseVideoGen2AttentionMetadata"]:
return SparseVideoGen2AttentionMetadata
@staticmethod
def get_builder_cls() -> type["SparseVideoGen2AttentionMetadataBuilder"]:
return SparseVideoGen2AttentionMetadataBuilder
@dataclass
class Svg2LayerCache:
# centroids for kmeans clustering
q_centroids: torch.Tensor | None = None
k_centroids: torch.Tensor | None = None
centroids_initialized: bool = False
@dataclass
class Svg2Cache:
layers: dict[int, Svg2LayerCache] = field(default_factory=dict)
def get_layer(self, layer_idx: int) -> Svg2LayerCache:
layer_cache = self.layers.get(layer_idx)
if layer_cache is None:
layer_cache = Svg2LayerCache()
self.layers[layer_idx] = layer_cache
return layer_cache
@dataclass
class SparseVideoGen2AttentionMetadata(AttentionMetadata):
current_timestep: int
num_q_centroids: int
num_k_centroids: int
top_p_kmeans: float
min_kc_ratio: float
kmeans_iter_init: int
kmeans_iter_step: int
zero_step_kmeans_init: bool
first_layers_fp: float
first_times_fp: float
context_length: int
num_frame: int
frame_size: int
cache: Svg2Cache
prompt_length: int | None = None
max_seqlen_q: int | None = None
max_seqlen_k: int | None = None
def _require_kwarg(kwargs: dict[str, Any], name: str) -> Any:
if name not in kwargs:
raise ValueError(
f"Missing required argument for SparseVideoGen2Attention: {name}"
)
return kwargs[name]
class SparseVideoGen2AttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self) -> None:
pass
def prepare(self) -> None:
pass
def build( # type: ignore[override]
self,
current_timestep: int,
raw_latent_shape: tuple[int, ...],
patch_size: tuple[int, int, int],
cache: Svg2Cache,
num_q_centroids: int,
num_k_centroids: int,
top_p_kmeans: float,
min_kc_ratio: float,
kmeans_iter_init: int,
kmeans_iter_step: int,
zero_step_kmeans_init: bool,
first_layers_fp: float,
first_times_fp: float,
context_length: int = 0,
prompt_length: int | None = None,
**kwargs: dict[str, Any],
) -> SparseVideoGen2AttentionMetadata:
raw_shape = tuple(raw_latent_shape)
if len(raw_shape) == 5:
t, h, w = raw_shape[2:5]
elif len(raw_shape) == 3:
t, h, w = raw_shape
else:
raise ValueError(
"raw_latent_shape must be (T, H, W) or (B, C, T, H, W) for SAP attention"
)
pt, ph, pw = patch_size
if t % pt != 0 or h % ph != 0 or w % pw != 0:
raise ValueError(
"raw_latent_shape must be divisible by patch_size for SAP attention"
)
num_frame = t // pt
frame_size = (h // ph) * (w // pw)
return SparseVideoGen2AttentionMetadata(
current_timestep=current_timestep,
num_q_centroids=num_q_centroids,
num_k_centroids=num_k_centroids,
top_p_kmeans=top_p_kmeans,
min_kc_ratio=min_kc_ratio,
kmeans_iter_init=kmeans_iter_init,
kmeans_iter_step=kmeans_iter_step,
zero_step_kmeans_init=zero_step_kmeans_init,
first_layers_fp=first_layers_fp,
first_times_fp=first_times_fp,
context_length=context_length,
prompt_length=prompt_length,
num_frame=num_frame,
frame_size=frame_size,
cache=cache,
)
class SparseVideoGen2AttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
if causal:
raise ValueError(
"Sparse Video Gen 2 attention does not support causal attention"
)
if not svg2_available:
raise ImportError(
"Sparse Video Gen 2 attention backend requires svg package to be installed"
"Please install it by following the instructions at "
"https://github.com/svg-project/Sparse-VideoGen"
)
self.prefix = prefix
self.layer_idx = self._get_layer_idx(prefix)
def _get_layer_idx(self, prefix: str) -> int:
parts = prefix.split(".")
if len(parts) < 3:
raise ValueError(
f"Invalid prefix for SparseVideoGen2AttentionImpl: {prefix}"
)
return int(parts[-3])
def kmeans_init(
self,
query: torch.Tensor,
key: torch.Tensor,
attn_metadata: SparseVideoGen2AttentionMetadata,
):
cfg, num_heads, seq_len, dim = query.size()
qlabels, qcentroids, qcluster_sizes, qiter = batch_kmeans_Euclid(
query.reshape(cfg * num_heads, seq_len, dim),
n_clusters=attn_metadata.num_q_centroids,
max_iters=attn_metadata.kmeans_iter_init,
)
klabels, kcentroids, kcluster_sizes, kiter = batch_kmeans_Euclid(
key.reshape(cfg * num_heads, seq_len, dim),
n_clusters=attn_metadata.num_k_centroids,
max_iters=attn_metadata.kmeans_iter_init,
)
layer_cache = attn_metadata.cache.get_layer(self.layer_idx)
layer_cache.q_centroids = qcentroids
layer_cache.k_centroids = kcentroids
return (
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
)
def kmeans_step(
self,
query: torch.Tensor,
key: torch.Tensor,
attn_metadata: SparseVideoGen2AttentionMetadata,
):
cfg, num_heads, seq_len, dim = query.size()
layer_cache = attn_metadata.cache.get_layer(self.layer_idx)
qlabels, qcentroids, qcluster_sizes, qiter = batch_kmeans_Euclid(
query.reshape(cfg * num_heads, seq_len, dim),
n_clusters=attn_metadata.num_q_centroids,
max_iters=attn_metadata.kmeans_iter_step,
init_centroids=layer_cache.q_centroids,
)
klabels, kcentroids, kcluster_sizes, kiter = batch_kmeans_Euclid(
key.reshape(cfg * num_heads, seq_len, dim),
n_clusters=attn_metadata.num_k_centroids,
max_iters=attn_metadata.kmeans_iter_step,
init_centroids=layer_cache.k_centroids,
)
layer_cache.q_centroids = qcentroids
layer_cache.k_centroids = kcentroids
return (
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
)
def kmeans_clustering(
self,
query: torch.Tensor,
key: torch.Tensor,
attn_metadata: SparseVideoGen2AttentionMetadata,
):
layer_cache = attn_metadata.cache.get_layer(self.layer_idx)
if not layer_cache.centroids_initialized:
(
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
) = self.kmeans_init(query, key, attn_metadata)
layer_cache.centroids_initialized = True
logger.debug(
"Centroids initialized at layer %s (init iters: %s).",
self.layer_idx,
attn_metadata.kmeans_iter_init,
)
else:
(
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
) = self.kmeans_step(query, key, attn_metadata)
return (
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
)
def semantic_aware_permutation(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: SparseVideoGen2AttentionMetadata,
):
cfg, num_heads, seq_len, dim = query.size()
# 1. Kmeans clustering
(
qlabels,
qcentroids,
qcluster_sizes,
qiter,
klabels,
kcentroids,
kcluster_sizes,
kiter,
) = self.kmeans_clustering(query, key, attn_metadata)
# 2. Identify dynamic map
q_cluster_sizes = qcluster_sizes.view(
cfg, num_heads, attn_metadata.num_q_centroids
)
k_cluster_sizes = kcluster_sizes.view(
cfg, num_heads, attn_metadata.num_k_centroids
)
dynamic_map = identify_dynamic_map(
qcentroids.view(cfg, num_heads, attn_metadata.num_q_centroids, dim),
kcentroids.view(cfg, num_heads, attn_metadata.num_k_centroids, dim),
q_cluster_sizes,
k_cluster_sizes,
attn_metadata.top_p_kmeans,
attn_metadata.min_kc_ratio,
)
# 3. Permute the query, key, value
q_permuted, q_sorted_indices = permute_tensor_by_labels_triton(
query, qlabels, dim=2
)
k_permuted, k_sorted_indices = permute_tensor_by_labels_triton(
key, klabels, dim=2
)
v_permuted, v_sorted_indices = permute_tensor_by_labels_triton(
value, klabels, dim=2, sorted_indices=k_sorted_indices
)
return (
q_permuted,
k_permuted,
v_permuted,
dynamic_map,
q_cluster_sizes,
k_cluster_sizes,
q_sorted_indices,
)
def _hunyuan_dynamic_map_post_processing(
self,
q_perm: torch.Tensor,
k_perm: torch.Tensor,
v_perm: torch.Tensor,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
dyn_map: torch.Tensor,
qc_sz_s: torch.Tensor,
kc_sz_s: torch.Tensor,
q_sorted_indices: torch.Tensor,
video_length: int,
context_length: int,
prompt_length: int,
unprompt_length: int,
) -> tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
]:
# Place the permuted video tokens back and keep text tokens at the tail.
query[:, :, :-context_length, :] = q_perm
key[:, :, :-context_length, :] = k_perm
value[:, :, :-context_length, :] = v_perm
# Add prompt/unprompt clusters to the dynamic map.
dyn_map = F.pad(dyn_map, (0, 2, 0, 2), value=0)
dyn_map[:, :, -2, :-1] = True
dyn_map[:, :, :-1, -2] = True
dyn_map[:, :, -1, -1] = True
qc_sz_s = F.pad(qc_sz_s, (0, 2), value=0)
qc_sz_s[:, :, -2] = prompt_length
qc_sz_s[:, :, -1] = unprompt_length
kc_sz_s = F.pad(kc_sz_s, (0, 2), value=0)
kc_sz_s[:, :, -2] = prompt_length
kc_sz_s[:, :, -1] = unprompt_length
q_sorted_indices = F.pad(q_sorted_indices, (0, context_length), value=0)
q_sorted_indices[:, video_length:] = torch.arange(
video_length,
video_length + context_length,
device=q_sorted_indices.device,
)
return query, key, value, dyn_map, qc_sz_s, kc_sz_s, q_sorted_indices
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: SparseVideoGen2AttentionMetadata,
) -> torch.Tensor:
torch.backends.cuda.preferred_linalg_library(backend="magma")
res = None
# bshd -> bhsd
query = query.transpose(1, 2).contiguous()
key = key.transpose(1, 2).contiguous()
value = value.transpose(1, 2).contiguous()
batch_size, num_heads, seq_len, dim = query.size()
context_length, num_frame, frame_size = (
attn_metadata.context_length,
attn_metadata.num_frame,
attn_metadata.frame_size,
)
prompt_length = attn_metadata.prompt_length
if prompt_length is None:
prompt_length = context_length
assert (
seq_len == context_length + num_frame * frame_size
), f"Query Shape: {seq_len} is not equivalent to {context_length} + {num_frame} * {frame_size}"
# Determine if we use Full Attention to calculate
full_attention_flag = False
if self.layer_idx < attn_metadata.first_layers_fp:
full_attention_flag = True
if attn_metadata.current_timestep > attn_metadata.first_times_fp:
full_attention_flag = True
if full_attention_flag:
if attn_metadata.zero_step_kmeans_init:
video_length = attn_metadata.num_frame * attn_metadata.frame_size
query_video = query[:, :, :video_length, :].contiguous()
key_video = key[:, :, :video_length, :].contiguous()
self.kmeans_clustering(query_video, key_video, attn_metadata)
with sdpa_kernel(
SDPBackend.CUDNN_ATTENTION
): # not sure why we need to force cudnn here, but it's faster than flash attention
output_hidden_states = torch.nn.functional.scaled_dot_product_attention(
query, key, value, dropout_p=0.0, is_causal=False
)
res = output_hidden_states.reshape(
batch_size, num_heads, seq_len, dim
).transpose(1, 2)
else:
if context_length > 0:
video_length = num_frame * frame_size
unprompt_length = max(context_length - prompt_length, 0)
query_video = query[:, :, :video_length, :].contiguous()
key_video = key[:, :, :video_length, :].contiguous()
value_video = value[:, :, :video_length, :].contiguous()
(
q_perm,
k_perm,
v_perm,
dyn_map,
qc_sz_s,
kc_sz_s,
q_sorted_indices,
) = self.semantic_aware_permutation(
query_video, key_video, value_video, attn_metadata
)
(
q_perm,
k_perm,
v_perm,
dyn_map,
qc_sz_s,
kc_sz_s,
q_sorted_indices,
) = self._hunyuan_dynamic_map_post_processing(
q_perm,
k_perm,
v_perm,
query,
key,
value,
dyn_map,
qc_sz_s,
kc_sz_s,
q_sorted_indices,
video_length,
context_length,
prompt_length,
unprompt_length,
)
else:
(
q_perm,
k_perm,
v_perm,
dyn_map,
qc_sz_s,
kc_sz_s,
q_sorted_indices,
) = self.semantic_aware_permutation(query, key, value, attn_metadata)
output_permuted = dynamic_block_sparse_fwd_flashinfer(
q_perm, k_perm, v_perm, dyn_map, qc_sz_s, kc_sz_s, is_cpu=False
)
attn_output = apply_inverse_permutation_triton(
output_permuted, q_sorted_indices, dim=2
)
res = attn_output.reshape(batch_size, num_heads, seq_len, dim).transpose(
1, 2
)
torch.backends.cuda.preferred_linalg_library(
backend="default"
) # reset to default
return res.contiguous()
@@ -0,0 +1,330 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import functools
import math
from dataclasses import dataclass
import torch
try:
from vsa import video_sparse_attn
except ImportError:
video_sparse_attn = None
from typing import Any
from sglang.multimodal_gen.runtime.distributed import get_sp_group
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
VSA_TILE_SIZE = (4, 4, 4)
@functools.lru_cache(maxsize=10)
def get_tile_partition_indices(
dit_seq_shape: tuple[int, int, int],
tile_size: tuple[int, int, int],
device: torch.device,
) -> torch.LongTensor:
T, H, W = dit_seq_shape
ts, hs, ws = tile_size
indices = torch.arange(T * H * W, device=device, dtype=torch.long).reshape(T, H, W)
ls = []
for t in range(math.ceil(T / ts)):
for h in range(math.ceil(H / hs)):
for w in range(math.ceil(W / ws)):
ls.append(
indices[
t * ts : min(t * ts + ts, T),
h * hs : min(h * hs + hs, H),
w * ws : min(w * ws + ws, W),
].flatten()
)
index = torch.cat(ls, dim=0)
return index
@functools.lru_cache(maxsize=10)
def get_reverse_tile_partition_indices(
dit_seq_shape: tuple[int, int, int],
tile_size: tuple[int, int, int],
device: torch.device,
) -> torch.LongTensor:
return torch.argsort(get_tile_partition_indices(dit_seq_shape, tile_size, device))
@functools.lru_cache(maxsize=10)
def construct_variable_block_sizes(
dit_seq_shape: tuple[int, int, int],
num_tiles: tuple[int, int, int],
device: torch.device,
) -> torch.LongTensor:
"""
Compute the number of valid (nonpadded) tokens inside every
(ts_t × ts_h × ts_w) tile after padding ‑‑ flattened in the order
(ttile, htile, wtile) that `rearrange` uses.
Returns
-------
torch.LongTensor # shape: [∏ full_window_size]
"""
# unpack
t, h, w = dit_seq_shape
ts_t, ts_h, ts_w = VSA_TILE_SIZE
n_t, n_h, n_w = num_tiles
def _sizes(dim_len: int, tile: int, n_tiles: int) -> torch.LongTensor:
"""Vector with the size of each tile along one dimension."""
sizes = torch.full((n_tiles,), tile, dtype=torch.int, device=device)
# size of last (possibly partial) tile
remainder = dim_len - (n_tiles - 1) * tile
sizes[-1] = remainder if remainder > 0 else tile
return sizes
t_sizes = _sizes(t, ts_t, n_t) # [n_t]
h_sizes = _sizes(h, ts_h, n_h) # [n_h]
w_sizes = _sizes(w, ts_w, n_w) # [n_w]
# broadcastmultiply to get voxels per tile, then flatten
block_sizes = (
t_sizes[:, None, None] # [n_t, 1, 1]
* h_sizes[None, :, None] # [1, n_h, 1]
* w_sizes[None, None, :] # [1, 1, n_w]
).reshape(
-1
) # [n_t * n_h * n_w]
return block_sizes
@functools.lru_cache(maxsize=10)
def get_non_pad_index(
variable_block_sizes: torch.LongTensor,
max_block_size: int,
):
n_win = variable_block_sizes.shape[0]
device = variable_block_sizes.device
starts_pad = torch.arange(n_win, device=device) * max_block_size
index_pad = (
starts_pad[:, None] + torch.arange(max_block_size, device=device)[None, :]
)
index_mask = (
torch.arange(max_block_size, device=device)[None, :]
< variable_block_sizes[:, None]
)
return index_pad[index_mask]
class VideoSparseAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 128]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.VIDEO_SPARSE_ATTN
@staticmethod
def get_impl_cls() -> type["VideoSparseAttentionImpl"]:
return VideoSparseAttentionImpl
@staticmethod
def get_metadata_cls() -> type["VideoSparseAttentionMetadata"]:
return VideoSparseAttentionMetadata
@staticmethod
def get_builder_cls() -> type["VideoSparseAttentionMetadataBuilder"]:
return VideoSparseAttentionMetadataBuilder
@dataclass
class VideoSparseAttentionMetadata(AttentionMetadata):
current_timestep: int
dit_seq_shape: list[int]
VSA_sparsity: float
num_tiles: list[int]
total_seq_length: int
tile_partition_indices: torch.LongTensor
reverse_tile_partition_indices: torch.LongTensor
variable_block_sizes: torch.LongTensor
non_pad_index: torch.LongTensor
untile_combined_index: torch.LongTensor
tile_buf: torch.Tensor | None = None
# adaption for FastWan2.1-T2V-1.3B-Diffusers
# Sequence lengths for the forward batch
# Maximum sequence length for query
max_seqlen_q: int = 1
# Maximum sequence length for key
max_seqlen_k: int = 0
class VideoSparseAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self):
pass
def prepare(self):
pass
def build( # type: ignore
self,
current_timestep: int,
raw_latent_shape: tuple[int, int, int],
patch_size: tuple[int, int, int],
VSA_sparsity: float,
device: torch.device,
**kwargs: dict[str, Any],
) -> VideoSparseAttentionMetadata:
patch_size = patch_size
dit_seq_shape = (
raw_latent_shape[0] // patch_size[0],
raw_latent_shape[1] // patch_size[1],
raw_latent_shape[2] // patch_size[2],
)
num_tiles = (
math.ceil(dit_seq_shape[0] / VSA_TILE_SIZE[0]),
math.ceil(dit_seq_shape[1] / VSA_TILE_SIZE[1]),
math.ceil(dit_seq_shape[2] / VSA_TILE_SIZE[2]),
)
total_seq_length = math.prod(dit_seq_shape)
tile_partition_indices = get_tile_partition_indices(
dit_seq_shape, VSA_TILE_SIZE, device
)
reverse_tile_partition_indices = get_reverse_tile_partition_indices(
dit_seq_shape, VSA_TILE_SIZE, device
)
variable_block_sizes = construct_variable_block_sizes(
dit_seq_shape, num_tiles, device
)
non_pad_index = get_non_pad_index(
variable_block_sizes, math.prod(VSA_TILE_SIZE)
)
untile_combined_index = non_pad_index[reverse_tile_partition_indices]
return VideoSparseAttentionMetadata(
current_timestep=current_timestep,
dit_seq_shape=dit_seq_shape, # type: ignore
VSA_sparsity=VSA_sparsity, # type: ignore
num_tiles=num_tiles, # type: ignore
total_seq_length=total_seq_length, # type: ignore
tile_partition_indices=tile_partition_indices, # type: ignore
reverse_tile_partition_indices=reverse_tile_partition_indices,
variable_block_sizes=variable_block_sizes,
non_pad_index=non_pad_index,
untile_combined_index=untile_combined_index,
)
class VideoSparseAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.prefix = prefix
sp_group = get_sp_group()
self.sp_size = sp_group.world_size
def tile(
self,
x: torch.Tensor,
attn_metadata: VideoSparseAttentionMetadata,
) -> torch.Tensor:
num_tiles = attn_metadata.num_tiles
t_padded_size = num_tiles[0] * VSA_TILE_SIZE[0]
h_padded_size = num_tiles[1] * VSA_TILE_SIZE[1]
w_padded_size = num_tiles[2] * VSA_TILE_SIZE[2]
target_shape = (
x.shape[0],
t_padded_size * h_padded_size * w_padded_size,
x.shape[-2],
x.shape[-1],
)
buf = attn_metadata.tile_buf
if (
buf is None
or buf.shape != target_shape
or buf.dtype != x.dtype
or buf.device != x.device
):
buf = torch.zeros(target_shape, device=x.device, dtype=x.dtype)
attn_metadata.tile_buf = buf
buf[:, attn_metadata.non_pad_index] = x[:, attn_metadata.tile_partition_indices]
return buf
def untile(
self,
x: torch.Tensor,
untile_combined_index: torch.LongTensor,
) -> torch.Tensor:
return x[:, untile_combined_index]
def preprocess_qkv(
self,
qkv: torch.Tensor,
attn_metadata: VideoSparseAttentionMetadata,
) -> torch.Tensor:
return self.tile(qkv, attn_metadata)
def postprocess_output(
self,
output: torch.Tensor,
attn_metadata: VideoSparseAttentionMetadata,
) -> torch.Tensor:
return self.untile(output, attn_metadata.untile_combined_index)
def forward( # type: ignore[override]
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
gate_compress: torch.Tensor,
attn_metadata: VideoSparseAttentionMetadata,
) -> torch.Tensor:
query = query.transpose(1, 2).contiguous()
key = key.transpose(1, 2).contiguous()
value = value.transpose(1, 2).contiguous()
gate_compress = gate_compress.transpose(1, 2).contiguous()
VSA_sparsity = attn_metadata.VSA_sparsity
cur_topk = math.ceil(
(1 - VSA_sparsity)
* (attn_metadata.total_seq_length / math.prod(VSA_TILE_SIZE))
)
if video_sparse_attn is None:
raise NotImplementedError("video_sparse_attn is not installed")
hidden_states = video_sparse_attn(
query,
key,
value,
variable_block_sizes=attn_metadata.variable_block_sizes,
topk=cur_topk,
block_size=VSA_TILE_SIZE,
compress_attn_weight=gate_compress,
).transpose(1, 2)
return hidden_states
@@ -0,0 +1,259 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import re
from dataclasses import dataclass
import torch
from einops import rearrange
from kernel.attn.vmoba_attn.vmoba import (
moba_attn_varlen,
process_moba_input,
process_moba_output,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class VMOBAAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.VMOBA_ATTN
@staticmethod
def get_impl_cls() -> type["VMOBAAttentionImpl"]:
return VMOBAAttentionImpl
@staticmethod
def get_metadata_cls() -> type["VideoMobaAttentionMetadata"]:
return VideoMobaAttentionMetadata
@staticmethod
def get_builder_cls() -> type["VideoMobaAttentionMetadataBuilder"]:
return VideoMobaAttentionMetadataBuilder
@dataclass
class VideoMobaAttentionMetadata(AttentionMetadata):
current_timestep: int
temporal_chunk_size: int
temporal_topk: int
spatial_chunk_size: tuple[int, int]
spatial_topk: int
st_chunk_size: tuple[int, int, int]
st_topk: int
moba_select_mode: str
moba_threshold: float
moba_threshold_type: str
patch_resolution: list[int]
first_full_step: int = 12
first_full_layer: int = 0
# temporal_layer -> spatial_layer -> st_layer
temporal_layer: int = 1
spatial_layer: int = 1
st_layer: int = 1
def pad_input(hidden_states, indices, batch, seqlen):
"""
Arguments:
hidden_states: (total_nnz, ...), where total_nnz = number of tokens in selected in attention_mask.
indices: (total_nnz), the indices that represent the non-masked tokens of the original padded input sequence.
batch: int, batch size for the padded sequence.
seqlen: int, maximum sequence length for the padded sequence.
Return:
hidden_states: (batch, seqlen, ...)
"""
dim = hidden_states.shape[1:]
output = torch.zeros(
(batch * seqlen), *dim, device=hidden_states.device, dtype=hidden_states.dtype
)
output[indices] = hidden_states
return rearrange(output, "(b s) ... -> b s ...", b=batch)
class VideoMobaAttentionMetadataBuilder(AttentionMetadataBuilder):
def __init__(self):
pass
def prepare(self):
pass
def build( # type: ignore
self,
current_timestep: int,
raw_latent_shape: tuple[int, int, int],
patch_size: tuple[int, int, int],
temporal_chunk_size: int,
temporal_topk: int,
spatial_chunk_size: tuple[int, int],
spatial_topk: int,
st_chunk_size: tuple[int, int, int],
st_topk: int,
moba_select_mode: str = "threshold",
moba_threshold: float = 0.25,
moba_threshold_type: str = "query_head",
device: torch.device = None,
first_full_layer: int = 0,
first_full_step: int = 12,
temporal_layer: int = 1,
spatial_layer: int = 1,
st_layer: int = 1,
**kwargs,
) -> VideoMobaAttentionMetadata:
if device is None:
device = torch.device("cpu")
assert (
raw_latent_shape[0] % patch_size[0] == 0
and raw_latent_shape[1] % patch_size[1] == 0
and raw_latent_shape[2] % patch_size[2] == 0
), f"spatial patch_resolution {raw_latent_shape} should be divisible by patch_size {patch_size}"
patch_resolution = [
t // pt for t, pt in zip(raw_latent_shape, patch_size, strict=False)
]
return VideoMobaAttentionMetadata(
current_timestep=current_timestep,
temporal_chunk_size=temporal_chunk_size,
temporal_topk=temporal_topk,
spatial_chunk_size=spatial_chunk_size,
spatial_topk=spatial_topk,
st_chunk_size=st_chunk_size,
st_topk=st_topk,
moba_select_mode=moba_select_mode,
moba_threshold=moba_threshold,
moba_threshold_type=moba_threshold_type,
patch_resolution=patch_resolution,
first_full_layer=first_full_layer,
first_full_step=first_full_step,
temporal_layer=temporal_layer,
spatial_layer=spatial_layer,
st_layer=st_layer,
)
class VMOBAAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads,
head_size,
softmax_scale,
causal=False,
num_kv_heads=None,
prefix="",
**extra_impl_args,
) -> None:
self.prefix = prefix
self.layer_idx = self._get_layer_idx(prefix)
self.pad_input = pad_input
def _get_layer_idx(self, prefix: str) -> int | None:
match = re.search(r"blocks\.(\d+)", prefix)
if not match:
raise ValueError(f"Invalid prefix: {prefix}")
return int(match.group(1))
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
"""
query: [B, L, H, D]
key: [B, L, H, D]
value: [B, L, H, D]
attn_metadata: AttentionMetadata
"""
batch_size, sequence_length, num_heads, head_dim = query.shape
# select chunk type according to layer idx:
loop_layer_num = (
attn_metadata.temporal_layer
+ attn_metadata.spatial_layer
+ attn_metadata.st_layer
)
moba_layer = self.layer_idx - attn_metadata.first_full_layer
if moba_layer % loop_layer_num < attn_metadata.temporal_layer:
moba_chunk_size = attn_metadata.temporal_chunk_size
moba_topk = attn_metadata.temporal_topk
elif (
moba_layer % loop_layer_num
< attn_metadata.temporal_layer + attn_metadata.spatial_layer
):
moba_chunk_size = attn_metadata.spatial_chunk_size
moba_topk = attn_metadata.spatial_topk
elif (
moba_layer % loop_layer_num
< attn_metadata.temporal_layer
+ attn_metadata.spatial_layer
+ attn_metadata.st_layer
):
moba_chunk_size = attn_metadata.st_chunk_size
moba_topk = attn_metadata.st_topk
query, chunk_size = process_moba_input(
query, attn_metadata.patch_resolution, moba_chunk_size
)
key, chunk_size = process_moba_input(
key, attn_metadata.patch_resolution, moba_chunk_size
)
value, chunk_size = process_moba_input(
value, attn_metadata.patch_resolution, moba_chunk_size
)
max_seqlen = query.shape[1]
indices_q = torch.arange(
0, query.shape[0] * query.shape[1], device=query.device
)
cu_seqlens = torch.arange(
0,
query.shape[0] * query.shape[1] + 1,
query.shape[1],
dtype=torch.int32,
device=query.device,
)
query = rearrange(query, "b s ... -> (b s) ...")
key = rearrange(key, "b s ... -> (b s) ...")
value = rearrange(value, "b s ... -> (b s) ...")
# current_timestep=attn_metadata.current_timestep
hidden_states = moba_attn_varlen(
query,
key,
value,
cu_seqlens=cu_seqlens,
max_seqlen=max_seqlen,
moba_chunk_size=chunk_size,
moba_topk=moba_topk,
select_mode=attn_metadata.moba_select_mode,
simsum_threshold=attn_metadata.moba_threshold,
threshold_type=attn_metadata.moba_threshold_type,
)
hidden_states = self.pad_input(
hidden_states, indices_q, batch_size, sequence_length
)
hidden_states = process_moba_output(
hidden_states, attn_metadata.patch_resolution, moba_chunk_size
)
return hidden_states
@@ -0,0 +1,122 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from functools import lru_cache
import torch
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
try:
from sgl_kernel.flash_attn import flash_attn_varlen_func
flash_attn_func = flash_attn_varlen_func
except ImportError as e:
raise e
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
AttentionImpl,
AttentionMetadata,
AttentionMetadataBuilder,
)
from sglang.multimodal_gen.runtime.layers.attention.backends.flash_attn import (
FlashAttentionMetadataBuilder,
)
class XPUAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 96, 128, 192, 256]
@staticmethod
def get_enum() -> AttentionBackendEnum:
return AttentionBackendEnum.FA
@staticmethod
def get_impl_cls() -> type["XPUAttentionImpl"]:
return XPUAttentionImpl
@staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]:
"""XPU backend does not require special metadata."""
return AttentionMetadata
@staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]:
return FlashAttentionMetadataBuilder
@lru_cache(maxsize=128)
def _get_cu_seqlens(device_index: int, bsz: int, seqlen: int) -> torch.Tensor:
return torch.arange(
0,
(bsz + 1) * seqlen,
step=seqlen,
device=torch.device("xpu", device_index),
dtype=torch.int32,
)
class XPUAttentionImpl(AttentionImpl):
def __init__(
self,
num_heads: int,
head_size: int,
causal: bool,
softmax_scale: float,
num_kv_heads: int | None = None,
prefix: str = "",
**extra_impl_args,
) -> None:
self.num_heads = num_heads
self.num_kv_heads = num_kv_heads
self.head_size = head_size
self.causal = causal
self.softmax_scale = softmax_scale
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata = None,
*,
return_softmax_lse: bool = False,
):
bsz, seqlen_q, nheads_q, d = tuple(query.shape)
_, seqlen_k, nheads_k, _ = tuple(key.shape)
max_seqlen_q = seqlen_q
max_seqlen_k = seqlen_k
q_ = query.contiguous().reshape(bsz * seqlen_q, nheads_q, d)
k_ = key.contiguous().reshape(bsz * seqlen_k, nheads_k, d)
v_ = value.contiguous().reshape(bsz * seqlen_k, nheads_k, d)
cu_q = _get_cu_seqlens(q_.device.index, bsz, seqlen_q)
cu_k = _get_cu_seqlens(q_.device.index, bsz, seqlen_k)
out = flash_attn_func(
q=q_,
k=k_,
v=v_,
cu_seqlens_q=cu_q,
cu_seqlens_k=cu_k,
max_seqlen_q=max_seqlen_q,
max_seqlen_k=max_seqlen_k,
softmax_scale=self.softmax_scale,
causal=self.causal,
return_softmax_lse=return_softmax_lse,
)
if return_softmax_lse:
out_tensor, softmax_lse = out[:2]
result = out_tensor.reshape(bsz, seqlen_q, nheads_q, d)
return result, softmax_lse
result = out.reshape(bsz, seqlen_q, nheads_q, d)
return result
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,288 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/attention/selector.py
import os
from collections.abc import Generator
from contextlib import contextmanager
from contextvars import ContextVar
from functools import cache
from typing import NamedTuple, cast
import torch
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionBackend,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.server_args import get_global_server_args
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.utils import STR_BACKEND_ENV_VAR, resolve_obj_by_qualname
logger = init_logger(__name__)
def backend_name_to_enum(backend_name: str) -> AttentionBackendEnum | None:
"""
Convert a string backend name to a _Backend enum value.
Returns:
* _Backend: enum value if backend_name is a valid in-tree type
* None: otherwise it's an invalid in-tree type or an out-of-tree platform is
loaded.
"""
assert backend_name is not None
return (
AttentionBackendEnum[backend_name]
if backend_name in AttentionBackendEnum.__members__
else None
)
def get_env_variable_attn_backend() -> AttentionBackendEnum | None:
"""
Get the backend override specified by the sglang-diffusion attention
backend environment variable, if one is specified.
Returns:
* _Backend enum value if an override is specified
* None otherwise
"""
backend_name = os.environ.get(STR_BACKEND_ENV_VAR)
return None if backend_name is None else backend_name_to_enum(backend_name)
# Global state allows a particular choice of backend
# to be forced, overriding the logic which auto-selects
# a backend based on system & workload configuration
# (default behavior if this variable is None)
#
# THIS SELECTION TAKES PRECEDENCE OVER THE
# FASTVIDEO ATTENTION BACKEND ENVIRONMENT VARIABLE
forced_attn_backend: AttentionBackendEnum | None = None
class ComponentAttnBackendContext(NamedTuple):
backend: AttentionBackendEnum | None
component_name: str | None
selected_backends: dict[str, str | None]
component_attn_backend_context: ContextVar[ComponentAttnBackendContext | None] = (
ContextVar("component_attn_backend_context", default=None)
)
def global_force_attn_backend(attn_backend: AttentionBackendEnum | None) -> None:
"""
Force all attention operations to use a specified backend.
Passing `None` for the argument re-enables automatic
backend selection.,
Arguments:
* attn_backend: backend selection (None to revert to auto)
"""
global forced_attn_backend
forced_attn_backend = attn_backend
def get_global_forced_attn_backend() -> AttentionBackendEnum | None:
"""
Get the currently-forced choice of attention backend,
or None if auto-selection is currently enabled.
"""
return forced_attn_backend
def get_component_attn_backend_context() -> ComponentAttnBackendContext | None:
return component_attn_backend_context.get()
def get_component_forced_attn_backend() -> AttentionBackendEnum | None:
context = get_component_attn_backend_context()
return context.backend if context is not None else None
def get_component_attn_backend_name() -> str | None:
context = get_component_attn_backend_context()
return context.component_name if context is not None else None
def _record_component_attn_backend(backend_name: str, reason: str | None) -> bool:
context = get_component_attn_backend_context()
if context is None or context.component_name is None:
return False
existing_reason = context.selected_backends.get(backend_name)
if backend_name not in context.selected_backends or existing_reason is None:
context.selected_backends[backend_name] = reason
return True
def _log_component_attn_backend_summary(
context: ComponentAttnBackendContext | None,
) -> None:
if (
context is None
or context.component_name is None
or not context.selected_backends
):
return
backend_parts = []
for backend_name, reason in context.selected_backends.items():
if reason:
backend_parts.append(f"{backend_name} ({reason})")
else:
backend_parts.append(backend_name)
logger.info_once(
f"Attention backends for {context.component_name}: "
f"{', '.join(backend_parts)}"
)
def get_attn_backend(
head_size: int,
dtype: torch.dtype,
supported_attention_backends: set[AttentionBackendEnum] | None = None,
selected_attention_backend: AttentionBackendEnum | None = None,
) -> type[AttentionBackend]:
if supported_attention_backends is None:
be_tuple = tuple()
else:
# Sort the backend names to ensure consistent cache key
be_tuple = tuple(
sorted(list(supported_attention_backends), key=lambda b: b.name)
)
selected_backend = selected_attention_backend or get_global_forced_attn_backend()
if selected_backend is None:
selected_backend = get_component_forced_attn_backend()
if selected_backend is None:
server_args = get_global_server_args()
if server_args.attention_backend is not None:
try:
selected_backend = AttentionBackendEnum[
server_args.attention_backend.upper()
]
except KeyError:
raise ValueError(
f"Invalid attention backend '{server_args.attention_backend}' specified via command line. "
f"Available options are: {[e.name.lower() for e in AttentionBackendEnum]}"
)
constraint_backend = None
if selected_backend is None and len(be_tuple) == 1:
constraint_backend = be_tuple[0].name.lower()
attention_backend_cls = _cached_get_attn_backend(
head_size,
dtype,
be_tuple,
selected_backend,
)
backend_name = attention_backend_cls.get_enum().name.lower()
reason = "component constraint" if backend_name == constraint_backend else None
if not _record_component_attn_backend(backend_name, reason):
logger.info_once(f"Using {backend_name} attention backend")
return attention_backend_cls
@cache
def _cached_get_attn_backend(
head_size: int,
dtype: torch.dtype,
supported_attention_backends: tuple[AttentionBackendEnum],
selected_backend: AttentionBackendEnum | None,
) -> type[AttentionBackend]:
from sglang.multimodal_gen.runtime.platforms import current_platform
supported_attention_backends = set(supported_attention_backends)
# get device-specific attn_backend
if len(supported_attention_backends) == 0:
# all attention backends are allowed
pass
elif selected_backend is None and len(supported_attention_backends) == 1:
selected_backend = next(iter(supported_attention_backends))
elif (
selected_backend is not None
and selected_backend not in supported_attention_backends
):
supported_attention_backends_str = [
supported_attention_backend.__str__()
for supported_attention_backend in supported_attention_backends
]
logger.debug(
"Selected attention backend: '%s' not in supported attention backends: %s",
selected_backend,
supported_attention_backends_str,
)
selected_backend = None
attention_cls = current_platform.get_attn_backend_cls_str(
selected_backend, head_size, dtype
)
if not attention_cls:
raise ValueError(
f"Invalid attention backend for {current_platform.device_name}"
)
return cast(type[AttentionBackend], resolve_obj_by_qualname(attention_cls))
@contextmanager
def component_attn_backend_context_manager(
attn_backend: AttentionBackendEnum | None,
component_name: str | None = None,
) -> Generator[None, None, None]:
if attn_backend is None and component_name is None:
yield
return
token = component_attn_backend_context.set(
ComponentAttnBackendContext(attn_backend, component_name, {})
)
try:
yield
finally:
context = component_attn_backend_context.get()
_log_component_attn_backend_summary(context)
component_attn_backend_context.reset(token)
@contextmanager
def global_force_attn_backend_context_manager(
attn_backend: AttentionBackendEnum,
) -> Generator[None, None, None]:
"""
Globally force a sglang-diffusion attention backend override within a
context manager, reverting the global attention backend
override to its prior state upon exiting the context
manager.
Arguments:
* attn_backend: attention backend to force
Returns:
* Generator
"""
# Save the current state of the global backend override (if any)
original_value = get_global_forced_attn_backend()
# Globally force the new backend override
global_force_attn_backend(attn_backend)
# Yield control back to the enclosed code block
try:
yield
finally:
# Revert the original global backend override, if any
global_force_attn_backend(original_value)
@@ -0,0 +1,325 @@
# copy and modify from https://github.com/thu-ml/TurboDiffusion/blob/main/turbodiffusion/rcm/utils/a2a_cp.py and https://github.com/thu-ml/TurboDiffusion/blob/main/turbodiffusion/SLA/core.py
from typing import Any, Callable, List, Tuple, Type, Union
import torch
import torch.distributed as dist
from einops import rearrange
from torch import Tensor
from torch.distributed import ProcessGroup
from torch.nn import Module
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionImpl,
)
from sglang.multimodal_gen.runtime.layers.attention.selector import get_attn_backend
from sglang.multimodal_gen.runtime.managers.forward_context import (
ForwardContext,
get_forward_context,
)
from sglang.multimodal_gen.runtime.platforms.interface import AttentionBackendEnum
from sglang.multimodal_gen.runtime.server_args import get_global_server_args
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.utils import get_compute_dtype
logger = init_logger(__name__)
_TURBO_WAN_SPARSE_BACKENDS = {
AttentionBackendEnum.SLA_ATTN,
AttentionBackendEnum.SAGE_SLA_ATTN,
}
def post_all2all(local_seq_2_local_head, seq_world_size):
def post_func(input):
# b, s, n, h
if local_seq_2_local_head:
output = rearrange(input, "w bs seq h d -> bs (w seq) h d")
else:
output = rearrange(input, "w bs s h d -> bs s (w h) d", w=seq_world_size)
return output
return post_func
def single_all_to_all(input, local_seq_2_local_head, group, async_op=False):
seq_world_size = dist.get_world_size(group)
# b, s, n, h
if local_seq_2_local_head:
bs, local_seq_len, num_total_head, head_dim = input.shape
assert (
num_total_head % seq_world_size == 0
), f"Number of heads ({num_total_head}) must be divisible by the sequence parallel size ({seq_world_size})!"
input_t = rearrange(
input,
"bs seq_len (w h) d -> w bs seq_len h d",
w=seq_world_size,
h=num_total_head // seq_world_size,
).contiguous()
post_all2all_fun = post_all2all(local_seq_2_local_head, seq_world_size)
else:
bs, global_seq_len, num_local_head, head_dim = input.shape
input_t = rearrange(
input,
"bs (w s) h d -> w bs s h d",
w=seq_world_size,
s=global_seq_len // seq_world_size,
).contiguous()
post_all2all_fun = post_all2all(local_seq_2_local_head, seq_world_size)
output = torch.empty_like(input_t)
dist.all_to_all_single(output, input_t, group=group, async_op=async_op)
res = post_all2all_fun(output)
return res
def _attention_backend_from_name(
backend_name: str | None,
) -> AttentionBackendEnum | None:
if backend_name is None:
return None
try:
return AttentionBackendEnum[backend_name.upper()]
except KeyError:
return None
def _resolve_turbo_wan_sparse_backend(
attention_type: str,
requested_attention_backend: str | None = None,
supported_attention_backends: set[AttentionBackendEnum] | None = None,
) -> tuple[AttentionBackendEnum, str | None]:
available_backends = _TURBO_WAN_SPARSE_BACKENDS
if supported_attention_backends is not None:
available_backends = _TURBO_WAN_SPARSE_BACKENDS & supported_attention_backends
if not available_backends:
available_backends = _TURBO_WAN_SPARSE_BACKENDS
preferred_backend = (
AttentionBackendEnum.SAGE_SLA_ATTN
if attention_type == "sagesla"
else AttentionBackendEnum.SLA_ATTN
)
if preferred_backend not in available_backends:
preferred_backend = sorted(available_backends, key=lambda b: b.name)[0]
requested_backend = _attention_backend_from_name(requested_attention_backend)
if requested_backend in available_backends:
return requested_backend, None
if requested_attention_backend is None:
return preferred_backend, None
return (
preferred_backend,
"TurboWan only supports `sla_attn` or `sage_sla_attn`; "
f"got attention_backend={requested_attention_backend!r}. "
f"Using `{preferred_backend.name.lower()}` from "
f"attention_type={attention_type!r}.",
)
def async_a2a_communicate(
a2a_inputs: Union[torch.Tensor, List[torch.Tensor]],
cp_size: int,
cp_group: ProcessGroup,
cp_stream: torch.get_device_module().Stream,
local_seq_2_local_head: bool,
) -> Union[torch.Tensor, List[torch.Tensor]]:
"""
A2A communication for context parallelism. best used in communicate qkv
Modified from Nvidia Transformer Engine.
"""
a2a_inputs = [a2a_inputs] if not isinstance(a2a_inputs, list) else a2a_inputs
a2a_outputs, a2a_reqs = [None] * len(a2a_inputs), [None] * len(a2a_inputs)
a2a_post_fns = [None] * len(a2a_inputs)
if local_seq_2_local_head:
for i in range(len(a2a_inputs) + 2):
if 0 < i < len(a2a_inputs) + 1:
a2a_outputs[i - 1] = torch.empty_like(a2a_inputs[i - 1])
a2a_reqs[i - 1] = torch.distributed.all_to_all_single(
a2a_outputs[i - 1], a2a_inputs[i - 1], group=cp_group, async_op=True
)
a2a_post_fns[i - 1] = post_all2all(local_seq_2_local_head, cp_size)
if i > 1:
with torch.get_device_module().stream(cp_stream):
a2a_reqs[i - 2].wait()
a2a_outputs[i - 2] = a2a_post_fns[i - 2](a2a_outputs[i - 2])
if i < len(a2a_inputs):
a2a_inputs[i] = rearrange(
a2a_inputs[i], "bs seq_len (w h) d -> w bs seq_len h d", w=cp_size
).contiguous()
else:
for i in range(len(a2a_inputs) + 2):
if 0 < i < len(a2a_inputs) + 1:
a2a_outputs[i - 1] = torch.empty_like(a2a_inputs[i - 1])
a2a_reqs[i - 1] = torch.distributed.all_to_all_single(
a2a_outputs[i - 1], a2a_inputs[i - 1], group=cp_group, async_op=True
)
a2a_post_fns[i - 1] = post_all2all(local_seq_2_local_head, cp_size)
if i < len(a2a_inputs):
a2a_inputs[i] = rearrange(
a2a_inputs[i], "bs (w s) h d -> w bs s h d", w=cp_size
).contiguous()
if i > 1:
with torch.get_device_module().stream(cp_stream):
a2a_reqs[i - 2].wait()
a2a_outputs[i - 2] = a2a_post_fns[i - 2](a2a_outputs[i - 2])
torch.get_device_module().current_stream().wait_stream(cp_stream)
return a2a_outputs[0] if len(a2a_inputs) == 1 else a2a_outputs
class _SeqAllToAll(torch.autograd.Function):
@staticmethod
def forward(
ctx: Any, group: dist.ProcessGroup, input: Tensor, local_seq_2_local_head: bool
) -> Tensor:
ctx.group = group
res = single_all_to_all(input, local_seq_2_local_head, group, False)
ctx.local_seq_2_local_head = local_seq_2_local_head
return res
@staticmethod
def backward(ctx: Any, *grad_output: Tensor) -> Tuple[None, Tensor, None]:
return (
None,
_SeqAllToAll.apply(ctx.group, *grad_output, not ctx.local_seq_2_local_head),
None,
)
class _SeqAllToAllQKV(torch.autograd.Function):
@staticmethod
def forward(
ctx: Any,
group: dist.ProcessGroup,
q: Tensor,
k: Tensor,
v: Tensor,
cp_size: int,
cp_stream: torch.get_device_module().Stream,
local_seq_2_local_head: bool,
) -> Tuple[Tensor, Tensor, Tensor]:
ctx.group = group
ctx.cp_size = cp_size
ctx.cp_stream = cp_stream
ctx.local_seq_2_local_head = local_seq_2_local_head
q, k, v = async_a2a_communicate(
[q, k, v], cp_size, group, cp_stream, local_seq_2_local_head
)
return q, k, v
@staticmethod
def backward(
ctx: Any, *grad_output: Tensor
) -> Tuple[None, Tensor, Tensor, Tensor, None, None, None]:
q_grad, k_grad, v_grad = _SeqAllToAllQKV.apply(
ctx.group,
*grad_output,
ctx.cp_size,
ctx.cp_stream,
not ctx.local_seq_2_local_head,
)
return (None, q_grad, k_grad, v_grad, None, None, None)
class DistributedAttention(torch.nn.Module):
"""Initialization.
Arguments:
local_attention (Module): local attention with q,k,v
sequence_process_group (ProcessGroup): sequence parallel process group
"""
def __init__(self, local_attention: Union[Module, Callable]) -> None:
super(DistributedAttention, self).__init__()
self.local_attn = local_attention
self.pg = None
self.stream = None
def forward(
self, query: Tensor, key: Tensor, value: Tensor, ctx_attn_metadata
) -> Tensor:
"""forward
Arguments:
query (Tensor): query input to the layer
key (Tensor): key input to the layer
value (Tensor): value input to the layer
Returns:
* output (Tensor): context output
"""
if self.pg is None:
return self.local_attn(query, key, value, ctx_attn_metadata)
pg_size = dist.get_world_size(self.pg)
if pg_size < 2:
return self.local_attn(query, key, value, ctx_attn_metadata)
query_layer, key_layer, value_layer = _SeqAllToAllQKV.apply(
self.pg, query, key, value, pg_size, self.stream, True
)
context_layer = self.local_attn(
query_layer, key_layer, value_layer, ctx_attn_metadata
)
output = _SeqAllToAll.apply(self.pg, context_layer, False)
return output
def set_context_parallel_group(self, group, stream):
self.pg = group
self.stream = stream
class MinimalA2AAttnOp(DistributedAttention):
def __init__(
self,
num_heads: int,
head_size: int,
attention_type: str,
topk: float,
supported_attention_backends: set[AttentionBackendEnum] | None = None,
prefix: str = "",
):
dtype = get_compute_dtype()
try:
requested_attention_backend = get_global_server_args().attention_backend
except ValueError:
requested_attention_backend = None
selected_attention_backend, warning_message = _resolve_turbo_wan_sparse_backend(
attention_type,
requested_attention_backend,
supported_attention_backends,
)
if warning_message is not None:
logger.warning_once(warning_message)
attn_backend = get_attn_backend(
head_size,
dtype,
supported_attention_backends={selected_attention_backend},
selected_attention_backend=selected_attention_backend,
)
impl_cls: Type[AttentionImpl] = attn_backend.get_impl_cls()
local_attn = impl_cls(
num_heads=num_heads,
head_size=head_size,
topk_ratio=topk,
prefix=f"{prefix}.impl",
)
super(MinimalA2AAttnOp, self).__init__(local_attn)
def set_context_parallel_group(self, process_group, ranks, stream):
del ranks
super().set_context_parallel_group(process_group, stream)
def forward(
self, query: Tensor, key: Tensor, value: Tensor, *args: Any, **kwargs
) -> Tensor:
forward_context: ForwardContext = get_forward_context()
ctx_attn_metadata = forward_context.attn_metadata
results = super().forward(query, key, value, ctx_attn_metadata)
return rearrange(results, "b ... h l -> b ... (h l)")
@@ -0,0 +1,115 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/model_executor/custom_op.py
from collections.abc import Callable
from typing import Any
import torch.nn as nn
from sglang.kernel_api_logging import debug_kernel_api
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
_is_cuda = current_platform.is_cuda()
class CustomOp(nn.Module):
"""
Base class for custom ops.
Dispatches the forward method to the appropriate backend.
"""
def __init__(self) -> None:
super().__init__()
self._forward_method = self.dispatch_forward()
@debug_kernel_api
def forward(self, *args, **kwargs) -> Any:
return self._forward_method(*args, **kwargs)
def forward_native(self, *args, **kwargs) -> Any:
"""PyTorch-native implementation of the forward method.
This method is optional. If implemented, it can be used with compilers
such as torch.compile or PyTorch XLA. Also, it can be used for testing
purposes.
"""
raise NotImplementedError
def forward_cuda(self, *args, **kwargs) -> Any:
raise NotImplementedError
def forward_hip(self, *args, **kwargs) -> Any:
# ROCm kernels follow the CUDA path by default.
return self.forward_cuda(*args, **kwargs)
def forward_cpu(self, *args, **kwargs) -> Any:
# By default, we assume that CPU ops are compatible with CUDA ops.
return self.forward_cuda(*args, **kwargs)
def forward_tpu(self, *args, **kwargs) -> Any:
# By default, we assume that TPU ops are compatible with the
# PyTorch-native implementation.
return self.forward_native(*args, **kwargs)
def forward_musa(self, *args, **kwargs) -> Any:
# MUSA kernels follow the CUDA path by default.
return self.forward_cuda(*args, **kwargs)
def forward_oot(self, *args, **kwargs) -> Any:
# By default, we assume that OOT ops are compatible with the
# PyTorch-native implementation.
return self.forward_native(*args, **kwargs)
def forward_npu(self, *args, **kwargs) -> Any:
# By default, we assume that NPU ops are compatible with the
# PyTorch-native implementation.
return self.forward_native(*args, **kwargs)
def dispatch_forward(self) -> Callable:
if _is_cuda:
return self.forward_cuda
elif current_platform.is_hip():
return self.forward_hip
elif current_platform.is_npu():
return self.forward_npu
elif current_platform.is_xpu():
return self.forward_xpu
elif current_platform.is_musa():
return self.forward_musa
else:
return self.forward_native
@classmethod
def enabled(cls) -> bool:
# since we are not using Inductor, we always return True
return True
@staticmethod
def default_on() -> bool:
"""
On by default if level < CompilationLevel.PIECEWISE
Specifying 'all' or 'none' in custom_op takes precedence.
"""
raise NotImplementedError
# Dictionary of all custom ops (classes, indexed by registered name).
# To check if an op with a name is enabled, call .enabled() on the class.
# Examples:
# - MyOp.enabled()
# - op_registry["my_op"].enabled()
op_registry: dict[str, type["CustomOp"]] = {}
# Decorator to register custom ops.
@classmethod
def register(cls, name: str) -> Callable:
def decorator(op_cls):
assert name not in cls.op_registry, f"Duplicate op name: {name}"
op_cls.name = name
cls.op_registry[name] = op_cls
return op_cls
return decorator
@@ -0,0 +1,53 @@
import torch
from sglang.jit_kernel.diffusion.triton.scale_shift import fuse_scale_shift_kernel
from sglang.multimodal_gen.runtime.layers.custom_op import CustomOp
class MulAdd(CustomOp):
"""
Fuse elementwise mul and add
Input: a, b, c, OptionalInt[k]
Output: a * (k + b) + c
"""
def __init__(self, prefix: str = ""):
super().__init__()
def forward_native(
self, a: torch.Tensor, b: torch.Tensor, c: torch.Tensor, k: int = 0
) -> torch.Tensor:
# a.shape: [batch_size, seq_len, inner_dim]
if b.dim() == 4:
# b.shape: [batch_size, num_frames, 1, inner_dim]
num_frames = b.shape[1]
frame_seqlen = a.shape[1] // num_frames
return c + (
a.unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * (k + b)
).flatten(1, 2)
else:
# b.shape: [batch_size, 1, inner_dim]
return c + a * (k + b)
def forward_cuda(
self, a: torch.Tensor, b: torch.Tensor, c: torch.Tensor, k: int = 0
):
return fuse_scale_shift_kernel(a, b, c, scale_constant=k)
def forward_xpu(
self, a: torch.Tensor, b: torch.Tensor, c: torch.Tensor, k: int = 0
):
return self.forward_native(a, b, c, k=k)
@torch.compile
def forward_musa(
self, a: torch.Tensor, b: torch.Tensor, c: torch.Tensor, k: int = 0
):
return self.forward_native(a, b, c, k=k)
def forward_npu(
self, a: torch.Tensor, b: torch.Tensor, c: torch.Tensor, k: int = 0
):
from sgl_kernel_npu.norm.scale_shift import fused_scale_shift
return fused_scale_shift(a, b, c, scale_constant=k)
@@ -0,0 +1,159 @@
# SPDX-License-Identifier: Apache-2.0
from typing import Optional, Tuple
import torch
import torch.nn.functional as F
from sglang.multimodal_gen.runtime.layers.custom_op import CustomOp
from sglang.multimodal_gen.runtime.platforms import current_platform
_is_cuda = current_platform.is_cuda()
if _is_cuda:
from sglang.jit_kernel.diffusion.triton.scale_shift import (
fuse_layernorm_scale_shift_gate_select01_kernel,
fuse_residual_layernorm_scale_shift_gate_select01_kernel,
)
@CustomOp.register("fuse_layernorm_scale_shift_gate_select01")
class FusedLayerNormScaleShiftGateSelect01(CustomOp):
"""Fused layernorm + scale/shift + gate with binary index selection.
CUDA path uses a Triton kernel; other platforms fall back to PyTorch ops.
"""
def forward_cuda(
self,
x: torch.Tensor,
weight: Optional[torch.Tensor],
bias: Optional[torch.Tensor],
scale0: torch.Tensor,
shift0: torch.Tensor,
gate0: torch.Tensor,
scale1: torch.Tensor,
shift1: torch.Tensor,
gate1: torch.Tensor,
index: torch.Tensor,
eps: float,
) -> Tuple[torch.Tensor, torch.Tensor]:
if not x.is_contiguous():
x = x.contiguous()
if not index.is_contiguous():
index = index.contiguous()
return fuse_layernorm_scale_shift_gate_select01_kernel(
x,
weight=weight,
bias=bias,
scale0=scale0.contiguous(),
shift0=shift0.contiguous(),
gate0=gate0.contiguous(),
scale1=scale1.contiguous(),
shift1=shift1.contiguous(),
gate1=gate1.contiguous(),
index=index,
eps=eps,
)
def forward_hip(self, *args, **kwargs):
return self.forward_native(*args, **kwargs)
def forward_native(
self,
x: torch.Tensor,
weight: Optional[torch.Tensor],
bias: Optional[torch.Tensor],
scale0: torch.Tensor,
shift0: torch.Tensor,
gate0: torch.Tensor,
scale1: torch.Tensor,
shift1: torch.Tensor,
gate1: torch.Tensor,
index: torch.Tensor,
eps: float,
) -> Tuple[torch.Tensor, torch.Tensor]:
idx = index.to(dtype=torch.bool).unsqueeze(-1)
shift = torch.where(idx, shift1.unsqueeze(1), shift0.unsqueeze(1))
scale = torch.where(idx, scale1.unsqueeze(1), scale0.unsqueeze(1))
gate = torch.where(idx, gate1.unsqueeze(1), gate0.unsqueeze(1))
x = F.layer_norm(x, (x.shape[-1],), weight=weight, bias=bias, eps=eps)
x = x * (1 + scale) + shift
return x, gate
@CustomOp.register("fuse_residual_layernorm_scale_shift_gate_select01")
class FusedResidualLayerNormScaleShiftGateSelect01(CustomOp):
"""Fused residual + layernorm + scale/shift + gate with binary index selection.
CUDA path uses a Triton kernel; other platforms fall back to PyTorch ops.
"""
def forward_cuda(
self,
x: torch.Tensor,
residual: torch.Tensor,
residual_gate: torch.Tensor,
weight: Optional[torch.Tensor],
bias: Optional[torch.Tensor],
scale0: torch.Tensor,
shift0: torch.Tensor,
gate0: torch.Tensor,
scale1: torch.Tensor,
shift1: torch.Tensor,
gate1: torch.Tensor,
index: torch.Tensor,
eps: float,
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
if not x.is_contiguous():
x = x.contiguous()
if not index.is_contiguous():
index = index.contiguous()
if not residual.is_contiguous():
residual = residual.contiguous()
if not residual_gate.is_contiguous():
residual_gate = residual_gate.contiguous()
return fuse_residual_layernorm_scale_shift_gate_select01_kernel(
x,
residual=residual,
residual_gate=residual_gate,
weight=weight,
bias=bias,
scale0=scale0.contiguous(),
shift0=shift0.contiguous(),
gate0=gate0.contiguous(),
scale1=scale1.contiguous(),
shift1=shift1.contiguous(),
gate1=gate1.contiguous(),
index=index,
eps=eps,
)
def forward_hip(self, *args, **kwargs):
return self.forward_native(*args, **kwargs)
def forward_native(
self,
x: torch.Tensor,
residual: torch.Tensor,
residual_gate: torch.Tensor,
weight: Optional[torch.Tensor],
bias: Optional[torch.Tensor],
scale0: torch.Tensor,
shift0: torch.Tensor,
gate0: torch.Tensor,
scale1: torch.Tensor,
shift1: torch.Tensor,
gate1: torch.Tensor,
index: torch.Tensor,
eps: float,
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
idx = index.to(dtype=torch.bool).unsqueeze(-1)
shift = torch.where(idx, shift1.unsqueeze(1), shift0.unsqueeze(1))
scale = torch.where(idx, scale1.unsqueeze(1), scale0.unsqueeze(1))
gate = torch.where(idx, gate1.unsqueeze(1), gate0.unsqueeze(1))
residual_out = residual_gate * x + residual
x = F.layer_norm(
residual_out, (residual_out.shape[-1],), weight=weight, bias=bias, eps=eps
)
x = x * (1 + scale) + shift
return x, residual_out, gate
@@ -0,0 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
from sglang.multimodal_gen.runtime.layers.kvcache.causal_attention_cache import (
CausalAttentionKVView,
CausalSelfAttentionKVCache,
CrossAttentionKVCache,
)
__all__ = [
"CausalAttentionKVView",
"CausalSelfAttentionKVCache",
"CrossAttentionKVCache",
]
@@ -0,0 +1,413 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass
import torch
@dataclass(slots=True)
class CausalAttentionKVView:
k: torch.Tensor
v: torch.Tensor
local_start_index: int
local_end_index: int
visible_local_end: int
visible_global_end: int
@dataclass(slots=True)
class CausalSelfAttentionKVCache:
"""one transformer block's causal self-attn K/V cache and write cursors"""
k: torch.Tensor
v: torch.Tensor
# the right bound of the valid global token range
# e.g., 12000 means [0, 12000) has been generated and cached
global_end_index: torch.Tensor
# the right bound of the valid local token range within the buffer (when cache is unfilled)
local_end_index: torch.Tensor
global_end_index_int: int | None = None
local_end_index_int: int | None = None
cache_size: int = 0
sink_tokens: int = 0
attention_window_size: int = 0
allow_growth: bool = False
def __post_init__(self) -> None:
if self.cache_size == 0:
self.cache_size = self.k.shape[1]
if self.attention_window_size == 0:
self.attention_window_size = self.cache_size
def reset_indices(self) -> None:
self.global_end_index.zero_()
self.local_end_index.zero_()
if self.global_end_index_int is not None:
self.global_end_index_int = 0
if self.local_end_index_int is not None:
self.local_end_index_int = 0
def _read_indices(self) -> tuple[int, int]:
global_end_index = self.global_end_index_int
local_end_index = self.local_end_index_int
if global_end_index is None or local_end_index is None:
global_end_index = int(self.global_end_index.item())
local_end_index = int(self.local_end_index.item())
self.global_end_index_int = global_end_index
self.local_end_index_int = local_end_index
return global_end_index, local_end_index
def _write_indices(self, *, global_end_index: int, local_end_index: int) -> None:
if (
self.global_end_index_int == global_end_index
and self.local_end_index_int == local_end_index
):
return
if self.global_end_index_int is not None:
self.global_end_index_int = global_end_index
if self.local_end_index_int is not None:
self.local_end_index_int = local_end_index
self.global_end_index.fill_(global_end_index)
self.local_end_index.fill_(local_end_index)
def _grow_to_fit(self, required_tokens: int) -> None:
if required_tokens <= self.cache_size:
return
old_cache_size = self.cache_size
new_cache_size = max(required_tokens, old_cache_size * 2)
new_k = self.k.new_zeros(
self.k.shape[0],
new_cache_size,
self.k.shape[2],
self.k.shape[3],
)
new_v = self.v.new_zeros(
self.v.shape[0],
new_cache_size,
self.v.shape[2],
self.v.shape[3],
)
new_k[:, :old_cache_size] = self.k
new_v[:, :old_cache_size] = self.v
self.k = new_k
self.v = new_v
self.cache_size = new_cache_size
if self.attention_window_size == old_cache_size:
self.attention_window_size = new_cache_size
def can_direct_current_attention(self, num_new_tokens: int) -> bool:
return (
self.sink_tokens == 0
and self.cache_size == num_new_tokens
and self.attention_window_size == num_new_tokens
)
def update_and_get_attention_kv(
self,
*,
key: torch.Tensor,
value: torch.Tensor,
current_chunk_start: int,
cache_head_start: int | None = None,
recent_window_tokens: int | None = None,
debug_name: str = "causal KV cache",
) -> CausalAttentionKVView:
"""write fresh kv into the cache, returns the part of view visible to the current chunk
Args:
current_chunk_start: the global position of the start of the chunk
cache_head_start: first cache head for key/value when they only
carry a local slice of the cache heads; other heads are left untouched
recent_window_tokens: recent-window attention size. ``None``
returns the full visible attention window. ``0`` keeps only sink
tokens plus the current chunk. A positive value keeps sink tokens,
up to that many tokens before the current chunk, and the current
chunk. Negative values are invalid.
"""
num_new_tokens = key.shape[1]
num_input_heads = key.shape[2]
num_cache_heads = self.k.shape[2]
cache_head_slice = None
if num_cache_heads != num_input_heads:
if cache_head_start is None:
raise ValueError(
f"{debug_name} requires cache_head_start when cache heads "
f"({num_cache_heads}) differ from input heads ({num_input_heads})."
)
cache_head_slice = slice(
cache_head_start, cache_head_start + num_input_heads
)
current_chunk_end = current_chunk_start + num_new_tokens
kv_cache_size = self.cache_size
sink_tokens = self.sink_tokens
global_end_index, local_end_index_prev = self._read_indices()
# local_start(/end)_index: the local position of the start/end of current chunk
# updated_local_end: the updated local end
# updated_global_end: the updated global end
# the global position of the start of the buffer
window_start = global_end_index - local_end_index_prev
if current_chunk_end <= global_end_index:
# the window stays as previous
# cache layout:
# [sink tokens, recent window tokens, current chunk tokens, uninitialized tokens (optional)]
local_start_index = current_chunk_start - window_start
local_end_index = local_start_index + num_new_tokens
# the local end and global end remains unchanged (since the chunk hasn't proceed)
updated_local_end = local_end_index_prev
updated_global_end = global_end_index
else:
# the chunk window has proceed, append new tokens, and evict earliest (if have to)
appended_tokens = current_chunk_end - global_end_index
if self.allow_growth:
self._grow_to_fit(local_end_index_prev + appended_tokens)
kv_cache_size = self.cache_size
if local_end_index_prev + appended_tokens > kv_cache_size:
# the new tokens can't fit in the remaining space (after local_end_index_prev), start evicting:
# before:
# [sink tokens, evicted tokens, rolled tokens, remaining space]
# ^ end of previous chunk
# after:
# [sink tokens, rolled tokens, remaining space ]
# 1. keep sink tokens ([0: sink_tokens]) untouched
# 2. evict obsolete tokens in: [sink_tokens:sink_tokens + num_evicted_tokens]
num_evicted_tokens = (
local_end_index_prev + appended_tokens - kv_cache_size
)
# number of tokens to move
num_rolled_tokens = max(
0,
local_end_index_prev - num_evicted_tokens - sink_tokens,
)
if num_rolled_tokens > 0:
if cache_head_slice is None:
self.k[:, sink_tokens : sink_tokens + num_rolled_tokens] = (
self.k[
:,
sink_tokens
+ num_evicted_tokens : sink_tokens
+ num_evicted_tokens
+ num_rolled_tokens,
].clone()
)
self.v[:, sink_tokens : sink_tokens + num_rolled_tokens] = (
self.v[
:,
sink_tokens
+ num_evicted_tokens : sink_tokens
+ num_evicted_tokens
+ num_rolled_tokens,
].clone()
)
else:
self.k[
:,
sink_tokens : sink_tokens + num_rolled_tokens,
cache_head_slice,
:,
] = self.k[
:,
sink_tokens
+ num_evicted_tokens : sink_tokens
+ num_evicted_tokens
+ num_rolled_tokens,
cache_head_slice,
:,
].clone()
self.v[
:,
sink_tokens : sink_tokens + num_rolled_tokens,
cache_head_slice,
:,
] = self.v[
:,
sink_tokens
+ num_evicted_tokens : sink_tokens
+ num_evicted_tokens
+ num_rolled_tokens,
cache_head_slice,
:,
].clone()
# if we move the minimum number of tokens, the right bound of the append token would be aligned with end of the buffer
local_end_index = kv_cache_size
else:
# enough space, directly append new tokens after end of previous chunk
local_end_index = local_end_index_prev + appended_tokens
local_start_index = local_end_index - num_new_tokens
updated_local_end = local_end_index
# after filling in the proceeded new chunk, the global end aligns with the global end of the current chunk
updated_global_end = current_chunk_end
if (
local_start_index < 0
or local_end_index > kv_cache_size
or local_end_index - local_start_index != num_new_tokens
):
raise RuntimeError(
f"Invalid {debug_name} write range: "
f"local=[{local_start_index}, {local_end_index}), "
f"global_end={global_end_index}, "
f"prev_local_end={local_end_index_prev}, "
f"kv_cache_size={kv_cache_size}, "
f"num_new_tokens={num_new_tokens}, "
f"current_start={current_chunk_start}, current_end={current_chunk_end}"
)
if self.k.requires_grad:
self.k = self.k.detach()
if self.v.requires_grad:
self.v = self.v.detach()
attn_start_index = max(0, updated_local_end - self.attention_window_size)
# write fresh kv and return visible view
if cache_head_slice is None:
self.k[:, local_start_index:local_end_index] = key
self.v[:, local_start_index:local_end_index] = value
visible_k, visible_v = self._visible_attention_kv(
local_start_index=local_start_index,
updated_local_end=updated_local_end,
attn_start_index=attn_start_index,
recent_window_tokens=recent_window_tokens,
)
else:
self.k[:, local_start_index:local_end_index, cache_head_slice, :] = key
self.v[:, local_start_index:local_end_index, cache_head_slice, :] = value
visible_k, visible_v = self._visible_attention_kv(
local_start_index=local_start_index,
updated_local_end=updated_local_end,
attn_start_index=attn_start_index,
recent_window_tokens=recent_window_tokens,
cache_head_slice=cache_head_slice,
)
self._write_indices(
global_end_index=updated_global_end,
local_end_index=updated_local_end,
)
return CausalAttentionKVView(
k=visible_k,
v=visible_v,
local_start_index=local_start_index,
local_end_index=local_end_index,
visible_local_end=updated_local_end,
visible_global_end=updated_global_end,
)
def _visible_attention_kv(
self,
*,
local_start_index: int,
updated_local_end: int,
attn_start_index: int,
recent_window_tokens: int | None,
cache_head_slice: slice | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Return the visible KV slice for the current attention call.
When ``recent_window_tokens`` is ``None``, the returned token range is
the standard sliding window::
[attn_start_index, updated_local_end)
When recent-window selection is enabled, ``recent_window_tokens`` must be
non-negative and the returned token ranges are::
sink_end = min(self.sink_tokens, updated_local_end)
recent_start = max(sink_end, local_start_index - recent_window_tokens)
[0, sink_end) + [recent_start, updated_local_end)
Thus ``0`` keeps only sink tokens plus the current chunk.
``cache_head_slice`` applies the same token ranges to a subset of KV
heads.
"""
if recent_window_tokens is None:
if cache_head_slice is None:
return (
self.k[:, attn_start_index:updated_local_end],
self.v[:, attn_start_index:updated_local_end],
)
return (
self.k[:, attn_start_index:updated_local_end, cache_head_slice, :],
self.v[:, attn_start_index:updated_local_end, cache_head_slice, :],
)
if recent_window_tokens < 0:
raise ValueError("recent_window_tokens must be non-negative or None")
sink_end = min(self.sink_tokens, updated_local_end)
recent_start = max(sink_end, local_start_index - recent_window_tokens)
if recent_start <= sink_end:
if cache_head_slice is None:
return self.k[:, :updated_local_end], self.v[:, :updated_local_end]
return (
self.k[:, :updated_local_end, cache_head_slice, :],
self.v[:, :updated_local_end, cache_head_slice, :],
)
if sink_end <= 0:
if cache_head_slice is None:
return (
self.k[:, recent_start:updated_local_end],
self.v[:, recent_start:updated_local_end],
)
return (
self.k[:, recent_start:updated_local_end, cache_head_slice, :],
self.v[:, recent_start:updated_local_end, cache_head_slice, :],
)
if cache_head_slice is None:
return (
torch.cat(
[
self.k[:, :sink_end],
self.k[:, recent_start:updated_local_end],
],
dim=1,
),
torch.cat(
[
self.v[:, :sink_end],
self.v[:, recent_start:updated_local_end],
],
dim=1,
),
)
return (
torch.cat(
[
self.k[:, :sink_end, cache_head_slice, :],
self.k[:, recent_start:updated_local_end, cache_head_slice, :],
],
dim=1,
),
torch.cat(
[
self.v[:, :sink_end, cache_head_slice, :],
self.v[:, recent_start:updated_local_end, cache_head_slice, :],
],
dim=1,
),
)
@dataclass(slots=True)
class CrossAttentionKVCache:
"""one transformer block's cross-attn condition K/V cache"""
k: torch.Tensor
v: torch.Tensor
is_init: bool = False
def store(self, k: torch.Tensor, v: torch.Tensor) -> None:
self.k = k.detach()
self.v = v.detach()
self.is_init = True
def reset(self) -> None:
self.is_init = False
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,730 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Code adapted from SGLang https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/lora/layers.py
import os
import torch
from torch import nn
from torch.distributed._composable.fsdp import (
CPUOffloadPolicy,
OffloadPolicy,
fully_shard,
)
from torch.distributed.tensor import DTensor
from sglang.multimodal_gen.runtime.distributed import (
get_local_torch_device,
get_tp_rank,
split_tensor_along_last_dim,
tensor_model_parallel_all_gather,
tensor_model_parallel_all_reduce,
)
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
LinearBase,
MergedColumnParallelLinear,
QKVParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.vocab_parallel_embedding import (
VocabParallelEmbedding,
)
from sglang.multimodal_gen.utils import get_mixed_precision_state
torch._dynamo.config.recompile_limit = 64
LORA_MERGE_CHUNK_BYTES = 32 * 1024 * 1024
LoRAWeightEntry = tuple[
torch.nn.Parameter,
torch.nn.Parameter,
str | None,
float,
int | None,
int | None,
]
class BaseLayerWithLoRA(nn.Module):
def __init__(
self,
base_layer: nn.Module,
lora_rank: int | None = None,
lora_alpha: int | None = None,
):
super().__init__()
self.base_layer: nn.Module = base_layer
self.merged: bool = False
# Immutable base-weight snapshot; `to("cpu")` may alias CPU storage.
# Use `clone()` so merge updates cannot mutate this backup tensor.
self.cpu_weight = base_layer.weight.detach().to("cpu").clone()
# indicates adapter weights don't contain this layer
# (which shouldn't normally happen, but we want to separate it from the case of erroneous merging)
# Default to True to prevent using uninitialized weights; set to False when weights are loaded
self.disable_lora: bool = True
self.lora_rank = lora_rank
self.lora_alpha = lora_alpha
self.lora_weights_list: list[LoRAWeightEntry] = []
self.lora_path: str | None = None
self.strength: float = 1.0
self.lora_A = None
self.lora_B = None
@property
def weight(self):
return self.base_layer.weight
@property
def bias(self):
return getattr(self.base_layer, "bias", None)
@torch.compile()
def forward(self, x: torch.Tensor) -> torch.Tensor:
lora_A = self.lora_A
lora_B = self.lora_B
if isinstance(self.lora_B, DTensor):
lora_B = self.lora_B.to_local()
lora_A = self.lora_A.to_local()
# TODO: Support multiple LoRA adapters when use not merged mode
if not self.merged and not self.disable_lora:
lora_dtype = lora_A.dtype
x_lora = x.to(dtype=lora_dtype)
lora_A_sliced = self.slice_lora_a_weights(
lora_A.to(device=x.device, non_blocking=True)
)
lora_B_sliced = self.slice_lora_b_weights(
lora_B.to(device=x.device, non_blocking=True)
)
delta = x_lora @ lora_A_sliced.T @ lora_B_sliced.T
if self.lora_alpha != self.lora_rank:
delta = delta * (
self.lora_alpha / self.lora_rank # type: ignore
) # type: ignore
delta = delta * self.strength
out, output_bias = self.base_layer(x)
return out + delta.to(dtype=out.dtype), output_bias
else:
out, output_bias = self.base_layer(x)
return out, output_bias
def slice_lora_a_weights(self, A: torch.Tensor) -> torch.Tensor:
return A
def slice_lora_b_weights(self, B: torch.Tensor) -> torch.Tensor:
return B
@staticmethod
def _as_mutable_tensor(tensor: torch.Tensor) -> torch.Tensor:
# lora can be reconfigured after executor forwards create inference tensors
if tensor.is_inference():
with torch.inference_mode(False):
return tensor.detach().clone()
return tensor
def set_lora_weights(
self,
A: torch.Tensor,
B: torch.Tensor,
lora_path: str | None = None,
strength: float = 1.0,
clear_existing: bool = False,
merge_weights: bool = True,
) -> None:
"""
Set LoRA weights. Supports multiple LoRA adapters.
Args:
A: LoRA A weight tensor
B: LoRA B weight tensor
lora_path: Path to the LoRA adapter (for logging)
strength: LoRA strength
clear_existing: If True, clear existing LoRA weights before adding new one.
If False, append to existing list (for multi-LoRA support).
"""
lora_A_param = torch.nn.Parameter(
A
) # share storage with weights in the pipeline
lora_B_param = torch.nn.Parameter(B)
if clear_existing:
self.lora_weights_list.clear()
# Also clear backward compatibility attributes
self.lora_A = None
self.lora_B = None
self.lora_path = None
self.strength = 1.0
# Add to list for multi-LoRA support
self.lora_weights_list.append(
(
lora_A_param,
lora_B_param,
lora_path,
strength,
self.lora_rank,
self.lora_alpha,
)
)
# Set backward compatibility attributes to point to the last LoRA (for single LoRA case)
# This ensures backward compatibility while supporting multiple LoRA
self.lora_A = lora_A_param
self.lora_B = lora_B_param
self.lora_path = lora_path
self.strength = strength
self.disable_lora = False
if merge_weights:
self.merge_lora_weights()
elif self.merged:
self.unmerge_lora_weights()
@torch.no_grad()
def _merge_lora_into_data(
self,
data: torch.Tensor,
lora_list: list[LoRAWeightEntry],
) -> None:
"""
Merge all LoRA adapters into the data tensor in-place.
Args:
data: The base weight tensor to merge LoRA into (modified in-place)
lora_list: List of (lora_A, lora_B, lora_path, lora_strength, rank, alpha) tuples
"""
# Merge all LoRA adapters in order
for lora_A, lora_B, _, lora_strength, lora_rank, lora_alpha in lora_list:
lora_A_sliced = self.slice_lora_a_weights(lora_A.to(data))
lora_B_sliced = self.slice_lora_b_weights(lora_B.to(data))
scale = lora_strength
if (
lora_alpha is not None
and lora_rank is not None
and lora_alpha != lora_rank
):
scale *= lora_alpha / lora_rank
if not isinstance(lora_B_sliced, torch.Tensor):
lora_delta = lora_B_sliced @ lora_A_sliced
if isinstance(lora_delta, torch.Tensor) and lora_delta.dim() > 2:
lora_delta = lora_delta.reshape(-1, lora_delta.shape[-1])
data.add_(lora_delta, alpha=scale)
continue
if lora_A_sliced.dim() > 2 or lora_B_sliced.dim() > 2:
lora_delta = lora_B_sliced @ lora_A_sliced
if lora_delta.dim() > 2:
lora_delta = lora_delta.reshape(-1, lora_delta.shape[-1])
data_2d = data.reshape(-1, data.shape[-1]) if data.dim() > 2 else data
data_2d.add_(lora_delta, alpha=scale)
continue
data_2d = data.reshape(-1, data.shape[-1]) if data.dim() > 2 else data
lora_B_2d = (
lora_B_sliced.reshape(-1, lora_B_sliced.shape[-1])
if lora_B_sliced.dim() > 2
else lora_B_sliced
)
chunk_rows = max(
1,
LORA_MERGE_CHUNK_BYTES
// (data_2d.shape[-1] * max(1, data_2d.element_size())),
)
for start in range(0, lora_B_2d.shape[0], chunk_rows):
end = min(start + chunk_rows, lora_B_2d.shape[0])
chunk_delta = lora_B_2d[start:end] @ lora_A_sliced
data_2d[start:end].add_(chunk_delta, alpha=scale)
def _should_merge_in_fp32(
self,
lora_list: list[LoRAWeightEntry],
) -> bool:
if os.getenv("SGLANG_DIFFUSION_LORA_MERGE_FP32", "1") != "1":
return False
for _, _, lora_path, _, _, _ in lora_list:
if lora_path and "distilled-lora" in lora_path.lower():
return False
return True
@torch.no_grad()
def merge_lora_weights(self, strength: float | None = None) -> None:
if strength is not None:
self.strength = strength
if self.lora_weights_list:
self.lora_weights_list = [
(lora_A, lora_B, lora_path, strength, lora_rank, lora_alpha)
for (
lora_A,
lora_B,
lora_path,
_,
lora_rank,
lora_alpha,
) in self.lora_weights_list
]
if self.disable_lora:
return
if self.merged:
self.unmerge_lora_weights()
# Use lora_weights_list if available, otherwise fall back to single LoRA for backward compatibility
lora_list = self.lora_weights_list if self.lora_weights_list else []
if not lora_list and self.lora_A is not None and self.lora_B is not None:
lora_list = [
(
self.lora_A,
self.lora_B,
self.lora_path,
self.strength,
self.lora_rank,
self.lora_alpha,
)
]
if not lora_list:
raise ValueError("LoRA weights not set. Please set them first.")
merge_in_fp32 = self._should_merge_in_fp32(lora_list)
if isinstance(self.base_layer.weight, DTensor):
mesh = self.base_layer.weight.data.device_mesh
unsharded_base_layer = ReplicatedLinear(
input_size=self.base_layer.input_size,
output_size=self.base_layer.output_size,
bias=getattr(self.base_layer, "bias", None) is not None,
skip_bias_add=self.base_layer.skip_bias_add,
params_dtype=self.base_layer.params_dtype,
quant_config=self.base_layer.quant_config,
prefix=self.base_layer.prefix,
)
# Using offload param is on CPU, so current_device is for "CPU -> GPU -> merge -> CPU"
current_device = self.base_layer.weight.data.device
data = self.base_layer.weight.data.to(
get_local_torch_device()
).full_tensor()
data = self._as_mutable_tensor(data)
target_dtype = data.dtype
if (
merge_in_fp32
and data.is_floating_point()
and data.dtype != torch.float32
):
data = data.to(torch.float32)
self._merge_lora_into_data(data, lora_list)
unsharded_base_layer.weight = nn.Parameter(
self._as_mutable_tensor(data.to(current_device, dtype=target_dtype))
)
if isinstance(getattr(self.base_layer, "bias", None), DTensor):
bias_data = (
self.base_layer.bias.to(get_local_torch_device(), non_blocking=True)
.full_tensor()
.to(current_device)
)
unsharded_base_layer.bias = nn.Parameter(
self._as_mutable_tensor(bias_data)
)
offload_policy = (
CPUOffloadPolicy() if "cpu" in str(current_device) else OffloadPolicy()
)
mp_policy = get_mixed_precision_state().mp_policy
self.base_layer = fully_shard(
unsharded_base_layer,
mesh=mesh,
mp_policy=mp_policy,
offload_policy=offload_policy,
)
else:
current_device = self.base_layer.weight.data.device
data = self.base_layer.weight.data.to(get_local_torch_device())
data = self._as_mutable_tensor(data)
target_dtype = data.dtype
if (
merge_in_fp32
and data.is_floating_point()
and data.dtype != torch.float32
):
data = data.to(torch.float32)
self._merge_lora_into_data(data, lora_list)
self.base_layer.weight.data = self._as_mutable_tensor(
data.to(current_device, dtype=target_dtype, non_blocking=True)
)
self.merged = True
@torch.no_grad()
# @torch.compile(dynamic=True)
def unmerge_lora_weights(self) -> None:
if self.disable_lora:
return
if not self.merged:
raise ValueError(
"LoRA weights not merged. Please merge them first before unmerging."
)
# avoid precision loss
if isinstance(self.base_layer.weight, DTensor):
device = self.base_layer.weight.data.device
old_weight = self.base_layer.weight
new_weight_data = self._as_mutable_tensor(
self.cpu_weight.to(device, non_blocking=True)
)
self.base_layer.weight = nn.Parameter(new_weight_data)
del old_weight
else:
current_device = self.base_layer.weight.data.device
cpu_weight_on_device = self.cpu_weight.to(current_device, non_blocking=True)
if self.base_layer.weight.data.is_inference():
self.base_layer.weight.data = self._as_mutable_tensor(
cpu_weight_on_device
)
else:
self.base_layer.weight.data.copy_(cpu_weight_on_device)
if (
cpu_weight_on_device.data_ptr()
!= self.base_layer.weight.data.data_ptr()
):
del cpu_weight_on_device
self.merged = False
@torch.no_grad()
def commit_merged_as_base(self) -> None:
"""Promote the currently merged weights to the permanent base.
Re-snapshots ``cpu_weight`` so the merged weights become the restore
target and resets adapter bookkeeping (``merged=False``). A later dynamic
``set_lora_weights`` then adds its delta on top of the merged base instead
of unmerging it.
"""
if not self.merged:
return
weight = self.base_layer.weight
if isinstance(weight, DTensor):
weight = weight.to_local()
# clone(): to("cpu") may alias storage; we must not mutate this backup.
self.cpu_weight = weight.detach().to("cpu").clone()
self.merged = False
self.disable_lora = True
self.lora_weights_list = []
self.lora_A = None
self.lora_B = None
self.lora_path = None
self.strength = 1.0
class VocabParallelEmbeddingWithLoRA(BaseLayerWithLoRA):
"""
Vocab parallel embedding layer with support for LoRA (Low-Rank Adaptation).
Note: The current version does not yet implement the LoRA functionality.
This class behaves exactly the same as the base VocabParallelEmbedding.
Future versions will integrate LoRA functionality to support efficient parameter fine-tuning.
"""
def __init__(
self,
base_layer: VocabParallelEmbedding,
) -> None:
super().__init__(base_layer)
def forward(self, input_: torch.Tensor) -> torch.Tensor:
raise NotImplementedError(
"We don't support VocabParallelEmbeddingWithLoRA yet."
)
class ColumnParallelLinearWithLoRA(BaseLayerWithLoRA):
def __init__(
self,
base_layer: ColumnParallelLinear,
lora_rank: int | None = None,
lora_alpha: int | None = None,
) -> None:
super().__init__(base_layer, lora_rank, lora_alpha)
def forward(self, input_: torch.Tensor) -> torch.Tensor:
if self.merged or self.disable_lora:
return self.base_layer(input_)
lora_A = self.lora_A
lora_B = self.lora_B
if isinstance(self.lora_B, DTensor):
lora_B = self.lora_B.to_local()
lora_A = self.lora_A.to_local()
bias = self.base_layer.bias if not self.base_layer.skip_bias_add else None
output_parallel = self.base_layer.quant_method.apply(
self.base_layer, input_, bias
)
if not self.merged and not self.disable_lora:
lora_dtype = lora_A.dtype
input_lora = input_.to(dtype=lora_dtype)
lora_A_sliced = self.slice_lora_a_weights(
lora_A.to(device=input_.device, non_blocking=True)
)
lora_B_sliced = self.slice_lora_b_weights(
lora_B.to(device=input_.device, non_blocking=True)
)
delta_parallel = input_lora @ lora_A_sliced.T @ lora_B_sliced.T
if self.lora_alpha != self.lora_rank:
delta_parallel = delta_parallel * (
self.lora_alpha / self.lora_rank # type: ignore
) # type: ignore
delta_parallel = delta_parallel * self.strength
output_parallel = output_parallel + delta_parallel.to(
dtype=output_parallel.dtype
)
if self.base_layer.gather_output:
output = tensor_model_parallel_all_gather(output_parallel)
else:
output = output_parallel
output_bias = self.base_layer.bias if self.base_layer.skip_bias_add else None
return output, output_bias
def slice_lora_a_weights(self, A: torch.Tensor) -> torch.Tensor:
return A
def slice_lora_b_weights(self, B: torch.Tensor) -> torch.Tensor:
tp_rank = get_tp_rank()
shard_size = self.base_layer.output_partition_sizes[0]
start_idx = tp_rank * shard_size
end_idx = (tp_rank + 1) * shard_size
B = B[start_idx:end_idx, :]
return B
class MergedColumnParallelLinearWithLoRA(ColumnParallelLinearWithLoRA):
def __init__(
self,
base_layer: MergedColumnParallelLinear,
lora_rank: int | None = None,
lora_alpha: int | None = None,
) -> None:
super().__init__(base_layer, lora_rank, lora_alpha)
def slice_lora_a_weights(self, A: torch.Tensor) -> torch.Tensor:
return A
def slice_lora_b_weights(self, B: torch.Tensor) -> torch.Tensor:
tp_rank = get_tp_rank()
# Since the outputs for both gate and up are identical, we use a random one.
shard_size = self.base_layer.output_partition_sizes[0]
start_idx = tp_rank * shard_size
end_idx = (tp_rank + 1) * shard_size
return B[:, start_idx:end_idx, :]
class QKVParallelLinearWithLoRA(ColumnParallelLinearWithLoRA):
def __init__(
self,
base_layer: QKVParallelLinear,
lora_rank: int | None = None,
lora_alpha: int | None = None,
) -> None:
super().__init__(base_layer, lora_rank, lora_alpha)
def slice_lora_a_weights(self, A: torch.Tensor) -> torch.Tensor:
return A
def slice_lora_b_weights(
self, B: list[torch.Tensor]
) -> tuple[torch.Tensor, torch.Tensor]:
tp_rank = get_tp_rank()
B_q, B_kv = B
base_layer = self.base_layer
q_proj_shard_size = base_layer.q_proj_shard_size
kv_proj_shard_size = base_layer.kv_proj_shard_size
num_kv_head_replicas = base_layer.num_kv_head_replicas
q_start_idx = q_proj_shard_size * tp_rank
q_end_idx = q_start_idx + q_proj_shard_size
kv_shard_id = tp_rank // num_kv_head_replicas
kv_start_idx = kv_proj_shard_size * kv_shard_id
kv_end_idx = kv_start_idx + kv_proj_shard_size
return B_q[q_start_idx:q_end_idx, :], B_kv[:, kv_start_idx:kv_end_idx, :]
class RowParallelLinearWithLoRA(BaseLayerWithLoRA):
def __init__(
self,
base_layer: RowParallelLinear,
lora_rank: int | None = None,
lora_alpha: int | None = None,
) -> None:
super().__init__(base_layer, lora_rank, lora_alpha)
def forward(self, input_: torch.Tensor):
if self.merged or self.disable_lora:
return self.base_layer(input_)
lora_A = self.lora_A
lora_B = self.lora_B
if isinstance(self.lora_B, DTensor):
lora_B = self.lora_B.to_local()
lora_A = self.lora_A.to_local()
if self.base_layer.input_is_parallel:
input_parallel = input_
else:
tp_rank = get_tp_rank()
splitted_input = split_tensor_along_last_dim(
input_, num_partitions=self.base_layer.tp_size
)
input_parallel = splitted_input[tp_rank].contiguous()
output_parallel = self.base_layer.quant_method.apply(
self.base_layer, input_parallel
)
if not self.merged and not self.disable_lora:
lora_dtype = lora_A.dtype
input_parallel_lora = input_parallel.to(dtype=lora_dtype)
lora_A_sliced = self.slice_lora_a_weights(
lora_A.to(device=input_parallel.device, non_blocking=True)
)
lora_B_sliced = self.slice_lora_b_weights(
lora_B.to(device=input_parallel.device, non_blocking=True)
)
delta_parallel = input_parallel_lora @ lora_A_sliced.T @ lora_B_sliced.T
if self.lora_alpha != self.lora_rank:
delta_parallel = delta_parallel * (
self.lora_alpha / self.lora_rank # type: ignore
) # type: ignore
delta_parallel = delta_parallel * self.strength
output_parallel = output_parallel + delta_parallel.to(
dtype=output_parallel.dtype
)
if self.base_layer.reduce_results and self.base_layer.tp_size > 1:
output_ = tensor_model_parallel_all_reduce(output_parallel)
else:
output_ = output_parallel
if not self.base_layer.skip_bias_add:
output = (
output_ + self.base_layer.bias
if self.base_layer.bias is not None
else output_
)
output_bias = None
else:
output = output_
output_bias = self.base_layer.bias
return output, output_bias
def slice_lora_a_weights(self, A: torch.Tensor) -> torch.Tensor:
tp_rank = get_tp_rank()
shard_size = self.base_layer.input_size_per_partition
start_idx = tp_rank * shard_size
end_idx = (tp_rank + 1) * shard_size
A = A[:, start_idx:end_idx].contiguous()
return A
def slice_lora_b_weights(self, B: torch.Tensor) -> torch.Tensor:
return B
class LinearWithLoRA(BaseLayerWithLoRA):
"""
Wrapper for standard torch.nn.Linear to support LoRA.
Unlike custom LinearBase classes, nn.Linear.forward() returns a single tensor,
not a tuple of (output, bias).
"""
def __init__(
self,
base_layer: nn.Linear,
lora_rank: int | None = None,
lora_alpha: int | None = None,
) -> None:
super().__init__(base_layer, lora_rank, lora_alpha)
@torch.compile()
def forward(self, x: torch.Tensor) -> torch.Tensor:
lora_A = self.lora_A
lora_B = self.lora_B
if isinstance(self.lora_B, DTensor):
lora_B = self.lora_B.to_local()
lora_A = self.lora_A.to_local()
# TODO: Support multiple LoRA adapters when use not merged mode
if not self.merged and not self.disable_lora:
lora_dtype = lora_A.dtype
x_lora = x.to(dtype=lora_dtype)
lora_A_sliced = self.slice_lora_a_weights(
lora_A.to(device=x.device, non_blocking=True)
)
lora_B_sliced = self.slice_lora_b_weights(
lora_B.to(device=x.device, non_blocking=True)
)
delta = x_lora @ lora_A_sliced.T @ lora_B_sliced.T
if self.lora_alpha != self.lora_rank:
delta = delta * (
self.lora_alpha / self.lora_rank # type: ignore
) # type: ignore
delta = delta * self.strength
# nn.Linear.forward() returns a single tensor, not a tuple
out = self.base_layer(x)
return out + delta.to(dtype=out.dtype)
else:
# nn.Linear.forward() returns a single tensor
out = self.base_layer(x)
return out
def wrap_with_lora_layer(
layer: nn.Module,
lora_rank: int | None = None,
lora_alpha: int | None = None,
) -> BaseLayerWithLoRA | None:
"""
transform the given layer to its corresponding LoRA layer
"""
supported_layer_types: dict[
type[LinearBase] | type[nn.Linear], type[BaseLayerWithLoRA]
] = {
# the order matters
# VocabParallelEmbedding: VocabParallelEmbeddingWithLoRA,
QKVParallelLinear: QKVParallelLinearWithLoRA,
MergedColumnParallelLinear: MergedColumnParallelLinearWithLoRA,
ColumnParallelLinear: ColumnParallelLinearWithLoRA,
RowParallelLinear: RowParallelLinearWithLoRA,
ReplicatedLinear: BaseLayerWithLoRA,
nn.Linear: LinearWithLoRA,
}
for src_layer_type, lora_layer_type in supported_layer_types.items():
if isinstance(layer, src_layer_type): # type: ignore[arg-type]
ret = lora_layer_type(
layer,
lora_rank=lora_rank,
lora_alpha=lora_alpha,
)
return ret
return None
# source: https://github.com/vllm-project/vllm/blob/93b38bea5dd03e1b140ca997dfaadef86f8f1855/vllm/lora/utils.py#L9
def replace_submodule(
model: nn.Module, module_name: str, new_module: nn.Module
) -> nn.Module:
"""Replace a submodule in a model with a new module."""
parent = model.get_submodule(".".join(module_name.split(".")[:-1]))
target_name = module_name.split(".")[-1]
setattr(parent, target_name, new_module)
return new_module
@@ -0,0 +1,121 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from typing import Optional
import torch
import torch.nn as nn
from diffusers.models.activations import (
GEGLU,
GELU,
ApproximateGELU,
LinearActivation,
SwiGLU,
)
from sglang.multimodal_gen.runtime.layers.activation import get_act_fn
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.quantization import QuantizationConfig
from sglang.srt.utils import add_prefix
class MLP(nn.Module):
"""
MLP for DiT blocks, NO gated linear units
"""
def __init__(
self,
input_dim: int,
mlp_hidden_dim: int,
output_dim: int | None = None,
bias: bool = True,
act_type: str = "gelu_pytorch_tanh",
dtype: torch.dtype | None = None,
prefix: str = "",
quant_config: QuantizationConfig = None,
):
super().__init__()
self.fc_in = ColumnParallelLinear(
input_dim,
mlp_hidden_dim,
bias=True,
gather_output=False,
quant_config=quant_config,
prefix=add_prefix("fc_in", prefix),
)
self.act = get_act_fn(act_type)
if output_dim is None:
output_dim = input_dim
self.fc_out = RowParallelLinear(
mlp_hidden_dim,
output_dim,
bias=True,
input_is_parallel=True,
quant_config=quant_config,
prefix=add_prefix("fc_out", prefix),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x, _ = self.fc_in(x)
x = self.act(x)
x, _ = self.fc_out(x)
return x
class FeedForward(nn.Module):
r"""
A feed-forward layer.
Parameters:
dim (`int`): The number of channels in the input.
dim_out (`int`, *optional*): The number of channels in the output. If not given, defaults to `dim`.
mult (`int`, *optional*, defaults to 4): The multiplier to use for the hidden dimension.
activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward.
bias (`bool`, defaults to True): Whether to use a bias in the linear layer.
"""
def __init__(
self,
dim: int,
dim_out: Optional[int] = None,
mult: int = 4,
activation_fn: str = "geglu",
inner_dim=None,
bias: bool = True,
):
super().__init__()
if inner_dim is None:
inner_dim = int(dim * mult)
dim_out = dim_out if dim_out is not None else dim
if activation_fn == "gelu":
act_fn = GELU(dim, inner_dim, bias=bias)
if activation_fn == "gelu-approximate":
act_fn = GELU(dim, inner_dim, approximate="tanh", bias=bias)
elif activation_fn == "geglu":
act_fn = GEGLU(dim, inner_dim, bias=bias)
elif activation_fn == "geglu-approximate":
act_fn = ApproximateGELU(dim, inner_dim, bias=bias)
elif activation_fn == "swiglu":
act_fn = SwiGLU(dim, inner_dim, bias=bias)
elif activation_fn == "linear-silu":
act_fn = LinearActivation(dim, inner_dim, bias=bias, activation="silu")
self.net = nn.ModuleList([])
# project in
self.net.append(act_fn)
# dummy dropout layer to match with checkpoints compatible with diffusers
self.net.append(nn.Dropout(0.0))
# project out
self.net.append(nn.Linear(inner_dim, dim_out, bias=bias))
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
for module in self.net:
hidden_states = module(hidden_states)
return hidden_states
@@ -0,0 +1,801 @@
import contextvars
import math
from contextlib import contextmanager
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.nn.functional as F
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_group_coordinator,
get_decode_parallel_rank,
get_decode_parallel_world_size,
)
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
if current_platform.is_cuda():
from sglang.jit_kernel.diffusion.causal_conv3d_cat_pad import (
can_use_fused_causal_conv3d_cat_pad_cuda,
fused_causal_conv3d_cat_pad_cuda,
)
from sglang.jit_kernel.diffusion.triton.causal_conv3d_pad import (
fused_causal_conv3d_cat_pad as fused_causal_conv3d_cat_pad_triton,
)
else:
can_use_fused_causal_conv3d_cat_pad_cuda = None
fused_causal_conv3d_cat_pad_cuda = None
fused_causal_conv3d_cat_pad_triton = None
_causal_conv3d_cat_pad_cuda_failed = False
def fused_causal_conv3d_cat_pad(
x: torch.Tensor,
cache_x: torch.Tensor,
padding: list[int],
) -> torch.Tensor:
global _causal_conv3d_cat_pad_cuda_failed
if (
fused_causal_conv3d_cat_pad_cuda is not None
and can_use_fused_causal_conv3d_cat_pad_cuda(x, cache_x, padding)
and not _causal_conv3d_cat_pad_cuda_failed
):
try:
return fused_causal_conv3d_cat_pad_cuda(x, cache_x, padding)
except Exception:
logger.warning(
"fused_causal_conv3d_cat_pad_cuda failed, falling back to Triton",
exc_info=True,
)
_causal_conv3d_cat_pad_cuda_failed = True
if fused_causal_conv3d_cat_pad_triton is None:
raise RuntimeError("causal Conv3D cat/pad fusion is only available on CUDA")
return fused_causal_conv3d_cat_pad_triton(x, cache_x, padding)
_SPATIAL_PARALLEL_DECODE_DISABLED = contextvars.ContextVar(
"spatial_parallel_decode_disabled", default=False
)
@contextmanager
def disable_spatial_parallel_decode():
token = _SPATIAL_PARALLEL_DECODE_DISABLED.set(True)
try:
yield
finally:
_SPATIAL_PARALLEL_DECODE_DISABLED.reset(token)
def spatial_parallel_decode_disabled() -> bool:
return _SPATIAL_PARALLEL_DECODE_DISABLED.get()
def _tensor_pad(x: torch.Tensor, len_to_pad: int, dim: int = -2):
return torch.cat(
[
x,
torch.zeros(
*x.shape[:dim],
len_to_pad,
*x.shape[dim + 1 :],
dtype=x.dtype,
device=x.device,
),
],
dim=dim,
)
def _tensor_chunk(x: torch.Tensor, dim: int = -2, world_size: int = 1, rank: int = 0):
if x is None:
return x
if world_size <= 1:
return x
return torch.tensor_split(x, world_size, dim=dim)[rank].contiguous(
memory_format=_halo_memory_format(x)
)
def _can_fuse_causal_conv3d_cat_pad(
x: torch.Tensor,
cache_x: torch.Tensor | None,
padding: list[int],
) -> bool:
if cache_x is None or fused_causal_conv3d_cat_pad is None:
return False
if not current_platform.is_cuda():
return False
if not x.is_cuda or not x.is_contiguous() or not cache_x.is_contiguous():
return False
if x.dim() != 5 or cache_x.dim() != 5 or x.dtype != cache_x.dtype:
return False
if x.shape[0] != cache_x.shape[0] or x.shape[1] != cache_x.shape[1]:
return False
if x.shape[3:] != cache_x.shape[3:]:
return False
width_left, width_right, height_top, height_bottom, depth_left, depth_right = (
padding
)
if width_left != width_right or height_top != height_bottom or depth_right != 0:
return False
if depth_left < cache_x.shape[2]:
return False
return bool(width_left or height_top)
def causal_conv3d_cat_pad(
x: torch.Tensor,
cache_x: torch.Tensor | None,
padding: list[int],
) -> torch.Tensor:
if cache_x is not None and padding[4] > 0:
if cache_x.device != x.device:
cache_x = cache_x.to(x.device)
if _can_fuse_causal_conv3d_cat_pad(x, cache_x, padding):
return fused_causal_conv3d_cat_pad(x, cache_x, padding)
x = torch.cat([cache_x, x], dim=2)
padding[4] -= cache_x.shape[2]
if any(padding):
x = F.pad(x, padding)
return x
def split_for_parallel_decode(
x: torch.Tensor, upsample_count: int, world_size: int, rank: int
):
return split_height_for_parallel_decode(
x,
expected_height=x.shape[-2] * (2**upsample_count),
world_size=world_size,
rank=rank,
)
def split_height_for_parallel_decode(
x: torch.Tensor, expected_height: int, world_size: int, rank: int
):
if spatial_parallel_decode_disabled():
return x, None
x = _tensor_chunk(x, dim=-2, world_size=world_size, rank=rank)
return x, expected_height
def _maybe_contiguous_for_sp_gather(x: torch.Tensor) -> torch.Tensor:
if (
x.dim() == 5
and hasattr(torch, "channels_last_3d")
and x.is_contiguous(memory_format=torch.channels_last_3d)
and not x.is_contiguous()
):
return x.contiguous()
if (
x.dim() == 4
and x.is_contiguous(memory_format=torch.channels_last)
and not x.is_contiguous()
):
return x.contiguous()
return x
def gather_and_trim_height(x: torch.Tensor, expected_height: int | None):
if spatial_parallel_decode_disabled():
return x
if expected_height is None:
return x
x, _ = gather_variable_height(x)
if x.shape[-2] != expected_height:
x = x[..., :expected_height, :].contiguous()
return x
def gather_height_for_global_op(x: torch.Tensor) -> torch.Tensor:
if spatial_parallel_decode_disabled():
return x
return gather_variable_height(x)[0]
def chunk_height_for_parallel_decode(x: torch.Tensor) -> torch.Tensor:
if spatial_parallel_decode_disabled():
return x
return _tensor_chunk(
x,
dim=-2,
world_size=get_decode_parallel_world_size(),
rank=get_decode_parallel_rank(),
)
def chunk_height_by_sizes(x: torch.Tensor, heights: list[int]) -> torch.Tensor:
if spatial_parallel_decode_disabled():
return x
rank = get_decode_parallel_rank()
start = sum(heights[:rank])
return x[..., start : start + heights[rank], :].contiguous(
memory_format=_halo_memory_format(x)
)
def gather_height_sizes(x: torch.Tensor) -> list[int]:
"""gather heights of sharded feature_maps from peers"""
if spatial_parallel_decode_disabled():
return [x.shape[-2]]
world_size = get_decode_parallel_world_size()
if world_size <= 1:
return [x.shape[-2]]
local_height = torch.tensor([x.shape[-2]], device=x.device, dtype=torch.int64)
gathered = [torch.empty_like(local_height) for _ in range(world_size)]
dist.all_gather(
gathered,
local_height,
group=get_decode_parallel_group_coordinator().device_group,
)
return [int(height.item()) for height in gathered]
def gather_variable_height(x: torch.Tensor) -> tuple[torch.Tensor, list[int]]:
if spatial_parallel_decode_disabled():
return x, [x.shape[-2]]
world_size = get_decode_parallel_world_size()
if world_size <= 1:
return x, [x.shape[-2]]
heights = gather_height_sizes(x)
max_height = max(heights)
if x.shape[-2] < max_height:
x = _tensor_pad(x, max_height - x.shape[-2], dim=-2)
gathered = get_decode_parallel_group_coordinator().all_gather(
_maybe_contiguous_for_sp_gather(x), dim=-2
)
chunks = torch.split(gathered, max_height, dim=-2)
return (
torch.cat(
[chunk[..., :height, :] for chunk, height in zip(chunks, heights)], dim=-2
),
heights,
)
def _halo_memory_format(reference: torch.Tensor) -> torch.memory_format:
if reference.dim() > 1 and reference.stride(1) == 1:
if reference.dim() == 5 and hasattr(torch, "channels_last_3d"):
return torch.channels_last_3d
if reference.dim() == 4:
return torch.channels_last
return torch.contiguous_format
def _ensure_recv_buf(
recv_buf: torch.Tensor | None, reference: torch.Tensor
) -> torch.Tensor:
memory_format = _halo_memory_format(reference)
if (
recv_buf is None
or recv_buf.shape != reference.shape
or recv_buf.dtype != reference.dtype
or recv_buf.device != reference.device
or not recv_buf.is_contiguous(memory_format=memory_format)
):
return torch.empty(
reference.shape,
dtype=reference.dtype,
device=reference.device,
memory_format=memory_format,
)
return recv_buf
def halo_exchange(
x: torch.Tensor,
height_halo_size: int = 1,
recv_top_buf: torch.Tensor | None = None,
recv_bottom_buf: torch.Tensor | None = None,
height_pad_mode: str = "zeros",
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""exchange(send and recv) top/bottom conv-input halos with adjacent spatial ranks"""
if spatial_parallel_decode_disabled():
return x, recv_top_buf, recv_bottom_buf
if height_halo_size == 0:
return x, recv_top_buf, recv_bottom_buf
decode_group = get_decode_parallel_group_coordinator()
rank = get_decode_parallel_rank()
world_size = get_decode_parallel_world_size()
group = decode_group.device_group
group_ranks = decode_group.ranks
top_row_ref = x[..., :height_halo_size, :]
bottom_row_ref = x[..., -height_halo_size:, :]
recv_top_buf = _ensure_recv_buf(recv_top_buf, top_row_ref)
recv_bottom_buf = _ensure_recv_buf(recv_bottom_buf, bottom_row_ref)
p2p_ops = []
if rank > 0:
prev_rank = group_ranks[rank - 1]
top_row = top_row_ref.contiguous(memory_format=_halo_memory_format(top_row_ref))
p2p_ops.append(dist.P2POp(dist.irecv, recv_top_buf, prev_rank, group))
p2p_ops.append(dist.P2POp(dist.isend, top_row, prev_rank, group))
if rank < world_size - 1:
next_rank = group_ranks[rank + 1]
bottom_row = bottom_row_ref.contiguous(
memory_format=_halo_memory_format(bottom_row_ref)
)
p2p_ops.append(dist.P2POp(dist.isend, bottom_row, next_rank, group))
p2p_ops.append(dist.P2POp(dist.irecv, recv_bottom_buf, next_rank, group))
if p2p_ops:
reqs = dist.batch_isend_irecv(p2p_ops)
for req in reqs:
req.wait()
if rank == 0:
recv_top_buf.copy_(
_make_boundary_halo(
x,
recv_bottom_buf if world_size > 1 else None,
height_halo_size=height_halo_size,
is_top=True,
mode=height_pad_mode,
)
)
if rank == world_size - 1:
recv_bottom_buf.copy_(
_make_boundary_halo(
x,
recv_top_buf if world_size > 1 else None,
height_halo_size=height_halo_size,
is_top=False,
mode=height_pad_mode,
)
)
return (
torch.concat([recv_top_buf, x, recv_bottom_buf], dim=-2),
recv_top_buf,
recv_bottom_buf,
)
def _make_boundary_halo(
x: torch.Tensor,
neighbor: torch.Tensor | None,
*,
height_halo_size: int,
is_top: bool,
mode: str,
) -> torch.Tensor:
if mode == "zeros":
shape = list(x.shape)
shape[-2] = height_halo_size
return torch.zeros(shape, dtype=x.dtype, device=x.device)
if mode == "replicate":
edge = x[..., :1, :] if is_top else x[..., -1:, :]
return edge.expand(*edge.shape[:-2], height_halo_size, edge.shape[-1])
if mode == "reflect":
source = x
if is_top and neighbor is not None:
source = torch.cat([x, neighbor], dim=-2)
elif not is_top and neighbor is not None:
source = torch.cat([neighbor, x], dim=-2)
if is_top:
index = torch.arange(
height_halo_size, 0, -1, device=x.device, dtype=torch.long
)
else:
index = torch.arange(
source.shape[-2] - 2,
source.shape[-2] - 2 - height_halo_size,
-1,
device=x.device,
dtype=torch.long,
)
return source.index_select(-2, index)
raise ValueError(f"Unsupported spatial padding mode for parallel decode: {mode}")
def _pad_with_mode(
x: torch.Tensor, padding: tuple[int, ...], mode: str
) -> torch.Tensor:
if mode == "zeros":
return F.pad(x, padding)
return F.pad(x, padding, mode=mode)
def _set_conv_padding(module: nn.Module, padding: tuple[int, ...]) -> None:
module.padding = padding
module._reversed_padding_repeated_twice = tuple(
value for pad in reversed(padding) for value in (pad, pad)
)
def _conv_preserves_local_height(
*,
height_halo_size: int,
height_pad_top: int,
height_pad_bottom: int,
kernel_height: int,
dilation_height: int,
stride_height: int,
) -> bool:
kernel_span = dilation_height * (kernel_height - 1)
return (
stride_height == 1
and 2 * height_halo_size == kernel_span
and height_pad_top == height_halo_size
and height_pad_bottom == height_halo_size
)
def _conv3d_weight_is_channels_last_3d(weight: torch.Tensor) -> bool:
return (
weight.dim() == 5
and hasattr(torch, "channels_last_3d")
and (current_platform.is_cuda() or current_platform.is_rocm())
and weight.is_contiguous(memory_format=torch.channels_last_3d)
)
def _match_conv3d_input_format(x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
if x.dim() == 5 and _conv3d_weight_is_channels_last_3d(weight):
return x.contiguous(memory_format=torch.channels_last_3d)
return x
def _spatial_parallel_conv_forward(
module: nn.Module,
x: torch.Tensor,
conv_forward,
*,
height_pad_mode: str,
match_conv3d_format: bool = False,
) -> torch.Tensor:
# send and recv halo
# x_padded: concatenated input
x_padded, module._halo_recv_top_buf, module._halo_recv_bottom_buf = halo_exchange(
x,
height_halo_size=module.height_halo_size,
recv_top_buf=module._halo_recv_top_buf,
recv_bottom_buf=module._halo_recv_bottom_buf,
height_pad_mode=height_pad_mode,
)
if match_conv3d_format:
x_padded = _match_conv3d_input_format(x_padded, module.weight)
if module.height_halo_size == 0:
return conv_forward(x_padded)
stride = module.stride[-2]
if _conv_preserves_local_height(
height_halo_size=module.height_halo_size,
height_pad_top=module.height_pad_top,
height_pad_bottom=module.height_pad_bottom,
kernel_height=module.kernel_size[-2],
dilation_height=module.dilation[-2],
stride_height=stride,
):
return conv_forward(x_padded)
heights = gather_height_sizes(x)
global_start = sum(heights[: module.rank])
global_height = sum(heights)
if stride > 1:
shift = (
global_start - module.height_halo_size + module.height_pad_top
) % stride
if shift:
x_padded = x_padded[..., shift:, :]
global_start += shift
if match_conv3d_format:
x_padded = _match_conv3d_input_format(x_padded, module.weight)
out = conv_forward(x_padded)
# trim the output to original shape
return _trim_conv_output_height(
out,
local_height=x.shape[-2],
global_height=global_height,
global_start=global_start,
height_halo_size=module.height_halo_size,
height_pad_top=module.height_pad_top,
height_pad_bottom=module.height_pad_bottom,
kernel_height=module.kernel_size[-2],
dilation_height=module.dilation[-2],
stride_height=stride,
)
class SpatialParallelConv2d(nn.Conv2d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int],
stride: int | tuple[int, int] = 1,
padding: int | tuple[int, int] = 0,
dilation: int | tuple[int, int] = 1,
groups: int = 1,
bias: bool = True,
padding_mode: str = "zeros",
height_padding: tuple[int, int] | None = None,
):
super().__init__(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
padding_mode=padding_mode,
)
self.height_halo_size = (self.dilation[-2] * (self.kernel_size[-2] - 1)) // 2
if height_padding is None:
height_padding = (self.padding[-2], self.padding[-2])
self.height_pad_top, self.height_pad_bottom = height_padding
self.padding: tuple[int, int]
if self.height_halo_size > 0:
self._padding = (0, 0, 0, 0)
else:
self._padding = (0, 0, self.padding[0], self.padding[0])
_set_conv_padding(self, (0, self.padding[1]))
self._halo_recv_top_buf: torch.Tensor | None = None
self._halo_recv_bottom_buf: torch.Tensor | None = None
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
def forward(self, x):
if spatial_parallel_decode_disabled():
return self._direct_forward(x)
if any(self._padding):
x = _pad_with_mode(x, self._padding, self.padding_mode)
return _spatial_parallel_conv_forward(
self,
x,
super().forward,
height_pad_mode=self.padding_mode,
)
def _direct_forward(self, x):
width_pad = self.padding[-1]
padding = (
width_pad,
width_pad,
self.height_pad_top,
self.height_pad_bottom,
)
if any(padding):
x = _pad_with_mode(x, padding, self.padding_mode)
return F.conv2d(
x,
self.weight,
self.bias,
self.stride,
(0, 0),
self.dilation,
self.groups,
)
class SpatialParallelCausalConv3d(nn.Conv3d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int, int],
stride: int | tuple[int, int, int] = 1,
padding: int | tuple[int, int, int] = 0,
):
super().__init__(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
)
self.height_pad_top = self.padding[1]
self.height_pad_bottom = self.padding[1]
self.height_halo_size = (self.kernel_size[-2] - 1) // 2
self.padding: tuple[int, int, int]
if self.height_halo_size > 0:
self._padding = (
self.padding[2],
self.padding[2],
0,
0,
2 * self.padding[0],
0,
)
else:
self._padding = (
self.padding[2],
self.padding[2],
self.padding[1],
self.padding[1],
2 * self.padding[0],
0,
)
self.padding = (0, 0, 0)
self._halo_recv_top_buf: torch.Tensor | None = None
self._halo_recv_bottom_buf: torch.Tensor | None = None
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
def forward(self, x, cache_x=None):
padding = list(self._padding)
if spatial_parallel_decode_disabled():
padding[2] = self.height_pad_top
padding[3] = self.height_pad_bottom
x = causal_conv3d_cat_pad(x, cache_x, padding)
x = x if current_platform.is_amp_supported() else x.to(self.weight.dtype)
if spatial_parallel_decode_disabled():
x = _match_conv3d_input_format(x, self.weight)
return F.conv3d(
x,
self.weight,
self.bias,
self.stride,
(0, 0, 0),
self.dilation,
self.groups,
)
return _spatial_parallel_conv_forward(
self,
x,
super().forward,
height_pad_mode="zeros",
match_conv3d_format=True,
)
class SpatialParallelConv3d(nn.Conv3d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int, int],
stride: int | tuple[int, int, int] = 1,
padding: int | tuple[int, int, int] = 0,
dilation: int | tuple[int, int, int] = 1,
groups: int = 1,
bias: bool = True,
padding_mode: str = "zeros",
height_padding: tuple[int, int] | None = None,
):
super().__init__(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias=bias,
padding_mode=padding_mode,
)
self.height_halo_size = (self.dilation[-2] * (self.kernel_size[-2] - 1)) // 2
if height_padding is None:
height_padding = (self.padding[-2], self.padding[-2])
self.height_pad_top, self.height_pad_bottom = height_padding
self.padding: tuple[int, int, int]
if self.height_halo_size > 0:
self._padding = (0, 0, 0, 0, 0, 0)
else:
self._padding = (
0,
0,
self.padding[1],
self.padding[1],
0,
0,
)
_set_conv_padding(self, (self.padding[0], 0, self.padding[2]))
self._halo_recv_top_buf: torch.Tensor | None = None
self._halo_recv_bottom_buf: torch.Tensor | None = None
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
def forward(self, x):
if spatial_parallel_decode_disabled():
return self._direct_forward(x)
if any(self._padding):
x = _pad_with_mode(x, self._padding, self.padding_mode)
return _spatial_parallel_conv_forward(
self,
x,
super().forward,
height_pad_mode=self.padding_mode,
match_conv3d_format=True,
)
def _direct_forward(self, x):
time_pad = self.padding[0]
width_pad = self.padding[-1]
padding = (
width_pad,
width_pad,
self.height_pad_top,
self.height_pad_bottom,
time_pad,
time_pad,
)
if any(padding):
x = _pad_with_mode(x, padding, self.padding_mode)
x = _match_conv3d_input_format(x, self.weight)
return F.conv3d(
x,
self.weight,
self.bias,
self.stride,
(0, 0, 0),
self.dilation,
self.groups,
)
class SpatialParallelZeroPad2d(nn.Module):
def __init__(self, padding: tuple[int, int, int, int]) -> None:
super().__init__()
self.padding = padding
self.rank = get_decode_parallel_rank()
self.world_size = get_decode_parallel_world_size()
def forward(self, x: torch.Tensor) -> torch.Tensor:
if spatial_parallel_decode_disabled():
return F.pad(x, self.padding)
left, right, top, bottom = self.padding
top = top if self.rank == 0 else 0
bottom = bottom if self.rank == self.world_size - 1 else 0
return F.pad(x, (left, right, top, bottom))
def _trim_conv_output_height(
out: torch.Tensor,
*,
local_height: int,
global_height: int,
global_start: int,
height_halo_size: int,
height_pad_top: int,
height_pad_bottom: int,
kernel_height: int,
dilation_height: int,
stride_height: int,
) -> torch.Tensor:
kernel_span = dilation_height * (kernel_height - 1)
min_i = math.ceil(
((-height_pad_top) - (global_start - height_halo_size)) / stride_height
)
max_i = math.floor(
(
(global_height - 1 + height_pad_bottom)
- kernel_span
- (global_start - height_halo_size)
)
/ stride_height
)
start = max(min_i, 0)
end = min(max_i + 1, out.shape[-2])
if start != 0 or end != out.shape[-2]:
out = out[..., start:end, :]
return out
@@ -0,0 +1,98 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
from typing import Literal, get_args
from sglang.multimodal_gen.runtime.layers.quantization.bitsandbytes import (
BitsAndBytesConfig,
)
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.layers.quantization.fp8 import Fp8Config
from sglang.multimodal_gen.runtime.layers.quantization.modelopt_fp8 import (
ModelOptFp8Config as ModelOptFp8DiffusionConfig,
)
from sglang.multimodal_gen.runtime.layers.quantization.modelopt_quant import (
ModelOptFp4Config,
ModelOptFp8Config,
)
from sglang.multimodal_gen.runtime.layers.quantization.modelslim import ModelSlimConfig
from sglang.multimodal_gen.runtime.layers.quantization.mxfp4 import Mxfp4Config
from sglang.multimodal_gen.runtime.layers.quantization.mxfp4_npu import (
NPUMXFP4Config,
)
from sglang.multimodal_gen.runtime.layers.quantization.mxfp8_npu import MXFP8Config
QuantizationMethods = Literal[
"fp8",
"modelopt",
"modelopt_fp8",
"modelopt_fp4",
"bitsandbytes",
"modelslim",
"mxfp8",
"mxfp4",
"mxfp4_npu",
]
QUANTIZATION_METHODS: list[str] = list(get_args(QuantizationMethods))
# The customized quantization methods which will be added to this dict.
_CUSTOMIZED_METHOD_TO_QUANT_CONFIG = {
"modelopt": ModelOptFp8DiffusionConfig,
"modelopt_fp8": ModelOptFp8Config,
"modelopt_fp4": ModelOptFp4Config,
"bitsandbytes": BitsAndBytesConfig,
"modelslim": ModelSlimConfig,
"fp8": Fp8Config,
"mxfp4": Mxfp4Config,
"mxfp8": MXFP8Config,
"mxfp4_npu": NPUMXFP4Config,
}
def register_quantization_config(quantization: str):
"""Register a customized vllm quantization config.
When a quantization method is not supported by vllm, you can register a customized
quantization config to support it.
Args:
quantization (str): The quantization method name.
""" # noqa: E501
def _wrapper(quant_config_cls):
if quantization in QUANTIZATION_METHODS:
raise ValueError(
f"The quantization method `{quantization}` is already exists."
)
if not issubclass(quant_config_cls, QuantizationConfig):
raise ValueError(
"The quantization config must be a subclass of " "`QuantizationConfig`."
)
_CUSTOMIZED_METHOD_TO_QUANT_CONFIG[quantization] = quant_config_cls
QUANTIZATION_METHODS.append(quantization)
return quant_config_cls
return _wrapper
def get_quantization_config(quantization: str) -> type[QuantizationConfig]:
if quantization not in QUANTIZATION_METHODS:
raise ValueError(f"Invalid quantization method: {quantization}")
method_to_config: dict[str, type[QuantizationConfig]] = {}
# Update the `method_to_config` with customized quantization methods.
method_to_config.update(_CUSTOMIZED_METHOD_TO_QUANT_CONFIG)
return method_to_config[quantization]
__all__ = [
"QuantizationMethods",
"QuantizationConfig",
"get_quantization_config",
"QUANTIZATION_METHODS",
]
@@ -0,0 +1,437 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from typing import Any, Optional
import torch
import torch.nn as nn
from packaging import version
from sglang.multimodal_gen.runtime.layers.linear import (
LinearBase,
LinearMethodBase,
UnquantizedLinearMethod,
)
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
QuantizeMethodBase,
)
from sglang.multimodal_gen.runtime.utils.weight_attrs import set_weight_attrs
def _require_bitsandbytes() -> None:
try:
import bitsandbytes
if version.parse(bitsandbytes.__version__) < version.parse("0.46.1"):
raise ImportError(
"bitsandbytes version is wrong. Please install bitsandbytes>=0.46.1."
)
except ImportError as err:
raise ImportError(
"Please install bitsandbytes>=0.46.1 via "
"`pip install bitsandbytes>=0.46.1` to use bitsandbytes quantizer."
) from err
def _calculate_quant_ratio(dtype: torch.dtype) -> int:
if dtype.is_floating_point:
return torch.finfo(dtype).bits // torch.iinfo(torch.uint8).bits
return torch.iinfo(dtype).bits // torch.iinfo(torch.uint8).bits
def _is_layer_skipped(prefix: str, skipped_modules: list[str]) -> bool:
components = prefix.split(".")
if any(module_name in components for module_name in skipped_modules):
return True
prefixes = {".".join(components[: i + 1]) for i in range(len(components))}
return bool(set(skipped_modules) & prefixes)
class BitsAndBytesConfig(QuantizationConfig):
"""Config class for pre-quantized bitsandbytes 4-bit checkpoints."""
def __init__(
self,
load_in_8bit: bool = False,
load_in_4bit: bool = True,
bnb_4bit_compute_dtype: str = "float32",
bnb_4bit_quant_storage: str = "uint8",
bnb_4bit_quant_type: str = "fp4",
bnb_4bit_use_double_quant: bool = False,
llm_int8_enable_fp32_cpu_offload: bool = False,
llm_int8_has_fp16_weight: bool = False,
llm_int8_skip_modules: list[str] | None = None,
llm_int8_threshold: float = 6.0,
) -> None:
super().__init__()
self.load_in_8bit = load_in_8bit
self.load_in_4bit = load_in_4bit
self.bnb_4bit_compute_dtype = bnb_4bit_compute_dtype
self.bnb_4bit_quant_storage = bnb_4bit_quant_storage
self.bnb_4bit_quant_type = bnb_4bit_quant_type
self.bnb_4bit_use_double_quant = bnb_4bit_use_double_quant
self.llm_int8_enable_fp32_cpu_offload = llm_int8_enable_fp32_cpu_offload
self.llm_int8_has_fp16_weight = llm_int8_has_fp16_weight
self.llm_int8_skip_modules = llm_int8_skip_modules or []
self.llm_int8_threshold = llm_int8_threshold
if self.load_in_8bit or not self.load_in_4bit:
raise ValueError("SGLang diffusion only supports bitsandbytes 4-bit.")
if self.bnb_4bit_quant_storage != "uint8":
raise ValueError(
f"Unsupported bnb_4bit_quant_storage: {self.bnb_4bit_quant_storage}"
)
@classmethod
def get_name(cls) -> str:
return "bitsandbytes"
def get_scaled_act_names(self) -> list[str]:
return []
@classmethod
def get_supported_act_dtypes(cls) -> list[torch.dtype]:
return [torch.float32, torch.float16, torch.bfloat16]
@classmethod
def get_min_capability(cls) -> int:
return 70
@staticmethod
def get_config_filenames() -> list[str]:
return []
@classmethod
def from_config(cls, config: dict[str, Any]) -> BitsAndBytesConfig:
def get_safe_value(keys, default_value=None):
try:
value = QuantizationConfig.get_from_keys(config, keys)
return value if value is not None else default_value
except ValueError:
return default_value
return cls(
load_in_8bit=get_safe_value(["load_in_8bit"], False),
load_in_4bit=get_safe_value(["load_in_4bit"], True),
bnb_4bit_compute_dtype=get_safe_value(
["bnb_4bit_compute_dtype"], "float32"
),
bnb_4bit_quant_storage=get_safe_value(["bnb_4bit_quant_storage"], "uint8"),
bnb_4bit_quant_type=get_safe_value(["bnb_4bit_quant_type"], "fp4"),
bnb_4bit_use_double_quant=get_safe_value(
["bnb_4bit_use_double_quant"], False
),
llm_int8_enable_fp32_cpu_offload=get_safe_value(
["llm_int8_enable_fp32_cpu_offload"], False
),
llm_int8_has_fp16_weight=get_safe_value(
["llm_int8_has_fp16_weight"], False
),
llm_int8_skip_modules=get_safe_value(["llm_int8_skip_modules"], []),
llm_int8_threshold=get_safe_value(["llm_int8_threshold"], 6.0),
)
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> Optional[QuantizeMethodBase]:
if isinstance(layer, LinearBase):
if _is_layer_skipped(prefix, self.llm_int8_skip_modules):
return UnquantizedLinearMethod()
return BitsAndBytesLinearMethod(self)
return None
class BitsAndBytesLinearMethod(LinearMethodBase):
"""Linear method for pre-quantized bitsandbytes 4-bit weights."""
def __init__(self, quant_config: BitsAndBytesConfig):
_require_bitsandbytes()
self.quant_config = quant_config
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: list[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
) -> None:
quant_ratio = _calculate_quant_ratio(params_dtype)
output_size_per_partition = sum(output_partition_sizes)
total_size = input_size_per_partition * output_size_per_partition
if total_size % quant_ratio != 0:
raise ValueError(
"The input size is not aligned with the quantized weight shape."
)
qweight = nn.Parameter(
torch.empty(total_size // quant_ratio, 1, dtype=torch.uint8),
requires_grad=False,
)
set_weight_attrs(
qweight,
{
"input_dim": 0,
"output_dim": 0,
"pack_factor": quant_ratio,
"use_bitsandbytes_4bit": True,
"bnb_full_shape": (output_size, input_size),
"bnb_local_shape": (
output_size_per_partition,
input_size_per_partition,
),
"bnb_output_shard_start": getattr(layer, "tp_rank", 0)
* output_size_per_partition,
"bnb_input_shard_start": (
0
if input_size_per_partition == input_size
else getattr(layer, "tp_rank", 0) * input_size_per_partition
),
},
)
layer.register_parameter("weight", qweight)
set_weight_attrs(qweight, extra_weight_attrs)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: torch.Tensor | None = None,
) -> torch.Tensor:
original_type = x.dtype
original_shape = x.shape
if x.ndim > 2:
x = x.reshape(-1, x.size(-1))
out_dim = sum(
quant_state.shape[0]
for quant_state in layer.weight.bnb_quant_state.values()
)
out = torch.empty(x.shape[0], out_dim, dtype=torch.bfloat16, device=x.device)
apply_bnb_4bit(x.to(torch.bfloat16), layer.weight, out)
out = out.to(original_type)
if len(original_shape) > 2:
out = out.view(*original_shape[:-1], out.size(-1))
if bias is not None:
out = out + bias
return out
def apply_bnb_4bit(
x: torch.Tensor,
weight: torch.Tensor,
out: torch.Tensor,
) -> None:
from bitsandbytes import matmul_4bit
offsets = weight.bnb_shard_offsets
quant_states = weight.bnb_quant_state
current_index = 0
for i in range(len(quant_states)):
output_size = quant_states[i].shape[0]
out[:, current_index : current_index + output_size] = matmul_4bit(
x,
weight[offsets[i] : offsets[i + 1]].t(),
quant_states[i],
)
current_index += output_size
class BitsAndBytes4BitLinear(nn.Module):
"""Storage-only bitsandbytes 4-bit linear for nn.Linear-based encoders."""
def __init__(
self,
in_features: int,
out_features: int,
bias: bool = True,
compute_dtype: torch.dtype | None = None,
) -> None:
super().__init__()
_require_bitsandbytes()
self.in_features = in_features
self.out_features = out_features
self.compute_dtype = compute_dtype
quant_ratio = _calculate_quant_ratio(compute_dtype or torch.get_default_dtype())
total_size = in_features * out_features
if total_size % quant_ratio != 0:
raise ValueError(
"The input size is not aligned with the quantized weight shape."
)
self.weight = nn.Parameter(
torch.empty(total_size // quant_ratio, 1, dtype=torch.uint8),
requires_grad=False,
)
set_weight_attrs(
self.weight,
{
"pack_factor": quant_ratio,
"use_bitsandbytes_4bit": True,
},
)
if bias:
self.bias = nn.Parameter(
torch.empty(
out_features, dtype=compute_dtype or torch.get_default_dtype()
),
requires_grad=False,
)
else:
self.register_parameter("bias", None)
def forward(self, x: torch.Tensor) -> torch.Tensor:
original_type = x.dtype
original_shape = x.shape
if x.ndim > 2:
x = x.reshape(-1, x.size(-1))
out = torch.empty(
x.shape[0], self.out_features, dtype=torch.bfloat16, device=x.device
)
apply_bnb_4bit(x.to(torch.bfloat16), self.weight, out)
out = out.to(original_type)
if len(original_shape) > 2:
out = out.view(*original_shape[:-1], out.size(-1))
if self.bias is not None:
out = out + self.bias
return out
def swap_linears_to_bitsandbytes_4bit(module: nn.Module) -> None:
for name, child in list(module.named_children()):
if isinstance(child, nn.Linear):
replacement = BitsAndBytes4BitLinear(
child.in_features,
child.out_features,
bias=child.bias is not None,
compute_dtype=child.weight.dtype,
)
setattr(module, name, replacement)
else:
swap_linears_to_bitsandbytes_4bit(child)
_BNB_4BIT_STATE_SUFFIXES = {
"absmax",
"quant_map",
"nested_absmax",
"nested_quant_map",
"bitsandbytes",
}
def is_bitsandbytes_4bit_state_name(weight_name: str) -> bool:
suffix = weight_name.split(".")[-1]
return any(state_suffix in suffix for state_suffix in _BNB_4BIT_STATE_SUFFIXES)
def split_bitsandbytes_4bit_state(
weights: Any,
) -> tuple[list[tuple[str, torch.Tensor]], dict[str, torch.Tensor]]:
normal_weights: list[tuple[str, torch.Tensor]] = []
quant_state_dict: dict[str, torch.Tensor] = {}
for name, tensor in weights:
if is_bitsandbytes_4bit_state_name(name):
if "quant_state.bitsandbytes" in name:
tensor = tensor.cpu().data
quant_state_dict[name] = tensor
continue
normal_weights.append((name, tensor))
return normal_weights, quant_state_dict
def build_bitsandbytes_4bit_quant_states(
normal_weight_names: list[str],
quant_state_dict: dict[str, torch.Tensor],
device: torch.device,
param_names_mapping=None,
) -> dict[str, Any]:
from bitsandbytes.functional import QuantState
quant_states: dict[str, Any] = {}
device_str = str(device)
for source_name in normal_weight_names:
if (
f"{source_name}.quant_state.bitsandbytes__nf4" not in quant_state_dict
and f"{source_name}.quant_state.bitsandbytes__fp4" not in quant_state_dict
):
continue
target_name = source_name
if param_names_mapping is not None:
target_name, _, _ = param_names_mapping(source_name)
state_tensors = {
name: tensor
for name, tensor in quant_state_dict.items()
if name.startswith(f"{source_name}.")
}
quant_states[target_name] = QuantState.from_dict(
state_tensors, device=device_str
)
return quant_states
def attach_bitsandbytes_4bit_quant_states(
params_dict: dict[str, torch.nn.Parameter],
quant_states: dict[str, Any],
) -> None:
for param_name, quant_state in quant_states.items():
param = params_dict.get(param_name)
if param is None:
raise ValueError(f"Parameter {param_name} not found in the model.")
quant_state = _maybe_shard_bitsandbytes_4bit_quant_state(param, quant_state)
state_by_shard = {0: quant_state}
set_weight_attrs(param, {"bnb_quant_state": state_by_shard})
offsets = torch.tensor([0, param.numel()]).cpu()
set_weight_attrs(param, {"bnb_shard_offsets": offsets})
def _maybe_shard_bitsandbytes_4bit_quant_state(
param: torch.nn.Parameter,
quant_state: Any,
) -> Any:
full_shape = tuple(getattr(param, "bnb_full_shape", tuple(quant_state.shape or ())))
local_shape = tuple(getattr(param, "bnb_local_shape", full_shape))
if not full_shape or local_shape == full_shape:
return quant_state
output_start = getattr(param, "bnb_output_shard_start", 0)
input_start = getattr(param, "bnb_input_shard_start", 0)
if input_start != 0 or local_shape[1] != full_shape[1]:
raise NotImplementedError(
"bitsandbytes 4-bit TP only supports column-parallel output shards."
)
if getattr(quant_state, "nested", False):
raise NotImplementedError(
"bitsandbytes 4-bit TP does not support nested quant states."
)
blocksize = quant_state.blocksize
start_elem = output_start * full_shape[1]
local_numel = local_shape[0] * local_shape[1]
if start_elem % blocksize != 0 or local_numel % blocksize != 0:
raise ValueError(
"bitsandbytes 4-bit TP shard is not aligned to quantization blocks."
)
start_block = start_elem // blocksize
num_blocks = local_numel // blocksize
return type(quant_state)(
absmax=quant_state.absmax.narrow(0, start_block, num_blocks).contiguous(),
shape=torch.Size(local_shape),
code=quant_state.code,
blocksize=quant_state.blocksize,
quant_type=quant_state.quant_type,
dtype=quant_state.dtype,
offset=None,
state2=None,
)
@@ -0,0 +1,155 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/model_executor/layers/quantization/base_config.py
import inspect
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
import torch
from torch import nn
if TYPE_CHECKING:
from sglang.multimodal_gen.runtime.layers.quantization import QuantizationMethods
else:
QuantizationMethods = str
class QuantizeMethodBase(ABC):
"""Base class for different quantized methods."""
@abstractmethod
def create_weights(
self, layer: torch.nn.Module, *weight_args, **extra_weight_attrs
):
"""Create weights for a layer.
The weights will be set as attributes of the layer."""
raise NotImplementedError
@abstractmethod
def apply(self, layer: torch.nn.Module, *args, **kwargs) -> torch.Tensor:
"""Apply the weights in layer to the input tensor.
Expects create_weights to have been called before on the layer."""
raise NotImplementedError
# Not required functions
def embedding(self, layer: torch.nn.Module, *args, **kwargs) -> torch.Tensor:
"""Gather embeddings in the layer based on indices in the input tensor.
Expects create_weights to have been called before on the layer."""
raise NotImplementedError
def process_weights_after_loading(self, layer: nn.Module) -> None:
"""Process the weight after loading.
This can be used for example, to transpose weights for computation.
"""
return
def method_has_implemented_embedding(method_class: type[QuantizeMethodBase]) -> bool:
"""
Not all quant methods have embedding implemented, so we need to check that
it exists for our given method. We check this by making sure the function
has been changed from the base implementation.
"""
base_embedding = inspect.getattr_static(QuantizeMethodBase, "embedding", None)
class_embedding = inspect.getattr_static(method_class, "embedding", None)
return class_embedding is not None and class_embedding is not base_embedding
class QuantizationConfig(ABC):
"""Base class for quantization configs."""
# for quantization frameworks with a separate quantized model provided, e.g. Nunchaku
quantized_model_path: str | None = None
def __init__(self):
super().__init__()
# mapping is updated by models as they initialize
self.packed_modules_mapping: dict[str, list[str]] = dict()
@abstractmethod
def get_name(self) -> QuantizationMethods:
"""Name of the quantization method."""
raise NotImplementedError
@abstractmethod
def get_supported_act_dtypes(self) -> list[torch.dtype]:
"""List of supported activation dtypes."""
raise NotImplementedError
@classmethod
@abstractmethod
def get_min_capability(cls) -> int:
"""Minimum GPU capability to support the quantization method.
E.g., 70 for Volta, 75 for Turing, 80 for Ampere.
This requirement is due to the custom CUDA kernels used by the
quantization method.
"""
raise NotImplementedError
@staticmethod
@abstractmethod
def get_config_filenames() -> list[str]:
"""List of filenames to search for in the model directory."""
raise NotImplementedError
@classmethod
@abstractmethod
def from_config(cls, config: dict[str, Any]) -> "QuantizationConfig":
"""Create a config class from the model's quantization config."""
raise NotImplementedError
@classmethod
def override_quantization_method(
cls, hf_quant_cfg, user_quant
) -> QuantizationMethods | None:
"""
Detects if this quantization method can support a given checkpoint
format by overriding the user specified quantization method --
this method should only be overwritten by subclasses in exceptional
circumstances
"""
return None
@staticmethod
def get_from_keys(config: dict[str, Any], keys: list[str]) -> Any:
"""Get a value from the model's quantization config."""
for key in keys:
if key in config:
return config[key]
raise ValueError(
f"Cannot find any of {keys} in the model's " "quantization config."
)
@staticmethod
def get_from_keys_or(config: dict[str, Any], keys: list[str], default: Any) -> Any:
"""Get a optional value from the model's quantization config."""
try:
return QuantizationConfig.get_from_keys(config, keys)
except ValueError:
return default
@abstractmethod
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> QuantizeMethodBase | None:
"""Get the quantize method to use for the quantized layer.
Args:
layer: The layer for the quant method.
prefix: The full name of the layer in the state dict
Returns:
The quantize method. None if the given layer doesn't support quant
method.
"""
raise NotImplementedError
def get_cache_scale(self, name: str) -> str | None:
return None
@@ -0,0 +1,283 @@
# SPDX-License-Identifier: Apache-2.0
import json
import os
from dataclasses import dataclass
from functools import lru_cache
from typing import Any, Optional
import torch
from safetensors.torch import load_file as safetensors_load_file
from torch import nn
from sglang.multimodal_gen.runtime.layers.linear import LinearBase
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from .base_config import QuantizationConfig, QuantizeMethodBase
logger = init_logger(__name__)
@lru_cache(maxsize=1)
def is_nunchaku_available() -> bool:
try:
import nunchaku # noqa
logger.debug("Nunchaku package detected")
return True
except Exception:
return False
@dataclass
class NunchakuConfig(QuantizationConfig):
"""
Configuration for Nunchaku (SVDQuant) W4A4-style quantization.
Attributes:
precision: Quantization precision type. Options:
- "int4": Standard INT4 quantization
- "nvfp4": FP4 quantization
rank: SVD low-rank dimension for absorbing outliers
group_size: Quantization group size (automatically set based on precision)
act_unsigned: Use unsigned activation quantization
transformer_weights_path: Path to pre-quantized transformer weights (.safetensors)
model_cls: DiT model class that provides quantization rules via get_nunchaku_quant_rules()
"""
precision: str = "int4"
rank: int = 32
group_size: Optional[int] = None
act_unsigned: bool = False
transformer_weights_path: Optional[str] = None
model_cls: Optional[type] = None
@classmethod
def get_name(cls) -> str:
return "svdquant"
@classmethod
def get_supported_act_dtypes(cls) -> list[torch.dtype]:
return [torch.bfloat16, torch.float16]
@classmethod
def get_min_capability(cls) -> int:
return 70
@staticmethod
def get_config_filenames() -> list[str]:
return ["quantization_config.json", "quant_config.json"]
@classmethod
def from_config(cls, config: dict[str, Any]) -> "NunchakuConfig":
return cls(
precision=config.get("precision", "int4"),
rank=int(config.get("rank", 32)),
group_size=config.get("group_size"),
act_unsigned=bool(config.get("act_unsigned", False)),
transformer_weights_path=config.get("transformer_weights_path"),
)
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> Optional[QuantizeMethodBase]:
if not isinstance(layer, LinearBase):
return None
# get quantization rules from model class
quant_rules = self._get_quant_rules()
# priority: skip > awq_w4a16 > svdq_w4a4 > default
skip_patterns = quant_rules.get("skip", [])
for pattern in skip_patterns:
if pattern in prefix.lower():
return None
awq_patterns = quant_rules.get("awq_w4a16", [])
for pattern in awq_patterns:
if pattern in prefix:
from ..nunchaku_linear import NunchakuAWQLinearMethod
return NunchakuAWQLinearMethod(group_size=64)
svdq_patterns = quant_rules.get("svdq_w4a4", [])
for pattern in svdq_patterns:
if pattern in prefix:
from ..nunchaku_linear import NunchakuSVDQLinearMethod
return NunchakuSVDQLinearMethod(
precision=self.precision,
rank=self.rank,
act_unsigned=self.act_unsigned,
)
# default: apply svdq_w4a4 to all remaining linear layers
from ..nunchaku_linear import NunchakuSVDQLinearMethod
return NunchakuSVDQLinearMethod(
precision=self.precision,
rank=self.rank,
act_unsigned=self.act_unsigned,
)
def _get_quant_rules(self) -> dict[str, list[str]]:
if self.model_cls is not None and hasattr(
self.model_cls, "get_nunchaku_quant_rules"
):
return self.model_cls.get_nunchaku_quant_rules()
return {}
def __post_init__(self):
if self.group_size is None:
if self.precision == "nvfp4":
self.group_size = 16
elif self.precision == "int4":
self.group_size = 64
else:
raise ValueError(
f"Invalid precision: {self.precision}. Must be 'int4' or 'nvfp4'"
)
if self.precision not in ["int4", "nvfp4"]:
raise ValueError(
f"Invalid precision: {self.precision}. Must be 'int4' or 'nvfp4'"
)
if self.rank <= 0:
raise ValueError(f"Rank must be positive, got {self.rank}")
@classmethod
def from_dict(cls, config_dict: dict) -> "NunchakuConfig":
"""Create configuration from dictionary."""
return cls(**config_dict)
def to_dict(self) -> dict:
"""Convert configuration to dictionary."""
return {
"precision": self.precision,
"rank": self.rank,
"group_size": self.group_size,
"act_unsigned": self.act_unsigned,
"transformer_weights_path": self.transformer_weights_path,
}
@classmethod
def from_pretrained(cls, model_path: str) -> Optional["NunchakuConfig"]:
for filename in cls.get_config_filenames():
config_path = os.path.join(model_path, filename)
if os.path.exists(config_path):
with open(config_path, "r") as f:
config_dict = json.load(f)
if config_dict.get("quant_method") == cls.get_name():
return cls.from_config(config_dict)
return None
def _patch_native_svdq_linear(
module: nn.Module, tensor: Any, svdq_linear_cls: type
) -> bool:
if (
isinstance(module, svdq_linear_cls)
and getattr(module, "wtscale", None) is not None
):
module.wtscale = tensor
return True
return False
def _patch_sglang_svdq_linear(
module: nn.Module, tensor: Any, svdq_method_cls: type
) -> bool:
quant_method = getattr(module, "quant_method", None)
if not isinstance(quant_method, svdq_method_cls):
return False
existing = getattr(module, "wtscale", None)
if isinstance(existing, nn.Parameter):
with torch.no_grad():
existing.data.copy_(tensor.to(existing.data.dtype))
else:
module.wtscale = tensor
# Keep alpha in sync (kernel reads `layer._nunchaku_alpha`)
try:
module._nunchaku_alpha = float(tensor.detach().cpu().item())
except Exception:
module._nunchaku_alpha = None
return True
def _patch_sglang_svdq_wcscales(
module: nn.Module, tensor: Any, svdq_method_cls: type
) -> bool:
quant_method = getattr(module, "quant_method", None)
if not isinstance(quant_method, svdq_method_cls):
return False
existing = getattr(module, "wcscales", None)
if isinstance(existing, nn.Parameter):
with torch.no_grad():
existing.data.copy_(tensor.to(existing.data.dtype))
else:
module.wcscales = tensor
return True
def _patch_nunchaku_scales(
model: nn.Module,
safetensors_list: list[str],
) -> None:
"""Patch transformer module with Nunchaku scale tensors from safetensors weights.
For NVFP4 checkpoints, correctness depends on `wtscale` and attention
`wcscales`. The FSDP loader may skip some of these metadata tensors.
"""
if not safetensors_list:
return
if len(safetensors_list) != 1:
logger.warning(
"Nunchaku scale patch expects a single safetensors file, "
"but got %d files. Skipping.",
len(safetensors_list),
)
return
from nunchaku.models.linear import SVDQW4A4Linear # type: ignore[import]
state_dict = safetensors_load_file(safetensors_list[0])
if state_dict is None:
return
num_wtscale = 0
num_wcscales = 0
from ..nunchaku_linear import NunchakuSVDQLinearMethod
for name, module in model.named_modules():
wt = state_dict.get(f"{name}.wtscale")
if wt is not None:
if _patch_native_svdq_linear(module, wt, SVDQW4A4Linear):
num_wtscale += 1
elif _patch_sglang_svdq_linear(module, wt, NunchakuSVDQLinearMethod):
num_wtscale += 1
wc = state_dict.get(f"{name}.wcscales")
if wc is not None:
# Some modules may have wcscales as a direct attribute/Parameter.
existing = getattr(module, "wcscales", None)
if isinstance(existing, nn.Parameter):
with torch.no_grad():
existing.data.copy_(wc.to(existing.data.dtype))
num_wcscales += 1
elif existing is not None:
setattr(module, "wcscales", wc)
num_wcscales += 1
elif _patch_sglang_svdq_wcscales(module, wc, NunchakuSVDQLinearMethod):
num_wcscales += 1
if num_wtscale > 0:
logger.info("Patched wtscale for %d layers", num_wtscale)
if num_wcscales > 0:
logger.info("Patched wcscales for %d layers", num_wcscales)
@@ -0,0 +1,508 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
import torch
from torch.nn import Module
from torch.nn.parameter import Parameter
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_tensor_model_parallel_world_size,
)
from sglang.multimodal_gen.runtime.layers.linear import (
LinearMethodBase,
UnquantizedLinearMethod,
)
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
QuantizeMethodBase,
)
from sglang.multimodal_gen.runtime.models.parameter import (
BlockQuantScaleParameter,
ModelWeightParameter,
PerTensorScaleParameter,
)
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.platforms.aiter import USE_AITER
from sglang.multimodal_gen.runtime.utils.common import (
cpu_has_amx_support,
get_bool_env_var,
use_intel_amx_backend,
)
from sglang.srt.layers.amx_utils import _amx_process_weight_after_loading
from sglang.srt.layers.quantization.fp8_kernel import (
is_fp8_fnuz,
per_token_group_quant_fp8,
)
from sglang.srt.layers.quantization.fp8_utils import (
apply_fp8_linear,
can_auto_enable_marlin_fp8,
cutlass_fp8_supported,
dispatch_w8a8_block_fp8_linear,
input_to_float8,
normalize_e4m3fn_to_e4m3fnuz,
requant_weight_ue8m0_inplace,
)
from sglang.srt.layers.quantization.marlin_utils_fp8 import (
apply_fp8_marlin_linear,
prepare_fp8_layer_for_marlin,
)
from sglang.srt.layers.quantization.utils import (
convert_to_channelwise,
is_layer_skipped,
requantize_with_max_scale,
)
if TYPE_CHECKING:
from sglang.srt.layers.quantization.w4afp8 import W4AFp8Config
_is_hip = current_platform.is_hip()
_is_cuda = current_platform.is_cuda()
_is_npu = current_platform.is_npu()
_is_cpu_amx_available = cpu_has_amx_support()
_is_cpu = current_platform.is_cpu()
_is_fp8_fnuz = is_fp8_fnuz()
_use_hip_int4 = get_bool_env_var("SGLANG_INT4_WEIGHT") and _is_hip
if USE_AITER or _use_hip_int4:
pass
ACTIVATION_SCHEMES = ["static", "dynamic"]
logger = logging.getLogger(__name__)
class Fp8Config(QuantizationConfig):
"""Config class for FP8.
No-arg ``Fp8Config()`` selects online (post-load) weight quantization:
``is_checkpoint_fp8_serialized=False`` with ``activation_scheme="dynamic"``.
"""
def __init__(
self,
is_checkpoint_fp8_serialized: bool = False,
activation_scheme: str = "dynamic",
ignored_layers: Optional[List[str]] = None,
weight_block_size: List[int] = None,
packed_modules_mapping: Optional[Dict[str, List[str]]] = None,
) -> None:
self.is_checkpoint_fp8_serialized = is_checkpoint_fp8_serialized
if is_checkpoint_fp8_serialized:
logger.info("Detected fp8 checkpoint.")
if activation_scheme not in ACTIVATION_SCHEMES:
raise ValueError(f"Unsupported activation scheme {activation_scheme}")
self.activation_scheme = activation_scheme
self.ignored_layers = ignored_layers or []
self.packed_modules_mapping = packed_modules_mapping or {}
if weight_block_size is not None:
if not is_checkpoint_fp8_serialized:
raise ValueError(
"The block-wise quantization only supports fp8-serialized checkpoint for now."
)
if len(weight_block_size) != 2:
raise ValueError(
f"The quantization block size of weight must have 2 dimensions, but got {len(weight_block_size)} dimensions."
)
if activation_scheme != "dynamic":
raise ValueError(
f"The block-wise quantization only supports dynamic activation scheme for now, but got {activation_scheme} activation scheme."
)
self.weight_block_size = weight_block_size
@classmethod
def get_name(cls) -> str:
return "fp8"
@classmethod
def get_supported_act_dtypes(cls) -> List[torch.dtype]:
return [torch.bfloat16, torch.half]
@classmethod
def get_min_capability(cls) -> int:
return 80
@classmethod
def get_config_filenames(cls) -> List[str]:
return []
@classmethod
def from_config(cls, config: Dict[str, Any]) -> Fp8Config:
quant_method = cls.get_from_keys(config, ["quant_method"])
is_checkpoint_fp8_serialized = "fp8" in quant_method
activation_scheme = cls.get_from_keys(config, ["activation_scheme"])
ignored_layers = cls.get_from_keys_or(
config, ["ignored_layers", "modules_to_not_convert"], None
)
if ignored_layers:
# hacking ministral
ignored_layers = [layer.replace("model.", "") for layer in ignored_layers]
weight_block_size = cls.get_from_keys_or(config, ["weight_block_size"], None)
return cls(
is_checkpoint_fp8_serialized=is_checkpoint_fp8_serialized,
activation_scheme=activation_scheme,
ignored_layers=ignored_layers,
weight_block_size=weight_block_size,
)
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> Optional[QuantizeMethodBase]:
from sglang.multimodal_gen.runtime.layers.linear import LinearBase
if isinstance(layer, LinearBase):
if is_layer_skipped(
prefix,
self.ignored_layers,
fused_mapping=self.packed_modules_mapping,
):
return UnquantizedLinearMethod()
return Fp8LinearMethod(self)
return None
def get_scaled_act_names(self) -> List[str]:
return []
class Fp8LinearMethod(LinearMethodBase):
"""Linear method for FP8.
Supports loading FP8 checkpoints with static weight scale and
dynamic/static activation scale.
Also supports loading quantized FP16/BF16 model checkpoints with dynamic
activation scaling. The weight scaling factor will be initialized after
the model weights are loaded.
Limitations:
1. Only support per-tensor quantization due to torch._scaled_mm support.
2. Only support float8_e4m3fn data type due to the limitation of
torch._scaled_mm (https://github.com/pytorch/pytorch/blob/2e48b39603411a41c5025efbe52f89560b827825/aten/src/ATen/native/cuda/Blas.cpp#L854-L856)
Args:
quant_config: The quantization config.
"""
def __init__(self, quant_config: Union[Fp8Config, W4AFp8Config]):
self.quant_config = quant_config
self.cutlass_fp8_supported = cutlass_fp8_supported()
# For GPUs that lack FP8 hardware support, we can leverage the Marlin
# kernel for fast weight-only FP8 quantization
self.use_marlin = False
if _is_cuda:
force_marlin = get_bool_env_var("SGLANG_FORCE_FP8_MARLIN")
auto_enable = can_auto_enable_marlin_fp8()
self.use_marlin = force_marlin or auto_enable
self.block_quant = self.quant_config.weight_block_size is not None
self.w8a8_block_fp8_linear = dispatch_w8a8_block_fp8_linear()
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
output_size_per_partition = sum(output_partition_sizes)
weight_loader = extra_weight_attrs.get("weight_loader")
tp_size = get_tensor_model_parallel_world_size()
if self.block_quant:
block_n, block_k = (
self.quant_config.weight_block_size[0],
self.quant_config.weight_block_size[1],
)
# Required by row parallel
if tp_size > 1 and input_size // input_size_per_partition == tp_size:
if input_size_per_partition % block_k != 0:
raise ValueError(
f"Weight input_size_per_partition = "
f"{input_size_per_partition} is not divisible by "
f"weight quantization block_k = {block_k}."
)
# Required by column parallel or enabling merged weights
if (
tp_size > 1 and output_size // output_size_per_partition == tp_size
) or len(output_partition_sizes) > 1:
for output_partition_size in output_partition_sizes:
if output_partition_size % block_n != 0:
raise ValueError(
f"Weight output_partition_size = "
f"{output_partition_size} is not divisible by "
f"weight quantization block_n = {block_n}."
)
layer.logical_widths = output_partition_sizes
layer.input_size_per_partition = input_size_per_partition
layer.output_size_per_partition = output_size_per_partition
layer.orig_dtype = params_dtype
# WEIGHT
weight_dtype = (
torch.float8_e4m3fn
if self.quant_config.is_checkpoint_fp8_serialized
else params_dtype
)
weight = ModelWeightParameter(
data=torch.empty(
output_size_per_partition, input_size_per_partition, dtype=weight_dtype
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
layer.register_parameter("weight", weight)
# If checkpoint is serialized fp8, load them.
# Otherwise, wait until process_weights_after_loading.
if self.quant_config.is_checkpoint_fp8_serialized:
# WEIGHT SCALE
if self.block_quant:
if hasattr(self.quant_config, "activation_scheme"):
assert self.quant_config.activation_scheme == "dynamic"
elif hasattr(self.quant_config, "linear_activation_scheme"):
assert self.quant_config.linear_activation_scheme == "dynamic"
scale = BlockQuantScaleParameter(
data=torch.empty(
(output_size_per_partition + block_n - 1) // block_n,
(input_size_per_partition + block_k - 1) // block_k,
dtype=torch.float32,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
scale.format_ue8m0 = False
scale[:] = torch.finfo(torch.float32).min
layer.register_parameter("weight_scale_inv", scale)
else:
scale = PerTensorScaleParameter(
data=torch.empty(len(output_partition_sizes), dtype=torch.float32),
weight_loader=weight_loader,
)
scale[:] = torch.finfo(torch.float32).min
layer.register_parameter("weight_scale", scale)
# INPUT ACTIVATION SCALE
if (
hasattr(self.quant_config, "activation_scheme")
and self.quant_config.activation_scheme == "static"
) or (
hasattr(self.quant_config, "linear_activation_scheme")
and self.quant_config.linear_activation_scheme == "static"
):
scale = PerTensorScaleParameter(
data=torch.empty(len(output_partition_sizes), dtype=torch.float32),
weight_loader=weight_loader,
)
scale[:] = torch.finfo(torch.float32).min
layer.register_parameter("input_scale", scale)
else:
layer.register_parameter("input_scale", None)
def process_weights_after_loading(self, layer: Module) -> None:
if self.block_quant:
# If ROCm, normalize the weights and scales to e4m3fnuz
if _is_fp8_fnuz:
# activation_scheme: dynamic
weight, weight_scale, _ = normalize_e4m3fn_to_e4m3fnuz(
weight=layer.weight,
weight_scale=layer.weight_scale_inv,
input_scale=None,
)
layer.input_scale = None
elif _is_cpu:
assert (
_is_cpu_amx_available
), "Fp8LinearMethod on CPU requires that CPU has AMX support"
_amx_process_weight_after_loading(layer, ["weight"])
layer.weight_scale_inv = torch.nn.Parameter(
layer.weight_scale_inv.data, requires_grad=False
)
return
else:
# For fp8 linear weights run with deepgemm, the weights and scales need be requantized to ue8m0
from sglang.srt.layers.quantization.fp8_utils import (
deepgemm_w8a8_block_fp8_linear_with_fallback,
)
from sglang.srt.model_loader.utils import (
should_deepgemm_weight_requant_ue8m0,
)
if (
should_deepgemm_weight_requant_ue8m0(
weight_block_size=getattr(
self.quant_config, "weight_block_size", None
),
)
and (
self.w8a8_block_fp8_linear
is deepgemm_w8a8_block_fp8_linear_with_fallback
)
and (not layer.weight_scale_inv.format_ue8m0)
):
requant_weight_ue8m0_inplace(
layer.weight,
layer.weight_scale_inv,
self.quant_config.weight_block_size,
)
layer.weight_scale_inv.format_ue8m0 = True
weight, weight_scale = layer.weight.data, layer.weight_scale_inv.data
layer.weight.data = weight.data
layer.weight_scale_inv.data = weight_scale.data
else:
layer.weight = Parameter(layer.weight.data, requires_grad=False)
# If checkpoint not serialized fp8, quantize the weights.
if not self.quant_config.is_checkpoint_fp8_serialized:
if self.cutlass_fp8_supported or self.use_marlin:
# apply per-channel quantization default as
# cutlass sgl-kernel and marlin only support per-channel scale
qweight, weight_scale = per_token_group_quant_fp8(
layer.weight, layer.weight.shape[-1]
)
weight_scale = weight_scale.t().contiguous()
else:
# per-tensor quantization
qweight, weight_scale = input_to_float8(layer.weight)
# Update the layer with the new values.
layer.weight = Parameter(qweight.t(), requires_grad=False)
layer.weight_scale = Parameter(weight_scale, requires_grad=False)
layer.input_scale = None
# If checkpoint is fp8, handle that there are N scales for N
# shards in a fused module
else:
layer.weight_scale = Parameter(
layer.weight_scale.data, requires_grad=False
)
if (
hasattr(self.quant_config, "activation_scheme")
and self.quant_config.activation_scheme == "static"
) or (
hasattr(self.quant_config, "linear_activation_scheme")
and self.quant_config.linear_activation_scheme == "static"
):
layer.input_scale = Parameter(
layer.input_scale.data, requires_grad=False
)
# cutlass sgl-kernel and marlin only support per-channel scale
if self.cutlass_fp8_supported or self.use_marlin:
weight = layer.weight
weight_scale = convert_to_channelwise(
layer.weight_scale, layer.logical_widths
)
else:
# Dequant -> Quant with max scale so we can run per tensor.
weight = layer.weight
weight_scale = layer.weight_scale
# If ROCm, normalize the weights and scales to e4m3fnuz
if _is_fp8_fnuz:
weight, weight_scale, input_scale = (
normalize_e4m3fn_to_e4m3fnuz(
weight=weight,
weight_scale=weight_scale,
input_scale=layer.input_scale,
)
)
if input_scale is not None:
layer.input_scale = Parameter(
input_scale, requires_grad=False
)
weight_scale, weight = requantize_with_max_scale(
weight=weight,
weight_scale=weight_scale,
logical_widths=layer.logical_widths,
)
# Update layer with new values.
layer.weight = Parameter(weight.t(), requires_grad=False)
layer.weight_scale = Parameter(weight_scale, requires_grad=False)
if (
hasattr(self.quant_config, "activation_scheme")
and self.quant_config.activation_scheme == "static"
) or (
hasattr(self.quant_config, "linear_activation_scheme")
and self.quant_config.linear_activation_scheme == "static"
):
layer.input_scale = Parameter(
layer.input_scale.max(), requires_grad=False
)
if self.use_marlin:
if self.block_quant:
layer.weight_block_size = self.quant_config.weight_block_size
prepare_fp8_layer_for_marlin(layer, not self.block_quant)
# Activations not quantized for marlin.
del layer.input_scale
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
if self.use_marlin:
return apply_fp8_marlin_linear(
input=x,
weight=layer.weight,
weight_scale=layer.weight_scale,
workspace=layer.workspace,
size_n=layer.output_size_per_partition,
size_k=layer.input_size_per_partition,
bias=bias,
)
if self.block_quant:
if use_intel_amx_backend(layer):
return torch.ops.sgl_kernel.fp8_scaled_mm_cpu(
x,
layer.weight,
layer.weight_scale_inv,
self.quant_config.weight_block_size,
bias,
x.dtype,
True, # is_vnni
)
if isinstance(x, tuple):
return self.w8a8_block_fp8_linear(
input=x[0],
weight=layer.weight,
block_size=self.quant_config.weight_block_size,
weight_scale=layer.weight_scale_inv,
input_scale=x[1],
bias=bias,
)
return self.w8a8_block_fp8_linear(
input=x,
weight=layer.weight,
block_size=self.quant_config.weight_block_size,
weight_scale=layer.weight_scale_inv,
input_scale=None,
bias=bias,
)
return apply_fp8_linear(
input=x,
weight=layer.weight,
weight_scale=layer.weight_scale,
input_scale=layer.input_scale,
bias=bias,
cutlass_fp8_supported=self.cutlass_fp8_supported,
use_per_token_if_dynamic=False,
)
@@ -0,0 +1,210 @@
"""ModelOpt FP8 quantization support for diffusion models.
Handles checkpoints produced by NVIDIA Model Optimizer (ModelOpt) with
``quant_algo: "FP8"`` and ``quant_method: "modelopt"``.
Per quantized linear layer the checkpoint contains:
.weight float8_e4m3fn [out, in] FP8 quantized weight
.weight_scale float32 scalar per-tensor weight scale
.input_scale float32 scalar per-tensor static activation scale
.bias bfloat16 [out] bias (unquantized)
._amax (ignored) calibration artifact
Layers listed in the ``ignore`` field of the quantization config remain in
bfloat16 and use the standard unquantized linear method.
"""
from __future__ import annotations
import fnmatch
import logging
from typing import Any, Dict, List, Optional
import torch
from sglang.multimodal_gen.runtime.layers.linear import (
LinearMethodBase,
UnquantizedLinearMethod,
)
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
QuantizeMethodBase,
)
from sglang.multimodal_gen.runtime.models.parameter import (
ModelWeightParameter,
PerTensorScaleParameter,
)
from sglang.srt.layers.quantization.fp8_utils import (
apply_fp8_linear,
cutlass_fp8_supported,
)
from sglang.srt.layers.quantization.utils import convert_to_channelwise
logger = logging.getLogger(__name__)
class ModelOptFp8Config(QuantizationConfig):
"""Config for ModelOpt static per-tensor FP8 quantization."""
def __init__(
self,
is_checkpoint_fp8_serialized: bool = True,
ignore: Optional[List[str]] = None,
) -> None:
super().__init__()
self.is_checkpoint_fp8_serialized = is_checkpoint_fp8_serialized
self.ignore = ignore or []
# -- QuantizationConfig interface ----------------------------------------
@classmethod
def get_name(cls) -> str:
return "modelopt"
@classmethod
def get_supported_act_dtypes(cls) -> list[torch.dtype]:
return [torch.bfloat16, torch.half]
@classmethod
def get_min_capability(cls) -> int:
return 89
@staticmethod
def get_config_filenames() -> list[str]:
return []
@classmethod
def from_config(
cls,
config: Dict[str, Any],
ignore_remap: Optional[Dict[str, str]] = None,
) -> ModelOptFp8Config:
quant_algo = config.get("quant_algo")
if quant_algo is None:
raise ValueError(
"ModelOptFp8Config requires 'quant_algo' in the quantization config."
)
if "FP8" not in quant_algo:
raise ValueError(
f"ModelOptFp8Config only supports FP8, got quant_algo={quant_algo!r}."
)
ignore = config.get("ignore", [])
if ignore_remap and ignore:
ignore = [ignore_remap.get(pattern, pattern) for pattern in ignore]
return cls(is_checkpoint_fp8_serialized=True, ignore=ignore)
def _is_layer_ignored(self, prefix: str) -> bool:
"""Check whether *prefix* matches any pattern in the ignore list.
ModelOpt ignore patterns are matched against the full prefix as a glob
(e.g. ``"norm_out*"`` matches ``"norm_out.linear"``) **and** against the
first path component (e.g. ``"proj_out"`` matches only the top-level
``proj_out``, not ``single_transformer_blocks.0.proj_out``).
"""
first_component = prefix.split(".")[0]
for pattern in self.ignore:
if fnmatch.fnmatch(prefix, pattern):
return True
if fnmatch.fnmatch(first_component, pattern):
return True
return False
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> Optional[QuantizeMethodBase]:
from sglang.multimodal_gen.runtime.layers.linear import LinearBase
if isinstance(layer, LinearBase):
if self._is_layer_ignored(prefix):
return UnquantizedLinearMethod()
return ModelOptFp8LinearMethod(self)
return None
def get_scaled_act_names(self) -> list[str]:
return []
class ModelOptFp8LinearMethod(LinearMethodBase):
"""Linear method for ModelOpt static per-tensor FP8 quantization.
Uses ``torch._scaled_mm`` (or CUTLASS FP8 GEMM when available) for
the FP8 matrix multiply - the same kernels used by the LLM runtime.
"""
def __init__(self, quant_config: ModelOptFp8Config):
self.quant_config = quant_config
self.cutlass_fp8_supported = cutlass_fp8_supported()
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
) -> None:
output_size_per_partition = sum(output_partition_sizes)
weight_loader = extra_weight_attrs.get("weight_loader")
layer.logical_widths = output_partition_sizes
layer.input_size_per_partition = input_size_per_partition
layer.output_size_per_partition = output_size_per_partition
weight = ModelWeightParameter(
data=torch.empty(
output_size_per_partition,
input_size_per_partition,
dtype=torch.float8_e4m3fn,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
layer.register_parameter("weight", weight)
for scale_name in ("weight_scale", "input_scale"):
scale = PerTensorScaleParameter(
data=torch.full(
(len(output_partition_sizes),),
torch.finfo(torch.float32).min,
dtype=torch.float32,
),
weight_loader=weight_loader,
)
layer.register_parameter(scale_name, scale)
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
# Diffusion models use single-partition layers (no TP, no fused QKV),
# so we just take the max scale directly without the
# dequantize-requantize round-trip that the LLM path does (which
# requires CUDA kernels that are unavailable during CPU-phase loading).
max_w_scale = layer.weight_scale.max()
# Transpose weight to [in, out] column-major layout for
# apply_fp8_linear / CUTLASS fp8_scaled_mm. Do not call .contiguous();
# the kernel requires column-major stride.
layer.weight = torch.nn.Parameter(layer.weight.data.t(), requires_grad=False)
if self.cutlass_fp8_supported:
max_w_scale = convert_to_channelwise(max_w_scale, layer.logical_widths)
layer.weight_scale = torch.nn.Parameter(max_w_scale, requires_grad=False)
layer.input_scale = torch.nn.Parameter(
layer.input_scale.max(), requires_grad=False
)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
return apply_fp8_linear(
input=x,
weight=layer.weight,
weight_scale=layer.weight_scale,
input_scale=layer.input_scale,
bias=bias,
cutlass_fp8_supported=self.cutlass_fp8_supported,
)
@@ -0,0 +1,683 @@
# Adapted from https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/layers/quantization/modelopt_quant.py
from __future__ import annotations
import logging
import re
from functools import lru_cache
from typing import Any, Dict, List, Optional
import torch
from sglang.multimodal_gen.runtime.layers.linear import (
LinearMethodBase,
UnquantizedLinearMethod,
)
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
QuantizeMethodBase,
)
from sglang.multimodal_gen.runtime.models.parameter import (
ModelWeightParameter,
PerTensorScaleParameter,
)
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.weight_attrs import set_weight_attrs
from sglang.srt.layers.quantization.fp8_utils import (
apply_fp8_linear,
cutlass_fp8_supported,
)
from sglang.srt.layers.quantization.modelopt_quant import (
pad_nvfp4_activation_for_cutlass,
pad_nvfp4_weight,
slice_nvfp4_output,
)
from sglang.srt.layers.quantization.utils import (
convert_to_channelwise,
is_layer_skipped,
requantize_with_max_scale,
)
from sglang.srt.layers.utils.common import copy_or_rebind_param
from sglang.srt.utils.common import is_flashinfer_available, round_up
logger = logging.getLogger(__name__)
if is_flashinfer_available():
import flashinfer
else:
flashinfer = None
@lru_cache(maxsize=1)
def _get_fp4_quantize_op():
return current_platform.get_modelopt_fp4_quantize_op()
@lru_cache(maxsize=1)
def _get_fp4_gemm_op():
return current_platform.get_modelopt_fp4_gemm_op()
def _prepare_nvfp4_weight_bytes(
weight: torch.Tensor, *, swap_weight_nibbles: bool
) -> torch.Tensor:
"""Normalize serialized NVFP4 bytes before padding for the runtime kernel."""
if not swap_weight_nibbles:
return weight.contiguous()
return ((weight >> 4) | (weight << 4)).contiguous()
def _swizzled_nvfp4_scales_to_linear(scales: torch.Tensor) -> torch.Tensor:
"""Convert FlashInfer/CUTLASS-swizzled FP4 scales back to row-major layout."""
scale_ndim = scales.ndim
if scale_ndim == 2:
scales = scales.unsqueeze(0)
assert scales.ndim == 3
B, M, K = scales.shape
M_padded = round_up(M, 128)
K_padded = round_up(K, 4)
if M != M_padded or K != K_padded:
padded = torch.zeros(
(B, M_padded, K_padded), dtype=scales.dtype, device=scales.device
)
padded[:B, :M, :K] = scales
scales = padded
linear = scales.reshape(B, M_padded // 128, K_padded // 4, 32, 4, 4)
linear = linear.permute(0, 1, 4, 3, 2, 5).contiguous()
linear = linear.reshape(B, M_padded, K_padded)[:, :M, :K]
return linear.squeeze(0) if scale_ndim == 2 else linear
def _require_flashinfer():
if flashinfer is None:
raise RuntimeError(
"flashinfer is required for the diffusion NVFP4 FlashInfer path."
)
return flashinfer
class ModelOptQuantConfig(QuantizationConfig):
def __init__(
self,
exclude_modules: Optional[List[str]],
packed_modules_mapping: Optional[Dict[str, List[str]]],
):
super().__init__()
self.packed_modules_mapping = packed_modules_mapping or {}
self.exclude_modules = exclude_modules or []
def _get_quant_method(
self,
layer: torch.nn.Module,
prefix: str,
*,
Linear: type[LinearMethodBase],
) -> Optional[QuantizeMethodBase]:
from sglang.multimodal_gen.runtime.layers.linear import LinearBase
if isinstance(layer, LinearBase):
if self.is_layer_excluded(prefix) or (
self.packed_modules_mapping
and is_layer_skipped(prefix, [], self.packed_modules_mapping)
):
return UnquantizedLinearMethod()
return Linear(self)
return None
@classmethod
def get_config_filenames(cls) -> List[str]:
return ["hf_quant_config.json"]
def get_scaled_act_names(self) -> List[str]:
return []
@classmethod
def override_quantization_method(cls, hf_quant_config, user_quant) -> Optional[str]:
if hf_quant_config is None:
return None
quant_algo = (
hf_quant_config.get("quant_algo")
or hf_quant_config.get("quantization", {}).get("quant_algo")
or ""
).upper()
if user_quant in {"modelopt", "modelopt_fp8"} and "FP8" in quant_algo:
return "modelopt_fp8"
if user_quant in {"modelopt", "modelopt_fp4"} and (
"NVFP4" in quant_algo or "FP4" in quant_algo
):
return "modelopt_fp4"
return None
def is_layer_excluded(self, prefix: str) -> bool:
for pattern in self.exclude_modules:
regex_str = re.escape(pattern).replace(r"\*", r".*")
if re.fullmatch(regex_str, prefix):
return True
return False
class ModelOptFp8Config(ModelOptQuantConfig):
"""Config class for ModelOpt FP8 diffusion checkpoints."""
def __init__(
self,
is_checkpoint_fp8_serialized: bool = False,
exclude_modules: Optional[List[str]] = None,
packed_modules_mapping: Optional[Dict[str, List[str]]] = None,
) -> None:
super().__init__(exclude_modules, packed_modules_mapping)
self.is_checkpoint_fp8_serialized = is_checkpoint_fp8_serialized
if is_checkpoint_fp8_serialized:
logger.warning(
"Detected ModelOpt FP8 checkpoint. The format is experimental and subject to change."
)
@classmethod
def get_name(cls) -> str:
return "modelopt_fp8"
@classmethod
def get_supported_act_dtypes(cls) -> List[torch.dtype]:
return [torch.bfloat16, torch.half]
@classmethod
def get_min_capability(cls) -> int:
return 89
@classmethod
def from_config(
cls,
config: Dict[str, Any],
ignore_remap: Optional[Dict[str, str]] = None,
) -> ModelOptFp8Config:
quant_method = config.get("quant_algo")
exclude_modules = config.get("ignore")
if quant_method is None:
try:
quantization_section = cls.get_from_keys(config, ["quantization"])
quant_method = quantization_section.get("quant_algo")
exclude_modules = quantization_section.get("exclude_modules")
except ValueError as exc:
raise ValueError(
"Cannot find 'quant_algo' in the model's quantization config."
) from exc
if quant_method is None or "FP8" not in quant_method:
raise ValueError(
"ModelOptFp8Config only supports static FP8 quantization in SGLang diffusion."
)
if ignore_remap and exclude_modules:
exclude_modules = [ignore_remap.get(p, p) for p in exclude_modules]
return cls(
is_checkpoint_fp8_serialized=True,
exclude_modules=exclude_modules,
packed_modules_mapping=config.get("packed_modules_mapping"),
)
def get_quant_method(self, layer: torch.nn.Module, prefix: str):
return self._get_quant_method(layer, prefix, Linear=ModelOptFp8LinearMethod)
class ModelOptFp4Config(ModelOptQuantConfig):
"""Config class for NVFP4."""
def __init__(
self,
is_checkpoint_nvfp4_serialized: bool = False,
group_size: int = None,
exclude_modules: List[str] = None,
packed_modules_mapping: Optional[Dict[str, List[str]]] = None,
checkpoint_uses_packed_qkv: bool = False,
swap_weight_nibbles: bool = False,
checkpoint_weight_scale_layout: str = "linear",
) -> None:
super().__init__(exclude_modules, packed_modules_mapping)
self.is_checkpoint_nvfp4_serialized = is_checkpoint_nvfp4_serialized
if is_checkpoint_nvfp4_serialized:
logger.warning(
"Detected nvfp4 checkpoint. Please note that the "
"format is experimental and subject to change."
)
self.group_size = group_size
self.checkpoint_uses_packed_qkv = checkpoint_uses_packed_qkv
self.swap_weight_nibbles = swap_weight_nibbles
self.checkpoint_weight_scale_layout = checkpoint_weight_scale_layout
@classmethod
def get_name(cls) -> str:
return "modelopt_fp4"
@classmethod
def get_supported_act_dtypes(cls) -> List[torch.dtype]:
return [torch.bfloat16, torch.half, torch.float8_e4m3fn]
@classmethod
def get_min_capability(cls) -> int:
return 100
@staticmethod
def common_group_size(cfg: dict) -> int:
"""Return the unique group_size across the config; raise if missing/mismatched."""
sizes = set()
def _add_group_size_from_dict(config: dict):
group_size = config.get("group_size")
if isinstance(group_size, int):
sizes.add(group_size)
# Top-level and 'quantization' block
_add_group_size_from_dict(cfg)
quantization = cfg.get("quantization")
if isinstance(quantization, dict):
_add_group_size_from_dict(quantization)
# config_groups: accept group-level or nested dicts (e.g., weights/input_activations)
for config_groups in (cfg.get("config_groups") or {}).values():
if isinstance(config_groups, dict):
_add_group_size_from_dict(config_groups)
for config_group in config_groups.values():
if isinstance(config_group, dict):
_add_group_size_from_dict(config_group)
if not sizes:
raise ValueError("No group_size found in config.")
if len(sizes) > 1:
raise ValueError(f"Inconsistent group_size values: {sorted(sizes)}")
return next(iter(sizes))
@classmethod
def from_config(cls, config: Dict[str, Any]) -> ModelOptFp4Config:
group_size = None
exclude_modules = []
swap_weight_nibbles = False
# Flat format (config.json quantization_config)
quant_method = config.get("quant_algo")
if quant_method is not None:
group_size = config.get("group_size")
if group_size is None:
config_groups = config.get("config_groups", {})
if config_groups:
first_group = next(iter(config_groups.values()), {})
group_size = first_group.get("weights", {}).get("group_size")
exclude_modules = config.get("ignore", [])
swap_weight_nibbles = config.get(
"swap_weight_nibbles",
config.get("checkpoint_uses_packed_qkv", False),
)
else:
# Nested format (hf_quant_config.json)
try:
quant_config = cls.get_from_keys(config, ["quantization"])
quant_method = quant_config["quant_algo"]
group_size = ModelOptFp4Config.common_group_size(config)
exclude_modules = quant_config.get("exclude_modules", [])
swap_weight_nibbles = quant_config.get(
"swap_weight_nibbles",
config.get(
"swap_weight_nibbles",
config.get("checkpoint_uses_packed_qkv", False),
),
)
except (ValueError, KeyError):
raise ValueError("Cannot find 'quant_algo' in quantization config.")
if quant_method not in ["NVFP4"]:
raise ValueError(
f"Only NVFP4 quantization is supported for diffusion, got '{quant_method}'."
)
if group_size is None or exclude_modules is None:
raise ValueError(
"NVFP4 quantization requires group_size and exclude_modules "
"in the quantization config"
)
return cls(
is_checkpoint_nvfp4_serialized=True,
group_size=group_size,
exclude_modules=exclude_modules,
packed_modules_mapping=config.get("packed_modules_mapping"),
checkpoint_uses_packed_qkv=config.get("checkpoint_uses_packed_qkv", False),
swap_weight_nibbles=swap_weight_nibbles,
checkpoint_weight_scale_layout=config.get(
"checkpoint_weight_scale_layout", "linear"
),
)
def get_quant_method(self, layer: torch.nn.Module, prefix: str):
return self._get_quant_method(layer, prefix, Linear=ModelOptFp4LinearMethod)
class ModelOptFp8LinearMethod(LinearMethodBase):
"""Linear method for ModelOpt static FP8 checkpoints."""
def __init__(self, quant_config: ModelOptFp8Config):
self.quant_config = quant_config
self.cutlass_fp8_supported = cutlass_fp8_supported()
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
del input_size, output_size
output_size_per_partition = sum(output_partition_sizes)
weight_loader = extra_weight_attrs.get("weight_loader")
layer.logical_widths = output_partition_sizes
layer.input_size_per_partition = input_size_per_partition
layer.output_size_per_partition = output_size_per_partition
weight_dtype = (
torch.float8_e4m3fn
if self.quant_config.is_checkpoint_fp8_serialized
else params_dtype
)
layer.register_parameter(
"weight",
ModelWeightParameter(
data=torch.empty(
output_size_per_partition,
input_size_per_partition,
dtype=weight_dtype,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
),
)
if self.quant_config.is_checkpoint_fp8_serialized:
for scale_name in ["weight_scale", "input_scale"]:
layer.register_parameter(
scale_name,
PerTensorScaleParameter(
data=torch.full(
(len(output_partition_sizes),),
torch.finfo(torch.float32).min,
dtype=torch.float32,
),
weight_loader=weight_loader,
),
)
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
max_w_scale, quantized_weight = requantize_with_max_scale(
layer.weight, layer.weight_scale, layer.logical_widths
)
# Preserve the parameter subclass metadata while rebinding to the
# transposed FP8 view expected by the runtime.
layer.weight.data = quantized_weight.t().detach()
layer.weight.requires_grad_(False)
if self.cutlass_fp8_supported:
max_w_scale = convert_to_channelwise(max_w_scale, layer.logical_widths)
copy_or_rebind_param(layer, "weight_scale", max_w_scale)
copy_or_rebind_param(layer, "input_scale", layer.input_scale.max())
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
return apply_fp8_linear(
input=x,
weight=layer.weight,
weight_scale=layer.weight_scale,
input_scale=layer.input_scale,
bias=bias,
cutlass_fp8_supported=self.cutlass_fp8_supported,
)
class ModelOptFp4LinearMethod(LinearMethodBase):
"""NVFP4 linear method using the selected FP4 GEMM backend."""
def __init__(self, quant_config: ModelOptFp4Config):
self.quant_config = quant_config
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
del input_size, output_size
if not self.quant_config.is_checkpoint_nvfp4_serialized:
raise ValueError(
"NVFP4 quantization was selected, "
" dynamic quantization is not supported."
)
if input_size_per_partition % 16 != 0:
raise ValueError(
f"Unsupported model when input features size is {input_size_per_partition}, not multiple of 16, for NVFP4 quantization."
)
output_size_per_partition = sum(output_partition_sizes)
weight_loader = extra_weight_attrs.get("weight_loader")
layer.logical_widths = output_partition_sizes
layer.input_size_per_partition = input_size_per_partition
layer.output_size_per_partition = output_size_per_partition
weight_dtype = (
torch.float8_e4m3fn
if self.quant_config.is_checkpoint_nvfp4_serialized
else params_dtype
)
weight = ModelWeightParameter(
data=torch.empty(
output_size_per_partition,
input_size_per_partition // 2,
dtype=torch.uint8,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
layer.register_parameter("weight", weight)
input_scale = PerTensorScaleParameter(
data=torch.empty(len(output_partition_sizes), dtype=torch.float32),
weight_loader=weight_loader,
)
set_weight_attrs(input_scale, {"missing_param_init": "ones"})
layer.register_parameter("input_scale", input_scale)
weight_scale_2 = PerTensorScaleParameter(
data=torch.empty(len(output_partition_sizes), dtype=torch.float32),
weight_loader=weight_loader,
)
set_weight_attrs(weight_scale_2, {"missing_param_init": "ones"})
layer.register_parameter("weight_scale_2", weight_scale_2)
weight_scale = ModelWeightParameter(
data=torch.empty(
output_size_per_partition,
input_size_per_partition // self.quant_config.group_size,
dtype=weight_dtype,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
set_weight_attrs(weight_scale, {"missing_param_init": "ones"})
layer.register_parameter("weight_scale", weight_scale)
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
input_scale_2 = layer.input_scale.max().to(torch.float32)
weight_scale_2 = layer.weight_scale_2.max().to(torch.float32)
copy_or_rebind_param(
layer, "alpha", (input_scale_2 * weight_scale_2).to(torch.float32)
)
copy_or_rebind_param(
layer, "input_scale_inv", (1 / input_scale_2).to(torch.float32)
)
layer.output_size_per_partition = layer.weight.shape[0]
w = layer.weight.data
w_swapped = _prepare_nvfp4_weight_bytes(
w,
swap_weight_nibbles=getattr(
self.quant_config, "swap_weight_nibbles", False
),
)
scales = layer.weight_scale
if (
getattr(self.quant_config, "checkpoint_weight_scale_layout", "linear")
== "swizzled"
):
scales = _swizzled_nvfp4_scales_to_linear(scales)
_, flashinfer_backend = _get_fp4_gemm_op()
if flashinfer_backend == "trtllm":
flashinfer_ops = _require_flashinfer()
weight, _ = pad_nvfp4_weight(w_swapped, n_alignment=128, k_alignment=0)
if scales.shape[0] != weight.shape[0]:
pad_n = weight.shape[0] - scales.shape[0]
scales = torch.nn.functional.pad(scales, (0, 0, 0, pad_n))
scale_k = scales.shape[1]
weights_padding_cols = 0
if scale_k % 4 != 0:
padded_scale_k = round_up(scale_k, 4)
pad_scale_k = padded_scale_k - scale_k
scales = torch.nn.functional.pad(scales, (0, pad_scale_k, 0, 0))
pad_weight_k = pad_scale_k * 8
weight = torch.nn.functional.pad(weight, (0, pad_weight_k, 0, 0))
weights_padding_cols = pad_weight_k
epilogue_tile_m = 128
shuffled_scale_shape = scales.shape
if not weight.is_cuda:
weight = weight.cuda()
if scales.device != weight.device:
scales = scales.to(device=weight.device)
weight = flashinfer_ops.shuffle_matrix_a(
weight.view(torch.uint8), epilogue_tile_m
)
scales = (
flashinfer_ops.shuffle_matrix_sf_a(
scales.view(torch.uint8), epilogue_tile_m
)
.reshape(shuffled_scale_shape)
.view(torch.float8_e4m3fn)
)
layer.weights_padding_cols = weights_padding_cols
copy_or_rebind_param(layer, "weight", weight)
copy_or_rebind_param(layer, "weight_scale_interleaved", scales)
return
weight, weights_padding_cols = pad_nvfp4_weight(w_swapped)
layer.weights_padding_cols = weights_padding_cols
copy_or_rebind_param(layer, "weight", weight)
scale_ndim = scales.ndim
if scale_ndim == 2:
scales = scales.unsqueeze(0)
assert scales.ndim == 3
B, M, K = scales.shape
M_padded = round_up(M, 128)
K_padded = round_up(K, 4)
padded_scales = torch.zeros((B, M_padded, K_padded), dtype=scales.dtype)
padded_scales[:B, :M, :K] = scales
_, flashinfer_backend = _get_fp4_gemm_op()
uses_flux1_scale_layout = not getattr(
self.quant_config, "checkpoint_uses_packed_qkv", False
) and getattr(layer, "prefix", "").startswith(
("transformer_blocks.", "single_transformer_blocks.")
)
if flashinfer_backend is None or uses_flux1_scale_layout:
# CUTLASS and FLUX.1 CUDNN paths need the TMA scale layout.
padded_scales = padded_scales.reshape(
B, M_padded // 128, 4, 32, K_padded // 4, 4
)
padded_scales = padded_scales.permute(0, 1, 4, 3, 2, 5)
padded_scales = padded_scales.contiguous().cuda()
padded_scales = (
padded_scales.reshape(M_padded, K_padded)
if scale_ndim == 2
else padded_scales.reshape(B, M_padded, K_padded)
)
copy_or_rebind_param(layer, "weight_scale_interleaved", padded_scales)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
output_dtype = x.dtype
input_shape = x.shape
x = x.view(-1, input_shape[-1])
output_size = layer.output_size_per_partition
output_shape = list(input_shape[:-1]) + [output_size]
fp4_quantize = _get_fp4_quantize_op()
if fp4_quantize is None:
raise RuntimeError(
"No FP4 quantization kernel available. Install flashinfer or sgl_kernel."
)
x_fp4, x_scale_interleaved = fp4_quantize(x, layer.input_scale_inv)
weights_padding_cols = getattr(layer, "weights_padding_cols", 0)
x_fp4 = pad_nvfp4_activation_for_cutlass(x_fp4, weights_padding_cols)
w = layer.weight
w_scale_interleaved = layer.weight_scale_interleaved
if x_scale_interleaved.dtype == torch.uint8:
x_scale_interleaved = x_scale_interleaved.view(torch.float8_e4m3fn)
if w_scale_interleaved.dtype == torch.uint8:
w_scale_interleaved = w_scale_interleaved.view(torch.float8_e4m3fn)
fp4_gemm, flashinfer_backend = _get_fp4_gemm_op()
if flashinfer_backend is not None:
out = fp4_gemm(
x_fp4,
w.T,
x_scale_interleaved,
w_scale_interleaved.T,
layer.alpha,
output_dtype,
backend=flashinfer_backend,
)
elif fp4_gemm is not None:
out = fp4_gemm(
x_fp4,
w,
x_scale_interleaved,
w_scale_interleaved,
layer.alpha,
output_dtype,
)
else:
raise RuntimeError(
"No FP4 GEMM kernel available. Install flashinfer or sgl_kernel."
)
out = slice_nvfp4_output(out, output_size)
if bias is not None:
out = out + bias
return out.view(*output_shape)
@@ -0,0 +1,253 @@
from __future__ import annotations
import logging
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, cast
import torch
from sglang.multimodal_gen.runtime.layers.linear import (
LinearMethodBase,
UnquantizedLinearMethod,
)
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
QuantizeMethodBase,
)
from sglang.srt.layers.quantization.compressed_tensors.utils import should_ignore_layer
from sglang.srt.layers.quantization.modelslim.schemes import (
ModelSlimW4A4Int4,
ModelSlimW8A8Int8,
)
if TYPE_CHECKING:
from sglang.srt.layers.quantization.modelslim.schemes import (
ModelSlimLinearScheme,
)
from sglang.multimodal_gen.runtime.loader.utils import get_param_names_mapping
logger = logging.getLogger(__name__)
class ModelSlimConfig(QuantizationConfig):
"""
Config class for ModelSlim Quantization of Diffusion models https://gitcode.com/Ascend/msmodelslim, a NPU-specific quantization type.
The quantization method (W8A8, W4A4, etc.) will be automatically parsed from the `quant_model_description.json` config.
ModelSlim for Diffusion models includes support for various quantization schemes, such as:
- W4A4 dynamic linear
- W8A8 static linear
- W8A8 dynamic linear
"""
def __init__(
self,
quant_config: Dict[str, Any] = {},
reverse_param_names_mapping: dict = None,
):
super().__init__()
self.quant_description = quant_config
ignore = cast(List[str], quant_config.get("ignore", []))
self.ignore = ignore
packed_modules_mapping = quant_config.get("packed_modules_mapping", {})
self.packed_modules_mapping = (
packed_modules_mapping if packed_modules_mapping is not None else {}
)
self._name_mapper = (
get_param_names_mapping(reverse_param_names_mapping)
if reverse_param_names_mapping is not None
else None
)
def get_linear_method(self) -> ModelSlimLinearMethod:
return ModelSlimLinearMethod(self)
@classmethod
def get_supported_act_dtypes(cls) -> List[torch.dtype]:
return [torch.int8, torch.float16, torch.bfloat16]
@classmethod
def get_min_capability(cls) -> int:
return 0
@classmethod
def get_name(cls) -> str:
return "modelslim"
@classmethod
def get_config_filenames(cls) -> List[str]:
filenames = ["quant_model_description.json"]
return filenames
@classmethod
def from_config(
cls, config: Dict[str, Any], reverse_param_names_mapping: dict = None
) -> ModelSlimConfig:
return cls(config, reverse_param_names_mapping)
def get_quant_method(
self,
layer: torch.nn.Module,
prefix: str,
) -> Optional[QuantizeMethodBase]:
from sglang.multimodal_gen.runtime.layers.linear import LinearBase
if isinstance(layer, LinearBase):
if should_ignore_layer(
prefix,
ignore=self.ignore,
fused_mapping=self.packed_modules_mapping,
):
return UnquantizedLinearMethod()
key = "model"
packed_modules_mapping_subset = self.packed_modules_mapping.get(key, {})
prefix_in_quant_config = prefix
proj_name = prefix.split(".")[-1]
if proj_name in packed_modules_mapping_subset:
prefix_in_quant_config = prefix.replace(
proj_name, packed_modules_mapping_subset[proj_name][0]
)
if self.is_layer_skipped(prefix, packed_modules_mapping_subset):
return UnquantizedLinearMethod()
scheme = self.get_scheme(layer=layer, layer_name=prefix_in_quant_config)
layer.scheme = scheme
return ModelSlimLinearMethod(self)
else:
return None
def _get_scheme_from_parts(
self,
layer_name: str,
) -> ModelSlimLinearScheme:
full_weight_name = layer_name + ".weight"
if self._name_mapper is not None:
mapped_name, _, _ = self._name_mapper(full_weight_name)
else:
mapped_name = full_weight_name
quant_type = self.quant_description.get(mapped_name, "")
prefix = mapped_name.removesuffix(".weight")
if quant_type == "W8A8_DYNAMIC" or quant_type == "W8A8":
return ModelSlimW8A8Int8(quant_config=self.quant_description, prefix=prefix)
elif quant_type == "W4A4_DYNAMIC":
return ModelSlimW4A4Int4(quant_config=self.quant_description, prefix=prefix)
elif quant_type == "W8A8_MXFP8":
from sglang.multimodal_gen.runtime.layers.quantization.modelslim_mxfp8_scheme import (
ModelSlimMXFP8Scheme,
)
return ModelSlimMXFP8Scheme()
elif quant_type in ("W4A4_MXFP4", "W4A4_MXFP4_DUALSCALE"):
from sglang.multimodal_gen.runtime.layers.quantization.modelslim_mxfp4_scheme import (
ModelSlimMXFP4Scheme,
)
return ModelSlimMXFP4Scheme()
raise NotImplementedError(
f"No modelslim compatible scheme was found for layer '{layer_name}'. "
f"quant_description['{layer_name}.weight'] = '{quant_type}'"
)
def get_scheme(
self, layer: torch.nn.Module, layer_name: Optional[str] = None
) -> Optional[ModelSlimLinearScheme]:
"""
get_scheme method adjusted for modelslim, taken from
python/sglang/srt/layers/quantization/compressed_tensors/compressed_tensors.py
"""
scheme = self._get_scheme_from_parts(
layer_name=layer_name,
)
# Ascend doesn't support device capability
logger.debug("Using scheme: %s for %s", scheme.__class__.__name__, layer_name)
return scheme
def is_layer_skipped(
self, prefix: str, fused_mapping: Mapping[str, List[str]] = MappingProxyType({})
):
# adapted from vllm.model_executor.layers.quantization.utils.quant_utils.is_layer_skipped
proj_name = prefix.split(".")[-1]
if proj_name in fused_mapping:
shard_prefixes = [
prefix.replace(proj_name, shard_proj_name)
for shard_proj_name in fused_mapping[proj_name]
]
is_skipped = None
for shard_prefix in shard_prefixes:
is_shard_skipped = (
self.quant_description.get(shard_prefix + ".weight", "") == "FLOAT"
)
if is_skipped is None:
is_skipped = is_shard_skipped
elif is_shard_skipped != is_skipped:
raise ValueError(
f"Detected some but not all shards of {prefix} "
"are quantized. All shards of fused layers "
"to have the same precision."
)
else:
is_skipped = self.quant_description.get(prefix + ".weight", "") == "FLOAT"
assert is_skipped is not None
return is_skipped
def get_scaled_act_names(self) -> List[str]:
return []
class ModelSlimLinearMethod(LinearMethodBase):
def __init__(self, quantization_config: ModelSlimConfig):
self.quantization_config = quantization_config
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
layer.scheme.process_weights_after_loading(layer)
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
"""
Use the ModelSlimLinearScheme associated with each layer to create
the necessary parameters for the layer. See LinearMethodBase for param
details
"""
weight_loader = extra_weight_attrs.get("weight_loader")
layer.scheme.create_weights(
layer=layer,
input_size=input_size,
input_size_per_partition=input_size_per_partition,
output_partition_sizes=output_partition_sizes,
output_size=output_size,
params_dtype=params_dtype,
weight_loader=weight_loader,
)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
):
"""
Use the output of create_weights and the CompressedTensorsScheme
associated with the layer to apply the forward pass with the
layer input. See LinearMethodBase for param details
"""
scheme = layer.scheme
if scheme is None:
raise ValueError("A scheme must be defined for each layer")
return scheme.apply_weights(layer, x, bias=bias)
@@ -0,0 +1,197 @@
"""ModelSlim MXFP4 scheme for pre-quantized weight inference on Ascend NPU.
Loads weights pre-quantized by msmodelslim and runs MXFP4 dual-level
matmul at inference via npu_dual_level_quant_matmul.
Checkpoint tensor formats (verified from msmodelslim export):
weight: [out, in] float8_e4m3fn (FP4 data in fp8 container)
weight_scale: [out, in/32] uint8 (L1 block scales, e8m0+127)
weight_dual_scale:[out, in/512, 1] float32 (L0 coarse scales)
mul_scale: [in] float32 (smooth quant activation scale)
Reference: MindIE-SD W4A4MXFP4DualQuantLinear
(MindIE-SD/mindiesd/quantization/layer.py)
"""
from typing import List, Optional
import torch
from sglang.multimodal_gen.runtime.platforms import current_platform
_is_npu = current_platform.is_npu()
if _is_npu:
import torch_npu
from sglang.multimodal_gen.runtime.models.parameter import (
BasevLLMParameter,
GroupQuantScaleParameter,
ModelWeightParameter,
)
from sglang.srt.layers.quantization.modelslim.schemes import ModelSlimLinearScheme
MXFP4_BLOCK_SIZE = 32
# L1 (dual) scale groups this many L0 blocks together.
# L1 block covers 16 * 32 = 512 elements.
MXFP4_DUAL_LEVEL_RATIO = 16
class ModelSlimMXFP4Scheme(ModelSlimLinearScheme):
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
weight_loader = extra_weight_attrs.get("weight_loader")
output_size_per_partition = sum(output_partition_sizes)
# msmodelslim exports weight as float8_e4m3fn, shape [out, in].
# Each byte is a float8 container for FP4 data; the actual FP4 packing
# (npu_dtype_cast → float4_e2m1fn_x2) happens in process_weights_after_loading.
weight = ModelWeightParameter(
data=torch.empty(
(output_size_per_partition, input_size_per_partition),
dtype=torch.float8_e4m3fn,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
layer.register_parameter("weight", weight)
# L1 block scale: uint8 [out, in/32], e8m0 scale with +127 offset.
scale_dim = input_size_per_partition // MXFP4_BLOCK_SIZE
weight_scale = GroupQuantScaleParameter(
data=torch.empty(
(output_size_per_partition, scale_dim),
dtype=torch.uint8,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
layer.register_parameter("weight_scale", weight_scale)
# L0 (coarse) scale for dual-level quantization matmul.
# Each L0 block covers MXFP4_DUAL_LEVEL_RATIO L1 blocks = 16 * 32 = 512 elements.
dual_scale_dim = scale_dim // MXFP4_DUAL_LEVEL_RATIO # in/32 / 16 = in/512
weight_dual_scale = GroupQuantScaleParameter(
data=torch.empty(
(output_size_per_partition, dual_scale_dim, 1),
dtype=torch.float32,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
layer.register_parameter("weight_dual_scale", weight_dual_scale)
# Smooth quant activation scale (mul_scale) from NonFusionSmoothQuantWrapper.
# msmodelslim exports this as `<prefix>.div.mul_scale` with shape [in].
# After repack, it becomes `<prefix>.mul_scale`.
# This is CRITICAL: the offline-quantized weights were calibrated with
# x * mul_scale applied to the activation. Omitting it causes mosaic output.
# Ref: MindIE-SD W4A4MXFP4DualQuantLinear.quant_matmul lines 385-386.
mul_scale = BasevLLMParameter(
data=torch.empty(
(input_size_per_partition,),
dtype=torch.float32,
),
weight_loader=weight_loader,
)
# If mul_scale is not in the checkpoint (e.g. non-smooth-quant model
# or old repack without .div. handling), initialize to ones so that
# x * 1.0 = x (no-op). fsdp_load.py checks this attribute.
mul_scale.missing_param_init = "ones"
layer.register_parameter("mul_scale", mul_scale)
def process_weights_after_loading(self, layer: torch.nn.Module):
# Cast weight from fp8 container to FP4 packed format
weight = layer.weight.data
if not weight.is_npu:
weight = weight.to(f"npu:{torch.npu.current_device()}")
weight = torch_npu.npu_dtype_cast(weight, torch_npu.float4_e2m1fn_x2)
# npu_dual_level_quant_matmul requires x2 in FRACTAL_NZ format (format 29).
# Reference: MindIE-SD W4A4MXFP4DualQuantLinear._init_dynamic_quant_param
weight = torch_npu.npu_format_cast(
weight.view(torch.int8), 29, customize_dtype=torch.int8
)
layer.weight = torch.nn.Parameter(weight, requires_grad=False)
# Reshape weight_scale: [out, in/32] -> [out, in/64, 2]
# The dual-level matmul API expects L1 scales in this 3D format
weight_scale = layer.weight_scale.data
if not weight_scale.is_npu:
weight_scale = weight_scale.to(f"npu:{torch.npu.current_device()}")
weight_scale = weight_scale.reshape(weight_scale.shape[0], -1, 2)
layer.weight_scale = torch.nn.Parameter(weight_scale, requires_grad=False)
# Transform weight_dual_scale: [out, in/512, 1] -> [in/512, out]
weight_dual_scale = layer.weight_dual_scale.data
if not weight_dual_scale.is_npu:
weight_dual_scale = weight_dual_scale.to(
f"npu:{torch.npu.current_device()}"
)
weight_dual_scale = weight_dual_scale.squeeze(-1).transpose(0, 1).contiguous()
layer.weight_dual_scale = torch.nn.Parameter(
weight_dual_scale, requires_grad=False
)
# Move mul_scale to NPU if present and not already there
mul_scale = layer.mul_scale.data
if not mul_scale.is_npu:
mul_scale = mul_scale.to(f"npu:{torch.npu.current_device()}")
layer.mul_scale = torch.nn.Parameter(mul_scale, requires_grad=False)
layer.use_mul_scale = not torch.all(mul_scale == 1.0).item()
def apply_weights(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
original_dtype = x.dtype
if original_dtype not in (torch.float16, torch.bfloat16):
x = x.to(torch.bfloat16)
original_dtype = torch.bfloat16
# Flatten to 2D for npu_dynamic_dual_level_mx_quant
input_shape = x.shape
x_2d = x.reshape(-1, x.shape[-1])
# Apply smooth quant scale before activation quantization.
# The offline-quantized weights were calibrated under x * mul_scale,
# so we MUST apply it here for scale alignment.
# Reference: MindIE-SD W4A4MXFP4DualQuantLinear.quant_matmul
mul_scale = layer.mul_scale
if getattr(layer, "use_mul_scale", True):
x_2d = x_2d * mul_scale.to(x_2d.dtype)
# Dual-level MXFP4 activation quantization
x1, l0_scale, l1_scale = torch_npu.npu_dynamic_dual_level_mx_quant(
x_2d, smooth_scale=None
)
# Dual-level MXFP4 matmul
output = torch_npu.npu_dual_level_quant_matmul(
x1,
layer.weight,
l0_scale,
layer.weight_dual_scale,
l1_scale,
layer.weight_scale,
bias=bias.to(torch.float32) if bias is not None else None,
output_dtype=original_dtype,
)
# Restore original shape
output_shape = list(input_shape[:-1]) + [output.shape[-1]]
return output.reshape(output_shape)
@@ -0,0 +1,124 @@
"""ModelSlim MXFP8 scheme for pre-quantized weight inference on Ascend NPU.
Loads weights pre-quantized by msmodelslim (float8_e4m3fn weights,
uint8 scales) and runs MXFP8 matmul at inference.
"""
from typing import List, Optional
import torch
from sglang.multimodal_gen.runtime.platforms import current_platform
_is_npu = current_platform.is_npu()
if _is_npu:
import torch_npu
from sglang.multimodal_gen.runtime.models.parameter import (
GroupQuantScaleParameter,
ModelWeightParameter,
)
from sglang.srt.layers.quantization.modelslim.schemes import ModelSlimLinearScheme
MXFP8_BLOCK_SIZE = 32
class ModelSlimMXFP8Scheme(ModelSlimLinearScheme):
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
weight_loader = extra_weight_attrs.get("weight_loader")
output_size_per_partition = sum(output_partition_sizes)
# msmodelslim exports weight as float8_e4m3fn, shape [out, in]
weight = ModelWeightParameter(
data=torch.empty(
(output_size_per_partition, input_size_per_partition),
dtype=torch.float8_e4m3fn,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
layer.register_parameter("weight", weight)
# msmodelslim exports weight_scale as uint8, shape [out, in/32].
# NOTE: This parameter is intentionally named "weight_scale" (not
# "weight_scale_inv" as used in mxfp8_npu.py) because the weight loader
# matches parameter names to checkpoint keys, and msmodelslim checkpoints
# store this tensor under the key "<layer>.weight_scale".
scale_dim = input_size_per_partition // MXFP8_BLOCK_SIZE
weight_scale = GroupQuantScaleParameter(
data=torch.empty(
(output_size_per_partition, scale_dim),
dtype=torch.uint8,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
layer.register_parameter("weight_scale", weight_scale)
def process_weights_after_loading(self, layer: torch.nn.Module):
# weight is already float8_e4m3fn, no cast needed
weight = layer.weight.data
layer.weight = torch.nn.Parameter(weight, requires_grad=False)
# Reshape weight_scale: [out, in/32] -> [out, in/32//2, 2]
weight_scale = layer.weight_scale.data
weight_scale = weight_scale.reshape(weight_scale.shape[0], -1, 2)
layer.weight_scale = torch.nn.Parameter(weight_scale, requires_grad=False)
def apply_weights(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
original_dtype = x.dtype
if original_dtype not in (torch.float16, torch.bfloat16):
# npu_dynamic_mx_quant only accepts fp16/bf16 activations
x = x.to(torch.bfloat16)
original_dtype = torch.bfloat16
# npu_dynamic_mx_quant requires a 2D input [tokens, hidden_size].
# Diffusion transformer inputs are typically 3D [batch, seq, hidden] or
# higher. Flattening to 2D merges all leading dimensions into a single
# token axis so the NPU kernel can compute per-token MXFP8 scales, then
# we restore the original shape from the output.
input_shape = x.shape
x_2d = x.reshape(-1, x.shape[-1])
# Dynamic MXFP8 activation quantisation
qx, input_scale = torch_npu.npu_dynamic_mx_quant(
x_2d, dst_type=torch_npu.float8_e4m3fn
)
# MXFP8 matmul
output = torch_npu.npu_quant_matmul(
qx,
layer.weight.transpose(0, 1),
layer.weight_scale.transpose(0, 1),
scale_dtype=torch_npu.float8_e8m0fnu,
pertoken_scale=input_scale,
pertoken_scale_dtype=torch_npu.float8_e8m0fnu,
bias=bias.to(torch.float32) if bias is not None else None,
output_dtype=original_dtype,
group_sizes=[1, 1, MXFP8_BLOCK_SIZE],
)
# Restore original shape
output_shape = list(input_shape[:-1]) + [output.shape[-1]]
output = output.reshape(output_shape)
return output
@@ -0,0 +1,238 @@
import logging
from typing import Dict, List, Optional
import torch
from torch.nn.parameter import Parameter
from sglang.multimodal_gen.runtime.layers.linear import (
LinearMethodBase,
UnquantizedLinearMethod,
)
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.models.parameter import (
ModelWeightParameter,
PerTensorScaleParameter,
)
from sglang.srt.layers.quantization.utils import is_layer_skipped
from sglang.srt.utils import is_hip, mxfp_supported
logger = logging.getLogger(__name__)
_is_hip = is_hip()
if _is_hip:
try:
import aiter
from aiter.ops.gemm_op_a4w4 import gemm_a4w4
from aiter.ops.shuffle import shuffle_weight
from aiter.utility.fp4_utils import dynamic_mxfp4_quant
except ImportError as e:
logger.warning(f"aiter MXFP4 kernels not available: {e}")
aiter = None
shuffle_weight = None
dynamic_mxfp4_quant = None
gemm_a4w4 = None
# The gemm_a4w4 ASM kernel has degraded precision when the output
# dimension (N) is smaller than its minimum tile size.
# Layers with output_size falls below this threshold will stay unquantized
_MXFP4_MIN_OUTPUT_DIM = 256
class Mxfp4Config(QuantizationConfig):
"""
MXFP4 quantization config for diffusion models.
Supports online quantization from unquantized BF16/FP16 checkpoints;
no-arg ``Mxfp4Config()`` selects that online (post-load) path.
Note: MXFP4 requires ROCm and MI350+ (gfx95x).
"""
def __init__(
self,
is_checkpoint_mxfp4_serialized: bool = False,
ignored_layers: Optional[List[str]] = None,
packed_modules_mapping: Optional[Dict[str, List[str]]] = None,
):
super().__init__()
self.is_checkpoint_mxfp4_serialized = is_checkpoint_mxfp4_serialized
self.ignored_layers = ignored_layers or []
self.packed_modules_mapping = packed_modules_mapping or {}
@classmethod
def get_name(cls) -> str:
return "mxfp4"
@classmethod
def get_supported_act_dtypes(cls) -> list[torch.dtype]:
return [torch.bfloat16, torch.float16]
@classmethod
def get_min_capability(cls) -> int:
return 95 # gfx95x, Note: mxfp_supported() is a better check
@classmethod
def get_config_filenames(cls) -> list[str]:
return [] # No config file needed for online quantization
@classmethod
def from_config(cls, config: dict) -> "Mxfp4Config":
"""Create from model config (for pre-quantized checkpoints)."""
is_serialized = config.get("quant_method") == "mxfp4"
return cls(is_checkpoint_mxfp4_serialized=is_serialized)
def get_quant_method(self, layer, prefix: str):
from sglang.multimodal_gen.runtime.layers.linear import LinearBase
if isinstance(layer, LinearBase):
if is_layer_skipped(
prefix,
self.ignored_layers,
fused_mapping=self.packed_modules_mapping,
):
logger.debug(
f"MXFP4: Keeping layer {prefix} unquantized (in ignored_layers)"
)
return UnquantizedLinearMethod()
# Skip layers whose output dims are too small, see ASM kernel comment above
output_size = getattr(layer, "output_size", None)
if output_size is not None and output_size < _MXFP4_MIN_OUTPUT_DIM:
logger.info(
f"MXFP4: Keeping layer {prefix} unquantized "
f"(output_size={output_size} < {_MXFP4_MIN_OUTPUT_DIM})"
)
return UnquantizedLinearMethod()
logger.debug(f"MXFP4: Replacing layer {prefix} with MXFP4 linear method")
return Mxfp4LinearMethod(self)
else:
logger.debug(f"MXFP4: Skipping layer {prefix} (not a LinearBase)")
return None
class Mxfp4LinearMethod(LinearMethodBase):
"""
MXFP4 online quantization method for linear layers.
Quantizes unquantized BF16/FP16 weights to MXFP4 format during
process_weights_after_loading().
"""
def __init__(self, quant_config: Mxfp4Config):
self.quant_config = quant_config
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: list[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
"""
Creates BF16/FP16 parameters that will be
quantized to MXFP4 in process_weights_after_loading().
"""
output_size_per_partition = sum(output_partition_sizes)
weight_loader = extra_weight_attrs.get("weight_loader")
weight = ModelWeightParameter(
data=torch.empty(
output_size_per_partition,
input_size_per_partition,
dtype=params_dtype,
),
weight_loader=weight_loader,
input_dim=1,
output_dim=0,
)
layer.register_parameter("weight", weight)
# Placeholder scale (will be created during quantization)
weight_scale = PerTensorScaleParameter(
data=torch.empty(1, dtype=torch.float32),
weight_loader=weight_loader,
)
layer.register_parameter("weight_scale", weight_scale)
def process_weights_after_loading(self, layer: torch.nn.Module):
"""
Quantize BF16/FP16 weights to MXFP4 after loading from checkpoint.
Converts weights from unquantized format to:
- Packed uint8 (2 FP4 values per byte)
- E8M0 scales (one per 32-element block)
"""
if not mxfp_supported():
platform = "unknown"
if _is_hip:
try:
platform = torch.cuda.get_device_properties(0).gcnArchName
except:
platform = "ROCm (unknown arch)"
raise RuntimeError(
f"MXFP4 quantization requires ROCm and MI350+ (gfx95x). "
f"Current platform: {platform}."
)
# Check if weights are already quantized
if layer.weight.dtype not in [torch.bfloat16, torch.float16]:
# Already quantized or unexpected dtype
logger.info("Weights are quantized or unexpected dtype")
return
if any(fn is None for fn in (dynamic_mxfp4_quant, shuffle_weight, gemm_a4w4)):
raise RuntimeError(
"aiter MXFP4 kernels not available. "
"Install aiter with MXFP4 support."
)
weight_data = layer.weight.data
was_on_cpu = weight_data.device.type == "cpu"
if was_on_cpu:
weight_data = weight_data.cuda()
w_quant, mx_scales = dynamic_mxfp4_quant(weight_data, shuffle=True)
w_quant_shuffled = shuffle_weight(w_quant)
if was_on_cpu:
w_quant_shuffled = w_quant_shuffled.cpu()
mx_scales = mx_scales.cpu()
layer.weight = Parameter(w_quant_shuffled, requires_grad=False)
layer.weight_scale = Parameter(mx_scales, requires_grad=False)
logger.debug(
f"MXFP4: Quantized layer weights - weight {layer.weight.shape} {layer.weight.dtype}, "
f"scale {layer.weight_scale.shape}"
)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
if not mxfp_supported():
raise RuntimeError(
"MXFP4 inference requires ROCm and MI350+ (gfx95x). "
"Current platform not supported."
)
# Handle 3D input tensors [batch, seq, hidden]
original_shape = x.shape
if x.dim() == 3:
x = x.view(-1, x.shape[-1])
x_fp4, x_scale = dynamic_mxfp4_quant(x, shuffle=True)
y = gemm_a4w4(x_fp4, layer.weight, x_scale, layer.weight_scale)
if bias is not None:
y = y + bias
return y.view(*original_shape[:-1], layer.weight.shape[0])
@@ -0,0 +1,201 @@
"""Online MXFP4 quantization for Diffusion models on Ascend NPU.
Provides ``NPUMXFP4Config`` (registered as ``"mxfp4_npu"``) and
``NPUMXFP4DiffusionLinearMethod`` which quantises FP16/BF16 weights to MXFP4
at load time using dual-level MX quantization and uses
``npu_dynamic_dual_level_mx_quant`` + ``npu_dual_level_quant_matmul`` for
inference.
The ``"mxfp4_npu"`` key is distinct from upstream's ROCm ``"mxfp4"``
(``Mxfp4Config`` in ``mxfp4.py``) which targets AMD MI350+ via aiter kernels.
NOTE: Online weight quantization via ``npu_dynamic_dual_level_mx_quant`` is
experimental. MindIE-SD only uses an offline (pre-quantized) path for MXFP4
weights. The online path quantizes FP16/BF16 weights at load time, which may
produce different numerical results than the offline calibrated path.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
import torch
from torch.nn.parameter import Parameter
from sglang.multimodal_gen.runtime.platforms import current_platform
_is_npu = current_platform.is_npu()
if _is_npu:
import torch_npu
from sglang.multimodal_gen.runtime.layers.linear import LinearBase, LinearMethodBase
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
QuantizeMethodBase,
)
from sglang.multimodal_gen.runtime.models.parameter import ModelWeightParameter
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class NPUMXFP4Config(QuantizationConfig):
"""Config for online MXFP4 quantization on Ascend NPU (Diffusion)."""
def __init__(self) -> None:
super().__init__()
@classmethod
def get_name(cls) -> str:
return "mxfp4_npu"
@classmethod
def get_supported_act_dtypes(cls) -> List[torch.dtype]:
return [torch.bfloat16, torch.float16]
@classmethod
def get_min_capability(cls) -> int:
return 0 # NPU, not CUDA
@classmethod
def get_config_filenames(cls) -> List[str]:
return []
@classmethod
def from_config(cls, config: Dict[str, Any]) -> NPUMXFP4Config:
return cls()
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> Optional[QuantizeMethodBase]:
if isinstance(layer, LinearBase):
return NPUMXFP4DiffusionLinearMethod(self)
return None
def get_scaled_act_names(self) -> List[str]:
return []
class NPUMXFP4DiffusionLinearMethod(LinearMethodBase):
"""Ascend NPU MXFP4 linear method for Diffusion models (dual-level).
Online mode: loads FP16/BF16 weights → quantises to MXFP4 at load time
via ``npu_dynamic_dual_level_mx_quant``.
Inference: dynamic dual-level MXFP4 activation quant + dual-level matmul.
Reference: MindIE-SD ``W4A4MXFP4DualQuantLinear`` (offline path only).
"""
def __init__(self, quant_config: NPUMXFP4Config):
self.quant_config = quant_config
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
output_size_per_partition = sum(output_partition_sizes)
weight_loader = extra_weight_attrs.get("weight_loader")
layer.logical_widths = output_partition_sizes
layer.input_size_per_partition = input_size_per_partition
layer.output_size_per_partition = output_size_per_partition
layer.orig_dtype = params_dtype
# Load weights in original dtype; quantise later in process_weights_after_loading
weight = ModelWeightParameter(
data=torch.empty(
output_size_per_partition,
input_size_per_partition,
dtype=params_dtype,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
layer.register_parameter("weight", weight)
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
weight_fp = layer.weight.data
if weight_fp.dtype not in (torch.float16, torch.bfloat16):
weight_fp = weight_fp.to(torch.bfloat16)
# Move weight to NPU if needed. dit_cpu_offload defaults to True in
# ServerArgs, which causes fsdp_load to move parameters back to CPU
# after loading. npu_dynamic_dual_level_mx_quant requires an NPU tensor.
if not weight_fp.is_npu:
weight_fp = weight_fp.to(f"npu:{torch.npu.current_device()}")
# Online dual-level MXFP4 weight quantisation.
# NOTE: This is experimental — MindIE-SD only has an offline path for
# MXFP4 weights. We assume npu_dynamic_dual_level_mx_quant can also
# quantise weights (not just activations).
# Returns: (qw, w_dual_scale, w_scale)
# qw — quantized weight in float4_e2m1fn_x2 (2 FP4 packed/byte)
# w_dual_scale — L0-level scale (goes to pos 3 in npu_dual_level_quant_matmul)
# w_scale — L1-level scale (goes to pos 5 in npu_dual_level_quant_matmul)
qw, w_dual_scale, w_scale = torch_npu.npu_dynamic_dual_level_mx_quant(
weight_fp, smooth_scale=None
)
# npu_dual_level_quant_matmul requires x2 (weight) in FRACTAL_NZ format.
# Reference: MindIE-SD W4A4MXFP4DualQuantLinear._init_dynamic_quant_param
qw = torch_npu.npu_format_cast(
qw.view(torch.int8), 29, customize_dtype=torch.int8
)
# x2Level0Scale must be [in/level0_block_size, out] — transpose from
# the [out, in/level0_block_size] shape returned by the quant op.
# Reference: MindIE-SD layer.py:409
w_dual_scale = w_dual_scale.squeeze(-1).transpose(0, 1).contiguous()
layer.weight = Parameter(qw, requires_grad=False)
layer.weight_dual_scale = Parameter(w_dual_scale, requires_grad=False)
layer.weight_scale = Parameter(w_scale, requires_grad=False)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
original_dtype = x.dtype
if original_dtype not in (torch.float16, torch.bfloat16):
x = x.to(torch.bfloat16)
original_dtype = torch.bfloat16
# Flatten to 2D [tokens, hidden] for the quantization operators
input_shape = x.shape
x_2d = x.reshape(-1, x.shape[-1])
# Dynamic dual-level MXFP4 activation quantisation
qx, act_l0_scale, act_l1_scale = torch_npu.npu_dynamic_dual_level_mx_quant(
x_2d, smooth_scale=None
)
# Dual-level MXFP4 matmul
# Arg order: act_quant, weight_quant, act_l0_scale, weight_dual_scale,
# act_l1_scale, weight_scale, bias=, output_dtype=
# NOTE: weight is NOT transposed (unlike MXFP8's npu_quant_matmul).
output = torch_npu.npu_dual_level_quant_matmul(
qx,
layer.weight,
act_l0_scale,
layer.weight_dual_scale,
act_l1_scale,
layer.weight_scale,
bias=bias.to(torch.float32) if bias is not None else None,
output_dtype=original_dtype,
)
# Restore original shape (replace last dim with output features)
output_shape = list(input_shape[:-1]) + [output.shape[-1]]
output = output.reshape(output_shape)
return output
@@ -0,0 +1,176 @@
"""Online MXFP8 quantization for Diffusion models on Ascend NPU.
Provides ``MXFP8Config`` (registered as ``"mxfp8"``) and
``NPUMXFP8DiffusionLinearMethod`` which quantise FP16/BF16 weights to MXFP8
at load time and use ``npu_dynamic_mx_quant`` + ``npu_quant_matmul`` for
inference, mirroring the LLM-side ``NPUMXFP8LinearMethod``.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
import torch
from torch.nn.parameter import Parameter
from sglang.multimodal_gen.runtime.platforms import current_platform
_is_npu = current_platform.is_npu()
if _is_npu:
import torch_npu
from sglang.multimodal_gen.runtime.layers.linear import LinearBase, LinearMethodBase
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
QuantizeMethodBase,
)
from sglang.multimodal_gen.runtime.models.parameter import ModelWeightParameter
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
MXFP8_BLOCK_SIZE = 32
class MXFP8Config(QuantizationConfig):
"""Config for online MXFP8 quantization on Ascend NPU (Diffusion)."""
def __init__(self) -> None:
super().__init__()
@classmethod
def get_name(cls) -> str:
return "mxfp8"
@classmethod
def get_supported_act_dtypes(cls) -> List[torch.dtype]:
return [torch.bfloat16, torch.float16]
@classmethod
def get_min_capability(cls) -> int:
return 0 # NPU, not CUDA
@classmethod
def get_config_filenames(cls) -> List[str]:
return []
@classmethod
def from_config(cls, config: Dict[str, Any]) -> MXFP8Config:
return cls()
def get_quant_method(
self, layer: torch.nn.Module, prefix: str
) -> Optional[QuantizeMethodBase]:
if isinstance(layer, LinearBase):
return NPUMXFP8DiffusionLinearMethod(self)
return None
def get_scaled_act_names(self) -> List[str]:
return []
class NPUMXFP8DiffusionLinearMethod(LinearMethodBase):
"""Ascend NPU MXFP8 linear method for Diffusion models.
Online mode: loads FP16/BF16 weights → quantises to MXFP8 at load time.
Inference: dynamic MXFP8 activation quant + MXFP8 matmul (block_size=32).
"""
def __init__(self, quant_config: MXFP8Config):
self.quant_config = quant_config
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
output_size_per_partition = sum(output_partition_sizes)
weight_loader = extra_weight_attrs.get("weight_loader")
layer.logical_widths = output_partition_sizes
layer.input_size_per_partition = input_size_per_partition
layer.output_size_per_partition = output_size_per_partition
layer.orig_dtype = params_dtype
# Load weights in original dtype; quantise later in process_weights_after_loading
weight = ModelWeightParameter(
data=torch.empty(
output_size_per_partition,
input_size_per_partition,
dtype=params_dtype,
),
input_dim=1,
output_dim=0,
weight_loader=weight_loader,
)
layer.register_parameter("weight", weight)
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
weight_fp = layer.weight.data
if weight_fp.dtype not in (torch.float16, torch.bfloat16):
weight_fp = weight_fp.to(torch.bfloat16)
# Move weight to NPU if needed. We intentionally use a conditional
# move rather than an assert because `dit_cpu_offload` defaults to
# True in ServerArgs, which causes fsdp_load to move every parameter
# back to CPU after loading (even when the target device is NPU).
# npu_dynamic_mx_quant requires an NPU tensor, so we must transfer
# here. The quantized fp8 weights produced below will remain on NPU
# for inference; if the model still needs to be offloaded after
# quantization (e.g. very large model on a small NPU), a higher-level
# offload pass can move them back afterwards.
if not weight_fp.is_npu:
weight_fp = weight_fp.to(f"npu:{torch.npu.current_device()}")
# Online MXFP8 quantisation of weights (block_size=32)
qw, w_scale = torch_npu.npu_dynamic_mx_quant(
weight_fp, dst_type=torch_npu.float8_e4m3fn
)
layer.weight = Parameter(qw, requires_grad=False)
layer.weight_scale_inv = Parameter(w_scale, requires_grad=False)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
original_dtype = x.dtype
if original_dtype not in (torch.float16, torch.bfloat16):
x = x.to(torch.bfloat16)
original_dtype = torch.bfloat16
# Flatten to 2D [tokens, hidden] so npu_dynamic_mx_quant returns 3D scale
input_shape = x.shape
x_2d = x.reshape(-1, x.shape[-1])
# Dynamic MXFP8 activation quantisation
qx, input_scale = torch_npu.npu_dynamic_mx_quant(
x_2d, dst_type=torch_npu.float8_e4m3fn
)
# MXFP8 matmul
output = torch_npu.npu_quant_matmul(
qx,
layer.weight.transpose(0, 1),
layer.weight_scale_inv.transpose(0, 1),
scale_dtype=torch_npu.float8_e8m0fnu,
pertoken_scale=input_scale,
pertoken_scale_dtype=torch_npu.float8_e8m0fnu,
bias=bias.to(torch.float32) if bias is not None else None,
output_dtype=original_dtype,
group_sizes=[1, 1, MXFP8_BLOCK_SIZE],
)
# Restore original shape (replace last dim with output features)
output_shape = list(input_shape[:-1]) + [output.shape[-1]]
output = output.reshape(output_shape)
return output
@@ -0,0 +1,291 @@
# SPDX-License-Identifier: Apache-2.0
from typing import List, Optional
import torch
import torch.nn as nn
from torch.nn.parameter import Parameter
from sglang.multimodal_gen.runtime.layers.linear import LinearMethodBase
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.runtime.utils.weight_attrs import set_weight_attrs
logger = init_logger(__name__)
try:
from nunchaku.ops.gemm import svdq_gemm_w4a4_cuda
from nunchaku.ops.gemv import awq_gemv_w4a16_cuda
from nunchaku.ops.quantize import svdq_quantize_w4a4_act_fuse_lora_cuda
except ImportError:
svdq_gemm_w4a4_cuda = None
awq_gemv_w4a16_cuda = None
svdq_quantize_w4a4_act_fuse_lora_cuda = None
class NunchakuSVDQLinearMethod(LinearMethodBase):
def __init__(
self,
precision: str = "int4",
rank: int = 32,
act_unsigned: bool = False,
):
self.precision = precision
self.rank = rank
self.act_unsigned = act_unsigned
if precision == "nvfp4":
self.group_size = 16
else:
self.group_size = 64
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
) -> None:
output_size_per_partition = sum(output_partition_sizes)
qweight = Parameter(
torch.empty(
output_size_per_partition,
input_size_per_partition // 2,
dtype=torch.int8,
),
requires_grad=False,
)
set_weight_attrs(qweight, {"input_dim": 1, "output_dim": 0})
num_groups = input_size_per_partition // self.group_size
if self.precision == "nvfp4":
scale_dtype = torch.float8_e4m3fn
else:
scale_dtype = params_dtype
wscales = Parameter(
torch.empty(num_groups, output_size_per_partition, dtype=scale_dtype),
requires_grad=False,
)
smooth_factor = Parameter(
torch.empty(input_size_per_partition, dtype=params_dtype),
requires_grad=False,
)
smooth_factor_orig = Parameter(
torch.empty(input_size_per_partition, dtype=params_dtype),
requires_grad=False,
)
proj_down = Parameter(
torch.empty(input_size_per_partition, self.rank, dtype=params_dtype),
requires_grad=False,
)
proj_up = Parameter(
torch.empty(output_size_per_partition, self.rank, dtype=params_dtype),
requires_grad=False,
)
if self.precision == "nvfp4":
wcscales = Parameter(
torch.empty(
output_size_per_partition,
dtype=params_dtype,
),
requires_grad=False,
)
wtscale = Parameter(
torch.empty(1, dtype=params_dtype),
requires_grad=False,
)
else:
wcscales = None
wtscale = None
layer.register_parameter("qweight", qweight)
layer.register_parameter("wscales", wscales)
layer.register_parameter("smooth_factor", smooth_factor)
layer.register_parameter("smooth_factor_orig", smooth_factor_orig)
layer.register_parameter("proj_down", proj_down)
layer.register_parameter("proj_up", proj_up)
if wcscales is not None:
layer.register_parameter("wcscales", wcscales)
if wtscale is not None:
layer.register_parameter("wtscale", wtscale)
layer.input_size_per_partition = input_size_per_partition
layer.output_size_per_partition = output_size_per_partition
layer.precision = self.precision
layer.rank = self.rank
layer.group_size = self.group_size
layer.act_unsigned = self.act_unsigned
weight_loader = extra_weight_attrs.get("weight_loader")
if weight_loader is not None:
set_weight_attrs(qweight, {"weight_loader": weight_loader})
set_weight_attrs(wscales, {"weight_loader": weight_loader})
set_weight_attrs(smooth_factor, {"weight_loader": weight_loader})
set_weight_attrs(smooth_factor_orig, {"weight_loader": weight_loader})
set_weight_attrs(proj_down, {"weight_loader": weight_loader})
set_weight_attrs(proj_up, {"weight_loader": weight_loader})
if wcscales is not None:
set_weight_attrs(wcscales, {"weight_loader": weight_loader})
if wtscale is not None:
set_weight_attrs(wtscale, {"weight_loader": weight_loader})
def process_weights_after_loading(self, layer: nn.Module) -> None:
layer.qweight = Parameter(layer.qweight.data, requires_grad=False)
layer.wscales = Parameter(layer.wscales.data, requires_grad=False)
layer.smooth_factor = Parameter(layer.smooth_factor.data, requires_grad=False)
layer.smooth_factor_orig = Parameter(
layer.smooth_factor_orig.data, requires_grad=False
)
layer.proj_down = Parameter(layer.proj_down.data, requires_grad=False)
layer.proj_up = Parameter(layer.proj_up.data, requires_grad=False)
if hasattr(layer, "wcscales") and layer.wcscales is not None:
layer.wcscales = Parameter(layer.wcscales.data, requires_grad=False)
if hasattr(layer, "wtscale") and layer.wtscale is not None:
layer.wtscale = Parameter(layer.wtscale.data, requires_grad=False)
alpha: float | None = None
wtscale = getattr(layer, "wtscale", None)
if wtscale is not None:
if isinstance(wtscale, Parameter):
wtscale = wtscale.data
if isinstance(wtscale, torch.Tensor):
alpha = float(wtscale.detach().cpu().item())
else:
alpha = float(wtscale)
layer._nunchaku_alpha = alpha
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
orig_shape = x.shape
x_2d = x.reshape(-1, orig_shape[-1])
quantized_x, ascales, lora_act_out = svdq_quantize_w4a4_act_fuse_lora_cuda(
x_2d,
lora_down=layer.proj_down,
smooth=layer.smooth_factor,
fp4=layer.precision == "nvfp4",
pad_size=256,
)
out_2d = torch.empty(
x_2d.shape[0],
layer.output_size_per_partition,
dtype=x_2d.dtype,
device=x_2d.device,
)
alpha: float | None = getattr(layer, "_nunchaku_alpha", None)
wcscales = getattr(layer, "wcscales", None)
svdq_gemm_w4a4_cuda(
act=quantized_x,
wgt=layer.qweight,
out=out_2d,
ascales=ascales,
wscales=layer.wscales,
lora_act_in=lora_act_out,
lora_up=layer.proj_up,
bias=bias,
fp4=layer.precision == "nvfp4",
alpha=alpha,
wcscales=wcscales,
act_unsigned=getattr(layer, "act_unsigned", False),
)
out = out_2d.reshape(*orig_shape[:-1], layer.output_size_per_partition)
return out
class NunchakuAWQLinearMethod(LinearMethodBase):
def __init__(self, group_size: int = 64):
self.group_size = group_size
self.pack_factor = 8
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: List[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
) -> None:
output_size_per_partition = sum(output_partition_sizes)
qweight = Parameter(
torch.empty(
output_size_per_partition // 4,
input_size_per_partition // 2,
dtype=torch.int32,
),
requires_grad=False,
)
set_weight_attrs(qweight, {"input_dim": 1, "output_dim": 0})
num_groups = input_size_per_partition // self.group_size
wscales = Parameter(
torch.empty(num_groups, output_size_per_partition, dtype=params_dtype),
requires_grad=False,
)
wzeros = Parameter(
torch.empty(num_groups, output_size_per_partition, dtype=params_dtype),
requires_grad=False,
)
layer.register_parameter("qweight", qweight)
layer.register_parameter("wscales", wscales)
layer.register_parameter("wzeros", wzeros)
layer.input_size_per_partition = input_size_per_partition
layer.output_size_per_partition = output_size_per_partition
layer.group_size = self.group_size
layer.pack_factor = self.pack_factor
weight_loader = extra_weight_attrs.get("weight_loader")
if weight_loader is not None:
set_weight_attrs(qweight, {"weight_loader": weight_loader})
set_weight_attrs(wscales, {"weight_loader": weight_loader})
set_weight_attrs(wzeros, {"weight_loader": weight_loader})
def process_weights_after_loading(self, layer: nn.Module) -> None:
layer.qweight = Parameter(layer.qweight.data, requires_grad=False)
layer.wscales = Parameter(layer.wscales.data, requires_grad=False)
layer.wzeros = Parameter(layer.wzeros.data, requires_grad=False)
def apply(
self,
layer: torch.nn.Module,
x: torch.Tensor,
bias: Optional[torch.Tensor] = None,
) -> torch.Tensor:
orig_shape = x.shape
x_2d = x.reshape(-1, orig_shape[-1])
in_features = layer.input_size_per_partition
out_features = layer.output_size_per_partition
out_2d = awq_gemv_w4a16_cuda(
in_feats=x_2d,
kernel=layer.qweight,
scaling_factors=layer.wscales,
zeros=layer.wzeros,
m=x_2d.shape[0],
n=out_features,
k=in_features,
group_size=layer.group_size,
)
if bias is not None:
view_shape = [1] * (out_2d.ndim - 1) + [-1]
out_2d.add_(bias.view(view_shape))
out = out_2d.reshape(*orig_shape[:-1], out_features)
return out
@@ -0,0 +1,437 @@
# SPDX-License-Identifier: Apache-2.0
import logging
import torch
import torch.nn as nn
import torch.nn.functional as F
import sglang.multimodal_gen.envs as envs
from sglang.multimodal_gen.runtime.distributed import (
divide,
get_tp_group,
split_tensor_along_last_dim,
tensor_model_parallel_all_gather,
tensor_model_parallel_all_reduce,
)
from sglang.multimodal_gen.runtime.layers.utils import get_group_rank, get_group_size
from sglang.multimodal_gen.runtime.utils.weight_attrs import set_weight_attrs
FP8_WEIGHT_DTYPE = torch.float8_e4m3fn
W8A8_FP8_GEMM_ENV = "SGLANG_DIFFUSION_ENABLE_W8A8_FP8_GEMM"
logger = logging.getLogger(__name__)
_w8a8_fp8_gemm_warning_logged = False
def _can_apply_fused_w8a8_fp8_linear(
x: torch.Tensor,
weight: torch.Tensor,
weight_scale: torch.Tensor,
compute_dtype: torch.dtype,
) -> bool:
return (
x.device.type == "cuda"
and weight.device.type == "cuda"
and weight_scale.device.type == "cuda"
and not x.is_meta
and not weight.is_meta
and not weight_scale.is_meta
and compute_dtype in (torch.float16, torch.bfloat16)
)
def dequantize_rowwise_fp8_weight(
weight: torch.Tensor,
weight_scale: torch.Tensor,
dtype: torch.dtype,
) -> torch.Tensor:
if weight.ndim != 2:
raise ValueError(f"FP8 linear weight must be 2-D, got shape {weight.shape}")
if weight_scale.ndim != 1 or weight_scale.shape[0] != weight.shape[0]:
raise ValueError(
"FP8 row-wise scale must have shape (out_features,), "
f"got weight={tuple(weight.shape)} scale={tuple(weight_scale.shape)}"
)
return weight.to(dtype) * weight_scale.to(dtype).unsqueeze(1)
def _apply_srt_w8a8_fp8_linear(*args, **kwargs) -> torch.Tensor:
from sglang.srt.layers.quantization.fp8_utils import apply_fp8_linear
return apply_fp8_linear(*args, **kwargs)
def _is_cutlass_fp8_supported() -> bool:
from sglang.srt.layers.quantization.fp8_utils import cutlass_fp8_supported
return cutlass_fp8_supported()
def _apply_weight_only_fp8_linear(
x: torch.Tensor,
weight: torch.Tensor,
weight_scale: torch.Tensor,
bias: torch.Tensor | None,
compute_dtype: torch.dtype,
enable_fused_w8a8: bool,
) -> torch.Tensor:
x = x.to(compute_dtype)
bias = bias.to(compute_dtype) if bias is not None else None
if enable_fused_w8a8 and _can_apply_fused_w8a8_fp8_linear(
x, weight, weight_scale, compute_dtype
):
try:
# The fused kernel uses W8A8 compute; fallback keeps BF16/FP16
# activations after dequantizing the FP8 weights.
output = _apply_srt_w8a8_fp8_linear(
input=x,
weight=weight.t(),
weight_scale=weight_scale,
input_scale=None,
bias=bias,
cutlass_fp8_supported=_is_cutlass_fp8_supported(),
)
_log_w8a8_fp8_gemm_warning_once()
return output
except (ImportError, NotImplementedError):
pass
dequant_weight = dequantize_rowwise_fp8_weight(weight, weight_scale, compute_dtype)
return F.linear(x, dequant_weight, bias)
class WeightOnlyFP8Linear(nn.Module):
"""Storage-only e4m3 FP8 linear with row-wise weight scales."""
def __init__(
self,
in_features: int,
out_features: int,
bias: bool = True,
compute_dtype: torch.dtype | None = None,
enable_fused_w8a8: bool | None = None,
) -> None:
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.compute_dtype = compute_dtype
self.enable_fused_w8a8 = _resolve_enable_fused_w8a8(enable_fused_w8a8)
self.weight = nn.Parameter(
torch.empty(out_features, in_features, dtype=FP8_WEIGHT_DTYPE),
requires_grad=False,
)
self.weight_scale = nn.Parameter(
torch.empty(out_features, dtype=torch.float32),
requires_grad=False,
)
set_weight_attrs(self.weight_scale, {"missing_param_init": "error"})
if bias:
self.bias = nn.Parameter(
torch.empty(
out_features, dtype=compute_dtype or torch.get_default_dtype()
),
requires_grad=False,
)
else:
self.register_parameter("bias", None)
def forward(self, x: torch.Tensor) -> torch.Tensor:
compute_dtype = self.compute_dtype or x.dtype
return _apply_weight_only_fp8_linear(
x,
self.weight,
self.weight_scale,
self.bias,
compute_dtype,
self.enable_fused_w8a8,
)
class WeightOnlyFP8ColumnParallelLinear(nn.Module):
"""Column-parallel storage-only e4m3 FP8 linear."""
def __init__(
self,
in_features: int,
out_features: int,
bias: bool = True,
compute_dtype: torch.dtype | None = None,
gather_output: bool = True,
tp_group=None,
enable_fused_w8a8: bool | None = None,
) -> None:
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.compute_dtype = compute_dtype
self.gather_output = gather_output
self.enable_fused_w8a8 = _resolve_enable_fused_w8a8(enable_fused_w8a8)
self.tp_group = tp_group or get_tp_group()
self.tp_size = get_group_size(self.tp_group)
self.tp_rank = get_group_rank(self.tp_group)
self.out_features_per_partition = divide(out_features, self.tp_size)
self.weight = nn.Parameter(
torch.empty(
self.out_features_per_partition,
in_features,
dtype=FP8_WEIGHT_DTYPE,
),
requires_grad=False,
)
set_weight_attrs(
self.weight,
{
"output_dim": 0,
"weight_loader": self.weight_loader,
},
)
self.weight_scale = nn.Parameter(
torch.empty(self.out_features_per_partition, dtype=torch.float32),
requires_grad=False,
)
set_weight_attrs(
self.weight_scale,
{
"missing_param_init": "error",
"output_dim": 0,
"weight_loader": self.weight_loader,
},
)
if bias:
self.bias = nn.Parameter(
torch.empty(
self.out_features_per_partition,
dtype=compute_dtype or torch.get_default_dtype(),
),
requires_grad=False,
)
set_weight_attrs(
self.bias,
{
"output_dim": 0,
"weight_loader": self.weight_loader,
},
)
else:
self.register_parameter("bias", None)
def weight_loader(
self, param: torch.nn.Parameter, loaded_weight: torch.Tensor
) -> None:
output_dim = getattr(param, "output_dim", None)
if output_dim is not None:
shard_size = param.data.shape[output_dim]
loaded_weight = loaded_weight.narrow(
output_dim, self.tp_rank * shard_size, shard_size
)
if len(loaded_weight.shape) == 0:
loaded_weight = loaded_weight.reshape(1)
assert param.data.shape == loaded_weight.shape
param.data.copy_(loaded_weight)
def forward(self, x: torch.Tensor) -> torch.Tensor:
compute_dtype = self.compute_dtype or x.dtype
output_parallel = _apply_weight_only_fp8_linear(
x,
self.weight,
self.weight_scale,
self.bias,
compute_dtype,
self.enable_fused_w8a8,
)
if self.gather_output:
return tensor_model_parallel_all_gather(
output_parallel, tp_group=self.tp_group
)
return output_parallel
class WeightOnlyFP8MergedColumnParallelLinear(WeightOnlyFP8ColumnParallelLinear):
"""Column-parallel storage-only FP8 packed linear."""
def __init__(
self,
in_features: int,
output_sizes: list[int],
bias: bool = True,
compute_dtype: torch.dtype | None = None,
gather_output: bool = False,
tp_group=None,
enable_fused_w8a8: bool | None = None,
) -> None:
self.output_sizes = output_sizes
super().__init__(
in_features,
sum(output_sizes),
bias=bias,
compute_dtype=compute_dtype,
gather_output=gather_output,
tp_group=tp_group,
enable_fused_w8a8=enable_fused_w8a8,
)
assert all(output_size % self.tp_size == 0 for output_size in output_sizes)
def weight_loader(
self, param: torch.nn.Parameter, loaded_weight: torch.Tensor
) -> None:
output_dim = getattr(param, "output_dim", None)
if output_dim is not None:
shards = []
current_offset = 0
for output_size in self.output_sizes:
loaded_shard = loaded_weight.narrow(
output_dim, current_offset, output_size
)
shard_size = output_size // self.tp_size
loaded_shard = loaded_shard.narrow(
output_dim, self.tp_rank * shard_size, shard_size
)
shards.append(loaded_shard)
current_offset += output_size
loaded_weight = torch.cat(shards, dim=output_dim)
if len(loaded_weight.shape) == 0:
loaded_weight = loaded_weight.reshape(1)
assert param.data.shape == loaded_weight.shape
param.data.copy_(loaded_weight)
class WeightOnlyFP8RowParallelLinear(nn.Module):
"""Row-parallel storage-only e4m3 FP8 linear."""
def __init__(
self,
in_features: int,
out_features: int,
bias: bool = True,
compute_dtype: torch.dtype | None = None,
input_is_parallel: bool = True,
reduce_results: bool = True,
tp_group=None,
enable_fused_w8a8: bool | None = None,
) -> None:
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.compute_dtype = compute_dtype
self.input_is_parallel = input_is_parallel
self.reduce_results = reduce_results
self.enable_fused_w8a8 = _resolve_enable_fused_w8a8(enable_fused_w8a8)
self.tp_group = tp_group or get_tp_group()
self.tp_size = get_group_size(self.tp_group)
self.tp_rank = get_group_rank(self.tp_group)
self.in_features_per_partition = divide(in_features, self.tp_size)
self.weight = nn.Parameter(
torch.empty(
out_features,
self.in_features_per_partition,
dtype=FP8_WEIGHT_DTYPE,
),
requires_grad=False,
)
set_weight_attrs(
self.weight,
{
"input_dim": 1,
"weight_loader": self.weight_loader,
},
)
self.weight_scale = nn.Parameter(
torch.empty(out_features, dtype=torch.float32),
requires_grad=False,
)
set_weight_attrs(
self.weight_scale,
{
"missing_param_init": "error",
"weight_loader": self.weight_loader,
},
)
if bias:
self.bias = nn.Parameter(
torch.empty(
out_features, dtype=compute_dtype or torch.get_default_dtype()
),
requires_grad=False,
)
set_weight_attrs(self.bias, {"weight_loader": self.weight_loader})
else:
self.register_parameter("bias", None)
def weight_loader(
self, param: torch.nn.Parameter, loaded_weight: torch.Tensor
) -> None:
input_dim = getattr(param, "input_dim", None)
if input_dim is not None:
shard_size = param.data.shape[input_dim]
loaded_weight = loaded_weight.narrow(
input_dim, self.tp_rank * shard_size, shard_size
)
if len(loaded_weight.shape) == 0:
loaded_weight = loaded_weight.reshape(1)
assert param.data.shape == loaded_weight.shape
param.data.copy_(loaded_weight)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.input_is_parallel:
input_parallel = x
else:
input_parallel = split_tensor_along_last_dim(
x, num_partitions=self.tp_size
)[self.tp_rank].contiguous()
compute_dtype = self.compute_dtype or x.dtype
bias = None if self.tp_rank > 0 else self.bias
output_parallel = _apply_weight_only_fp8_linear(
input_parallel,
self.weight,
self.weight_scale,
bias,
compute_dtype,
self.enable_fused_w8a8,
)
if self.reduce_results and self.tp_size > 1:
return tensor_model_parallel_all_reduce(
output_parallel, tp_group=self.tp_group
)
return output_parallel
def _resolve_enable_fused_w8a8(value: bool | None) -> bool:
if value is not None:
return value
return envs.SGLANG_DIFFUSION_ENABLE_W8A8_FP8_GEMM
def _log_w8a8_fp8_gemm_warning_once() -> None:
global _w8a8_fp8_gemm_warning_logged
if _w8a8_fp8_gemm_warning_logged:
return
logger.warning(
"%s=1 enables W8A8 FP8 GEMM for weight-only FP8 linears; activations "
"are dynamically quantized to FP8 and outputs may differ from the "
"official weight-only FP8 path.",
W8A8_FP8_GEMM_ENV,
)
_w8a8_fp8_gemm_warning_logged = True
def swap_linears_to_weight_only_fp8(module: nn.Module) -> None:
"""Recursively replace nn.Linear with WeightOnlyFP8Linear.
Ideogram FP8 checkpoints provide ``<linear>.weight_scale`` for every
quantized linear. Swapping before load lets strict state-dict checks verify
both the FP8 weight and its row-wise scale.
"""
for name, child in list(module.named_children()):
if isinstance(child, nn.Linear):
replacement = WeightOnlyFP8Linear(
child.in_features,
child.out_features,
bias=child.bias is not None,
compute_dtype=child.weight.dtype,
)
setattr(module, name, replacement)
else:
swap_linears_to_weight_only_fp8(child)
@@ -0,0 +1,54 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/model_executor/layers/rotary_embedding.py
# Adapted from
# https://github.com/huggingface/transformers/blob/v4.33.2/src/transformers/models/llama/modeling_llama.py
# Copyright 2023 The vLLM team.
# Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved.
#
# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
# and OPT implementations in this library. It has been modified from its
# original forms to accommodate minor architectural differences compared
# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Rotary Positional Embeddings — unified public API (drop-in replacement)."""
from .base import RotaryEmbedding
from .factory import get_rope, get_rotary_pos_embed
from .mrope import (
NDRotaryEmbedding,
Qwen3VLTextRotaryEmbedding,
qwen3_apply_rotary_pos_emb,
)
from .utils import (
_apply_rotary_emb,
apply_flashinfer_rope_qk_inplace,
)
__all__ = [
# _utils
"_apply_rotary_emb",
"apply_flashinfer_rope_qk_inplace",
# _base
"RotaryEmbedding",
# _mrope
"NDRotaryEmbedding",
"Qwen3VLTextRotaryEmbedding",
"qwen3_apply_rotary_pos_emb",
# _factory
"get_rope",
"get_rotary_pos_embed",
]
@@ -0,0 +1,133 @@
"""RotaryEmbedding base class and LinearScalingRotaryEmbedding variant."""
import torch
from sglang.multimodal_gen.runtime.layers.custom_op import CustomOp
from .utils import _apply_rotary_emb
@CustomOp.register("rotary_embedding")
class RotaryEmbedding(CustomOp):
"""Original rotary positional embedding."""
def __init__(
self,
head_size: int,
rotary_dim: int,
max_position_embeddings: int,
base: int | float,
is_neox_style: bool,
dtype: torch.dtype,
) -> None:
super().__init__()
self.head_size = head_size
self.rotary_dim = rotary_dim
self.max_position_embeddings = max_position_embeddings
self.base = base
self.is_neox_style = is_neox_style
self.dtype = dtype
cache = self._compute_cos_sin_cache()
cache = cache.to(dtype)
self.cos_sin_cache: torch.Tensor
self.register_buffer("cos_sin_cache", cache, persistent=False)
def _compute_inv_freq(self, base: int | float) -> torch.Tensor:
"""Compute the inverse frequency."""
# NOTE(woosuk): To exactly match the HF implementation, we need to
# use CPU to compute the cache and then move it to GPU. However, we
# create the cache on GPU for faster initialization. This may cause
# a slight numerical difference between the HF implementation and ours.
inv_freq = 1.0 / (
base
** (
torch.arange(0, self.rotary_dim, 2, dtype=torch.float) / self.rotary_dim
)
)
return inv_freq
def _compute_cos_sin_cache(self) -> torch.Tensor:
"""Compute the cos and sin cache."""
inv_freq = self._compute_inv_freq(self.base)
t = torch.arange(self.max_position_embeddings, dtype=torch.float)
freqs = torch.einsum("i,j -> ij", t, inv_freq)
cos = freqs.cos()
sin = freqs.sin()
cache = torch.cat((cos, sin), dim=-1)
return cache
def forward_cuda(self, *args, **kwargs):
return self.forward_native(*args, **kwargs)
def forward_xpu(self, *args, **kwargs):
return self.forward_native(*args, **kwargs)
def forward_native(
self,
positions: torch.Tensor,
query: torch.Tensor,
key: torch.Tensor,
offsets: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""A PyTorch-native implementation of forward()."""
if offsets is not None:
positions = positions + offsets
positions = positions.flatten()
num_tokens = positions.shape[0]
cos_sin = self.cos_sin_cache.index_select(0, positions)
cos, sin = cos_sin.chunk(2, dim=-1)
query_shape = query.shape
query = query.reshape(num_tokens, -1, self.head_size)
query_rot = query[..., : self.rotary_dim]
query_pass = query[..., self.rotary_dim :]
query_rot = _apply_rotary_emb(query_rot, cos, sin, self.is_neox_style)
query = torch.cat((query_rot, query_pass), dim=-1).reshape(query_shape)
key_shape = key.shape
key = key.reshape(num_tokens, -1, self.head_size)
key_rot = key[..., : self.rotary_dim]
key_pass = key[..., self.rotary_dim :]
key_rot = _apply_rotary_emb(key_rot, cos, sin, self.is_neox_style)
key = torch.cat((key_rot, key_pass), dim=-1).reshape(key_shape)
return query, key
def extra_repr(self) -> str:
s = f"head_size={self.head_size}, rotary_dim={self.rotary_dim}"
s += f", max_position_embeddings={self.max_position_embeddings}"
s += f", base={self.base}, is_neox_style={self.is_neox_style}"
return s
class LinearScalingRotaryEmbedding(RotaryEmbedding):
def __init__(
self,
head_size: int,
rotary_dim: int,
max_position_embeddings: int,
base: int | float,
is_neox_style: bool,
dtype: torch.dtype,
scaling_factor: float,
) -> None:
self.scaling_factor = float(scaling_factor)
super().__init__(
head_size=head_size,
rotary_dim=rotary_dim,
max_position_embeddings=max_position_embeddings,
base=base,
is_neox_style=is_neox_style,
dtype=dtype,
)
def _compute_cos_sin_cache(self) -> torch.Tensor:
inv_freq = self._compute_inv_freq(self.base)
t = torch.arange(self.max_position_embeddings, dtype=torch.float)
t = t / self.scaling_factor
freqs = torch.einsum("i,j -> ij", t, inv_freq)
cos = freqs.cos()
sin = freqs.sin()
cache = torch.cat((cos, sin), dim=-1)
return cache
@@ -0,0 +1,171 @@
"""get_rope / get_rotary_pos_embed factory functions and module-level caches."""
from collections import OrderedDict
from typing import Any
import torch
from .base import LinearScalingRotaryEmbedding, RotaryEmbedding
from .mrope import NDRotaryEmbedding, _to_tuple
_ROPE_DICT: dict[tuple, RotaryEmbedding] = {}
_ND_ROPE_CACHE: "OrderedDict[tuple, NDRotaryEmbedding]" = OrderedDict()
_ROPE_3D_CACHE: "OrderedDict[tuple, tuple[torch.Tensor, torch.Tensor]]" = OrderedDict()
def get_rope(
head_size: int,
rotary_dim: int,
max_position: int,
base: int | float,
is_neox_style: bool = True,
rope_scaling: dict[str, Any] | None = None,
dtype: torch.dtype | None = None,
partial_rotary_factor: float = 1.0,
) -> RotaryEmbedding:
if dtype is None:
dtype = torch.get_default_dtype()
if rope_scaling is not None:
# Transforms every value that is a list into a tuple for caching calls
rope_scaling_tuple = {
k: tuple(v) if isinstance(v, list) else v for k, v in rope_scaling.items()
}
rope_scaling_args = tuple(rope_scaling_tuple.items())
else:
rope_scaling_args = None
if partial_rotary_factor < 1.0:
rotary_dim = int(rotary_dim * partial_rotary_factor)
max_position_embeddings = max_position
rope_type = None
if rope_scaling is not None:
rope_type = rope_scaling.get("rope_type", rope_scaling.get("type", None))
if rope_type in (None, "default"):
rope_scaling = None
elif rope_type == "linear":
factor = float(rope_scaling.get("factor", 1.0))
original_max = rope_scaling.get("original_max_position_embeddings", None)
if original_max is not None:
max_position_embeddings = max(
max_position_embeddings, int(float(original_max) * factor)
)
key = (
head_size,
rotary_dim,
max_position_embeddings,
base,
is_neox_style,
rope_scaling_args,
dtype,
)
if key in _ROPE_DICT:
return _ROPE_DICT[key]
if rope_scaling is None:
rotary_emb = RotaryEmbedding(
head_size, rotary_dim, max_position_embeddings, base, is_neox_style, dtype
)
else:
if rope_type == "linear":
factor = float(rope_scaling.get("factor", 1.0))
rotary_emb = LinearScalingRotaryEmbedding(
head_size=head_size,
rotary_dim=rotary_dim,
max_position_embeddings=max_position_embeddings,
base=base,
is_neox_style=is_neox_style,
dtype=dtype,
scaling_factor=factor,
)
else:
raise ValueError(f"Unknown RoPE scaling {rope_scaling}")
_ROPE_DICT[key] = rotary_emb
return rotary_emb
def get_rotary_pos_embed(
rope_sizes,
hidden_size,
heads_num,
rope_dim_list,
rope_theta,
theta_rescale_factor=1.0,
interpolation_factor=1.0,
shard_dim: int = 0,
dtype: torch.dtype = torch.float32,
start_frame: int = 0,
device: torch.device | str | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Generate rotary positional embeddings for the given sizes.
Args:
rope_sizes: Tuple of dimensions (t, h, w)
hidden_size: Hidden dimension size
heads_num: Number of attention heads
rope_dim_list: List of dimensions for each axis, or None
rope_theta: Base for frequency calculations
theta_rescale_factor: Rescale factor for theta. Defaults to 1.0
interpolation_factor: Factor to scale positions. Defaults to 1.0
shard_dim: Which dimension to shard for sequence parallelism. Defaults to 0.
Returns:
Tuple of (cos, sin) tensors for rotary embeddings
"""
target_ndim = 3
head_dim = hidden_size // heads_num
if rope_dim_list is None:
rope_dim_list = [head_dim // target_ndim for _ in range(target_ndim)]
assert (
sum(rope_dim_list) == head_dim
), "sum(rope_dim_list) should equal to head_dim of attention layer"
# Get SP info - now handled within NDRotaryEmbedding
# sp_group = get_sp_group()
# sp_rank = sp_group.rank_in_group
# sp_world_size = sp_group.world_size
# Simple LRU cache keyed by parameters
global _ND_ROPE_CACHE
key = (
tuple(rope_dim_list),
float(rope_theta),
(
tuple(theta_rescale_factor)
if isinstance(theta_rescale_factor, list)
else float(theta_rescale_factor)
),
(
tuple(interpolation_factor)
if isinstance(interpolation_factor, list)
else float(interpolation_factor)
),
dtype,
)
cache_hit = key in _ND_ROPE_CACHE
if cache_hit:
rope_emb = _ND_ROPE_CACHE.pop(key)
_ND_ROPE_CACHE[key] = rope_emb # move to end (most-recent)
else:
rope_emb = NDRotaryEmbedding(
rope_dim_list=rope_dim_list,
rope_theta=rope_theta,
theta_rescale_factor=theta_rescale_factor,
interpolation_factor=interpolation_factor,
dtype=dtype,
)
_ND_ROPE_CACHE[key] = rope_emb
if len(_ND_ROPE_CACHE) > 16:
# pop least-recently-used
_ND_ROPE_CACHE.pop(next(iter(_ND_ROPE_CACHE)))
freqs_cos, freqs_sin = rope_emb.forward_from_grid(
grid_size=_to_tuple(rope_sizes, dim=3),
shard_dim=shard_dim,
start_frame=start_frame,
device=device,
)
return freqs_cos, freqs_sin
@@ -0,0 +1,502 @@
"""MRotaryEmbedding, YaRNScalingMRotaryEmbedding, NDRotaryEmbedding, OneDRotaryEmbedding."""
import functools
import torch
from sglang.multimodal_gen.runtime.distributed.parallel_state import get_sp_group
def _to_tuple(x: int | tuple[int, ...], dim: int = 2) -> tuple[int, ...]:
if isinstance(x, int):
return (x,) * dim
elif len(x) == dim:
return x
else:
raise ValueError(f"Expected length {dim} or int, but got {x}")
def get_1d_rotary_pos_embed(
dim: int,
pos: torch.FloatTensor | int,
theta: float = 10000.0,
theta_rescale_factor: float = 1.0,
interpolation_factor: float = 1.0,
dtype: torch.dtype = torch.float32,
device: torch.device | str | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Precompute the frequency tensor for complex exponential (cis) with given dimensions.
(Note: `cis` means `cos + i * sin`, where i is the imaginary unit.)
This function calculates a frequency tensor with complex exponential using the given dimension 'dim'
and the end index 'end'. The 'theta' parameter scales the frequencies.
Args:
dim (int): Dimension of the frequency tensor.
pos (int or torch.FloatTensor): Position indices for the frequency tensor. [S] or scalar
theta (float, optional): Scaling factor for frequency computation. Defaults to 10000.0.
theta_rescale_factor (float, optional): Rescale factor for theta. Defaults to 1.0.
interpolation_factor (float, optional): Factor to scale positions. Defaults to 1.0.
Returns:
freqs_cos, freqs_sin: Precomputed frequency tensor with real and imaginary parts separately. [S, D]
"""
if isinstance(pos, int):
pos = torch.arange(pos, dtype=dtype, device=device)
elif (
isinstance(pos, torch.Tensor)
and device is not None
and pos.device != torch.device(device)
):
# Ensure positions are on the requested device to avoid implicit CPU ops.
pos = pos.to(device)
# proposed by reddit user bloc97, to rescale rotary embeddings to longer sequence length without fine-tuning
# has some connection to NTK literature
if theta_rescale_factor != 1.0:
theta *= theta_rescale_factor ** (dim / (dim - 2))
freqs = 1.0 / (
theta
** (torch.arange(0, dim, 2, device=device)[: (dim // 2)].to(dtype) / dim).to(
device=device
)
) # [D/2]
freqs = torch.outer(pos * interpolation_factor, freqs) # [S, D/2]
freqs_cos = freqs.cos() # [S, D/2]
freqs_sin = freqs.sin() # [S, D/2]
return freqs_cos, freqs_sin
def qwen3_apply_rotary_pos_emb(
q: torch.Tensor,
k: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Apply Qwen3-style RoPE to q/k tensors shaped [B, S, H, D]."""
half = q.shape[-1] // 2
q1 = q[..., :half]
q2 = q[..., half:]
q_embed = torch.empty_like(q)
q_embed[..., :half] = q1 * cos[..., :half] - q2 * sin[..., :half]
q_embed[..., half:] = q2 * cos[..., half:] + q1 * sin[..., half:]
half = k.shape[-1] // 2
k1 = k[..., :half]
k2 = k[..., half:]
k_embed = torch.empty_like(k)
k_embed[..., :half] = k1 * cos[..., :half] - k2 * sin[..., :half]
k_embed[..., half:] = k2 * cos[..., half:] + k1 * sin[..., half:]
return q_embed, k_embed
class Qwen3VLTextRotaryEmbedding(torch.nn.Module):
"""Qwen3-VL multi-dimensional rotary embedding with interleaved mRoPE."""
def __init__(
self,
head_dim: int = 128,
rope_theta: float = 5_000_000.0,
mrope_section: tuple[int, int, int] | list[int] = (24, 20, 20),
):
super().__init__()
self.rope_type = "default"
self.max_seq_len_cached = 262144
self.mrope_section = list(mrope_section)
self.head_dim = head_dim
inv_freq = 1.0 / (
rope_theta ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim)
)
self.register_buffer("inv_freq", inv_freq, persistent=False)
self.attention_scaling = 1.0
def apply_interleaved_mrope(
self, freqs: torch.Tensor, mrope_section: list[int]
) -> torch.Tensor:
freqs_t = freqs[0].clone()
for dim, offset in enumerate((1, 2), start=1):
length = mrope_section[dim] * 3
idx = slice(offset, length, 3)
freqs_t[..., idx] = freqs[dim, ..., idx]
return freqs_t
def _normalize_position_ids(self, position_ids: torch.Tensor) -> torch.Tensor:
if position_ids.ndim == 3 and position_ids.shape[-1] == 3:
position_ids = position_ids.permute(2, 0, 1)
elif position_ids.ndim == 2:
position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1)
elif position_ids.ndim != 3 or position_ids.shape[0] != 3:
raise ValueError(
"Qwen3 mRoPE position_ids must have shape [3, B, S], [B, S, 3], "
f"or [B, S], got {tuple(position_ids.shape)}"
)
return position_ids
def _compute_interleaved_freqs(self, position_ids: torch.Tensor) -> torch.Tensor:
position_ids = self._normalize_position_ids(position_ids)
inv_freq_expanded = (
self.inv_freq[None, None, :, None]
.float()
.expand(3, position_ids.shape[1], -1, 1)
.to(position_ids.device)
)
position_ids_expanded = position_ids[:, :, None, :].float()
freqs = (inv_freq_expanded @ position_ids_expanded).transpose(2, 3)
return self.apply_interleaved_mrope(freqs, self.mrope_section)
@torch.no_grad()
def build_rope_cache_inputs(
self, position_ids: torch.Tensor, *, cache_dtype: torch.dtype | None = None
) -> tuple[torch.Tensor, torch.Tensor]:
freqs = self._compute_interleaved_freqs(position_ids)
cos = freqs.cos() * self.attention_scaling
sin = freqs.sin() * self.attention_scaling
if cache_dtype is not None and cache_dtype != torch.float32:
cos = cos.to(cache_dtype).float()
sin = sin.to(cache_dtype).float()
cos_sin_cache = torch.cat((cos, sin), dim=-1).reshape(-1, self.head_dim)
cos_sin_cache = cos_sin_cache.contiguous()
cache_positions = torch.arange(
cos_sin_cache.shape[0], device=cos_sin_cache.device, dtype=torch.long
)
return cos_sin_cache, cache_positions
@torch.no_grad()
def forward(
self, x: torch.Tensor, position_ids: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
"""Return cos/sin for position IDs shaped [3, B, S], [B, S, 3], or [B, S]."""
freqs = self._compute_interleaved_freqs(position_ids)
emb = torch.cat((freqs, freqs), dim=-1)
cos = emb.cos() * self.attention_scaling
sin = emb.sin() * self.attention_scaling
return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
class OneDRotaryEmbedding(torch.nn.Module):
"""1D rotary positional embedding with caching."""
def __init__(
self,
dim: int,
theta: float = 10000.0,
theta_rescale_factor: float = 1.0,
interpolation_factor: float = 1.0,
dtype: torch.dtype = torch.float32,
use_real: bool = False,
repeat_interleave_real: bool = False,
):
super().__init__()
assert dim % 2 == 0
self.dim = dim
self.theta = theta
self.theta_rescale_factor = theta_rescale_factor
self.interpolation_factor = interpolation_factor
# dtype of freqs
self.dtype = dtype
self.use_real = use_real
self.repeat_interleave_real = repeat_interleave_real
def build_freqs(self, device):
freqs = 1.0 / (
self.theta
** (
torch.arange(0, self.dim, 2, dtype=self.dtype, device=device)[
: (self.dim // 2)
]
/ self.dim
).to(device=device)
)
return freqs
def build_freqs_outer(self, pos: torch.Tensor, device):
theta = self.theta
# proposed by reddit user bloc97, to rescale rotary embeddings to longer sequence length without fine-tuning
# has some connection to NTK literature
if self.theta_rescale_factor != 1.0:
theta *= self.theta_rescale_factor ** (self.dim / (self.dim - 2))
freqs = self.build_freqs(device)
freqs = torch.outer(pos * self.interpolation_factor, freqs)
freqs_cos = freqs.cos()
freqs_sin = freqs.sin()
if self.use_real and self.repeat_interleave_real:
freqs_cos = freqs_cos.repeat_interleave(2, dim=1)
freqs_sin = freqs_sin.repeat_interleave(2, dim=1)
return freqs_cos.float(), freqs_sin.float()
@functools.lru_cache(maxsize=16)
def forward_from_grid(
self, seq_len: int, start_pos: int, device_str: str
) -> tuple[torch.Tensor, torch.Tensor]:
device = torch.device(device_str)
pos = torch.arange(
start_pos, start_pos + seq_len, dtype=self.dtype, device=device
)
freqs_cos, freqs_sin = self.build_freqs_outer(pos, device)
return freqs_cos, freqs_sin
def forward(self, pos: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""
Calculates 1D rotary embeddings for the given positions.
This method converts the input tensor to a hashable representation
and calls a cached helper method to perform the computation.
"""
pos_tuple = tuple(pos.tolist())
device_str = str(pos.device)
return self._forward_cached(pos_tuple, device_str)
@functools.lru_cache(maxsize=16)
def _forward_cached(
self, pos_tuple: tuple, device_str: str
) -> tuple[torch.Tensor, torch.Tensor]:
"""
The core implementation that computes 1D rotary embeddings.
This method is wrapped by an LRU cache.
"""
device = torch.device(device_str)
pos = torch.as_tensor(pos_tuple, dtype=self.dtype, device=device)
freqs_cos, freqs_sin = self.build_freqs_outer(pos, device)
return freqs_cos, freqs_sin
class NDRotaryEmbedding(torch.nn.Module):
"""N-dimensional rotary positional embedding."""
def __init__(
self,
rope_dim_list: list[int],
rope_theta: float,
theta_rescale_factor: float | list[float] = 1.0,
interpolation_factor: float | list[float] = 1.0,
use_real: bool = False,
repeat_interleave_real: bool = False,
dtype: torch.dtype = torch.float32,
):
super().__init__()
self.rope_dim_list = rope_dim_list
self.ndim = len(rope_dim_list)
self.rope_theta = rope_theta
# dtype of freqs
# does not control the output dtype
self.dtype = dtype
if isinstance(theta_rescale_factor, (int, float)):
self.theta_rescale_factor = [theta_rescale_factor] * self.ndim
elif isinstance(theta_rescale_factor, list) and len(theta_rescale_factor) == 1:
self.theta_rescale_factor = [theta_rescale_factor[0]] * self.ndim
else:
self.theta_rescale_factor = theta_rescale_factor
assert (
len(self.theta_rescale_factor) == self.ndim
), "len(theta_rescale_factor) should equal to len(rope_dim_list)"
if isinstance(interpolation_factor, (int, float)):
self.interpolation_factor = [interpolation_factor] * self.ndim
elif isinstance(interpolation_factor, list) and len(interpolation_factor) == 1:
self.interpolation_factor = [interpolation_factor[0]] * self.ndim
else:
self.interpolation_factor = interpolation_factor
assert (
len(self.interpolation_factor) == self.ndim
), "len(interpolation_factor) should equal to len(rope_dim_list)"
self.rope_generators: list[OneDRotaryEmbedding] = torch.nn.ModuleList()
_config_to_gen_idx: dict[tuple, int] = {}
self.dim_idx_to_gen_idx: list[int] = []
for i in range(self.ndim):
dim = self.rope_dim_list[i]
rescale = self.theta_rescale_factor[i]
interp = self.interpolation_factor[i]
config_key = (dim, rescale, interp, use_real, repeat_interleave_real)
if config_key not in _config_to_gen_idx:
generator = OneDRotaryEmbedding(
dim=dim,
theta=self.rope_theta,
theta_rescale_factor=rescale,
interpolation_factor=interp,
dtype=self.dtype,
use_real=use_real,
repeat_interleave_real=repeat_interleave_real,
)
_config_to_gen_idx[config_key] = len(self.rope_generators)
self.rope_generators.append(generator)
gen_idx = _config_to_gen_idx[config_key]
self.dim_idx_to_gen_idx.append(gen_idx)
def forward(self, positions: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""
Calculates n-d rotary embeddings for given absolute positions.
Args:
positions (torch.Tensor): A tensor of shape `[num_tokens, ndim]`
containing the integer coordinates for each token.
Returns:
A tuple of (cos, sin) tensors.
"""
# Caching wrapper: convert tensor to a hashable tuple of tuples.
pos_tuple = tuple(map(tuple, positions.tolist()))
device_str = str(positions.device)
return self._forward_cached(pos_tuple, device_str)
@functools.lru_cache(maxsize=16)
def _forward_cached(
self, pos_tuple: tuple[tuple[int, ...], ...], device_str: str
) -> tuple[torch.Tensor, torch.Tensor]:
"""
The core implementation that computes embeddings from a position tensor.
This method is wrapped by an LRU cache.
"""
device = torch.device(device_str)
positions = torch.tensor(pos_tuple, dtype=torch.long, device=device)
return self.forward_uncached(pos=positions)
def forward_uncached(self, pos: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""
The core implementation that computes embeddings from a position tensor.
This method is wrapped by an LRU cache.
"""
device = pos.device
# Pre-allocate the final tensors for efficiency.
num_tokens = pos.shape[0]
first_generator = self.rope_generators[0]
if first_generator.use_real and first_generator.repeat_interleave_real:
head_dim = sum(self.rope_dim_list)
else:
head_dim = sum(self.rope_dim_list) // 2
cos = torch.empty((num_tokens, head_dim), device=device, dtype=self.dtype)
sin = torch.empty((num_tokens, head_dim), device=device, dtype=self.dtype)
col_offset = 0
for i in range(self.ndim):
# Extract position coordinates for the current dimension for all tokens.
pos_i = pos[:, i].to(self.dtype)
# Get the appropriate 1D generator.
gen_idx = self.dim_idx_to_gen_idx[i]
generator = self.rope_generators[gen_idx]
# Calculate 1D embeddings.
cos_1d, sin_1d = generator(pos_i)
slice_width = cos_1d.shape[1]
cos[:, col_offset : col_offset + slice_width] = cos_1d
sin[:, col_offset : col_offset + slice_width] = sin_1d
col_offset += slice_width
return cos.float(), sin.float()
def forward_from_grid(
self,
grid_size: tuple[int, ...],
shard_dim: int = 0,
start_frame: int = 0,
device: torch.device | str | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Handles sp internally
"""
# Caching wrapper: use grid parameters directly as the key.
# grid_tuple = _to_tuple(grid_size, dim=self.ndim)
device_str = str(device) if device is not None else "cpu"
return self._forward_cached_from_grid(
grid_size, shard_dim, start_frame, device_str
)
@functools.lru_cache(maxsize=16)
def _forward_cached_from_grid(
self,
grid_size: tuple[int, ...],
shard_dim: int,
start_frame: int,
device_str: str,
) -> tuple[torch.Tensor, torch.Tensor]:
"""
Computes embeddings for a structured grid, using a highly efficient
implementation that avoids materializing the full position tensor.
This method is wrapped by an LRU cache.
"""
device = torch.device(device_str)
sp_group = get_sp_group()
sp_rank = sp_group.rank_in_group
sp_world_size = sp_group.world_size
sizes = _to_tuple(grid_size, dim=self.ndim)
starts = (0,) * self.ndim
# Apply sequence parallel sharding to the sizes and compute shard offset
shard_sizes = list(sizes)
shard_offsets = [0] * self.ndim
if sp_world_size > 1:
assert sizes[shard_dim] % sp_world_size == 0, (
f"Dimension {shard_dim} with size {sizes[shard_dim]} is not divisible "
f"by sequence parallel world size {sp_world_size}"
)
shard_size = sizes[shard_dim] // sp_world_size
shard_offsets[shard_dim] = sp_rank * shard_size
shard_sizes[shard_dim] = shard_size
# Pre-allocate outputs on the requested device to avoid CPU ops and extra cats
num_tokens = 1
for s in shard_sizes:
num_tokens *= int(s)
head_dim_half = sum(self.rope_dim_list) // 2
cos = torch.empty((num_tokens, head_dim_half), device=device, dtype=self.dtype)
sin = torch.empty((num_tokens, head_dim_half), device=device, dtype=self.dtype)
# Compute per-axis 1D embeddings once and expand via repeats to [N, d_i/2]
col_offset = 0
for i in range(self.ndim):
dim_i = self.rope_dim_list[i]
dim_i_half = dim_i // 2
size_i = int(shard_sizes[i])
# Starting position for this axis, with optional frame offset for time axis (i==0)
base_offset = starts[i]
if i == 0 and start_frame > 0:
base_offset += start_frame
if sp_world_size > 1 and i == shard_dim:
base_offset += shard_offsets[i]
gen_idx = self.dim_idx_to_gen_idx[i]
generator = self.rope_generators[gen_idx]
cos_1d, sin_1d = generator.forward_from_grid(
size_i, base_offset, device_str
)
# Expand to [num_tokens, dim_i/2] matching flatten order (last dims vary fastest)
repeats_per_entry = 1
for j in range(i + 1, self.ndim):
repeats_per_entry *= int(shard_sizes[j])
tile_count = 1
for j in range(0, i):
tile_count *= int(shard_sizes[j])
cos_expanded = cos_1d.repeat_interleave(repeats_per_entry, dim=0)
sin_expanded = sin_1d.repeat_interleave(repeats_per_entry, dim=0)
if tile_count > 1:
cos_expanded = cos_expanded.repeat(tile_count, 1)
sin_expanded = sin_expanded.repeat(tile_count, 1)
cos[:, col_offset : col_offset + dim_i_half] = cos_expanded
sin[:, col_offset : col_offset + dim_i_half] = sin_expanded
col_offset += dim_i_half
return cos.float(), sin.float()
@@ -0,0 +1,196 @@
"""Primitive RoPE ops: rotate helpers and apply_rotary_emb utilities."""
from typing import Optional, Tuple
import torch
from sglang.jit_kernel.diffusion.triton.rotary import apply_rotary_embedding
from sglang.kernel_api_logging import debug_kernel_api
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.srt.utils.custom_op import register_custom_op_from_extern
logger = init_logger(__name__)
_is_cuda = current_platform.is_cuda()
if _is_cuda:
try:
from flashinfer.rope import (
apply_rope_with_cos_sin_cache_inplace as _flashinfer_apply_rope_inplace,
)
except Exception:
_flashinfer_apply_rope_inplace = None
else:
_flashinfer_apply_rope_inplace = None
if _flashinfer_apply_rope_inplace is not None:
flashinfer_apply_rope_inplace = register_custom_op_from_extern(
_flashinfer_apply_rope_inplace,
op_name="flashinfer_apply_rope_with_cos_sin_cache_inplace",
mutates_args=["query", "key"],
)
else:
flashinfer_apply_rope_inplace = None
def _apply_rotary_emb(
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor,
is_neox_style: bool,
interleaved: bool = False,
) -> torch.Tensor:
"""
Args:
x: [num_tokens, num_heads, head_size] or [num_tokens, head_size]
cos: [num_tokens, head_size // 2]
sin: [num_tokens, head_size // 2]
is_neox_style: Whether to use the Neox-style or GPT-J-style rotary
positional embeddings.
"""
# cos = cos.unsqueeze(-2).to(x.dtype)
# sin = sin.unsqueeze(-2).to(x.dtype)
if is_neox_style:
cos = cos.unsqueeze(-2)
sin = sin.unsqueeze(-2)
if is_neox_style:
x1, x2 = torch.chunk(x, 2, dim=-1)
else:
x1 = x[..., ::2]
x2 = x[..., 1::2]
o1 = (x1.float() * cos - x2.float() * sin).type_as(x)
o2 = (x2.float() * cos + x1.float() * sin).type_as(x)
return torch.cat((o1, o2), dim=-1)
else:
return apply_rotary_embedding(x, cos, sin, interleaved)
@debug_kernel_api
def apply_flashinfer_rope_qk_inplace(
q: torch.Tensor,
k: torch.Tensor,
cos_sin_cache: torch.Tensor,
*,
head_size: Optional[int] = None,
is_neox: bool = False,
positions: Optional[torch.Tensor] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
if q.dim() != 4 or k.dim() != 4:
raise ValueError(
f"Expected q/k to be 4D [bsz, seqlen, nheads, head_size], "
f"got q:{tuple(q.shape)} k:{tuple(k.shape)}"
)
if q.shape[:2] != k.shape[:2] or q.shape[-1] != k.shape[-1]:
raise ValueError(
f"q and k must share batch, sequence, and head size, got {q.shape} vs {k.shape}"
)
if not (isinstance(cos_sin_cache, torch.Tensor) and cos_sin_cache.dim() == 2):
raise ValueError("cos_sin_cache must be a 2D torch.Tensor")
bsz, seqlen, q_heads, d = q.shape
k_heads = k.shape[2]
rope_dim = cos_sin_cache.shape[-1]
if k.device != q.device or cos_sin_cache.device != q.device:
raise ValueError(
"q, k, and cos_sin_cache must be on the same device, "
f"got q={q.device}, k={k.device}, cos_sin_cache={cos_sin_cache.device}"
)
if rope_dim % 2 != 0 or rope_dim > d:
raise ValueError(
f"cos_sin_cache width must be even and <= head_size, got {rope_dim} vs {d}"
)
if head_size is None:
head_size = d
if head_size != d:
raise ValueError(f"head_size mismatch: inferred {d}, but head_size={head_size}")
use_flashinfer = (
flashinfer_apply_rope_inplace is not None
and q.is_cuda
and k.is_cuda
and cos_sin_cache.is_cuda
and q_heads == k_heads
)
if not use_flashinfer:
if flashinfer_apply_rope_inplace is None:
_warn_about_missing_flashinfer()
half_size = rope_dim // 2
if positions is None:
cos = cos_sin_cache[:seqlen, :half_size].to(q.dtype)
sin = cos_sin_cache[:seqlen, half_size:].to(q.dtype)
cos = cos.unsqueeze(0).expand(bsz, -1, -1).reshape(bsz * seqlen, -1)
sin = sin.unsqueeze(0).expand(bsz, -1, -1).reshape(bsz * seqlen, -1)
else:
positions = positions.to(device=q.device, dtype=torch.long).view(-1)
cos = cos_sin_cache[positions, :half_size].to(q.dtype)
sin = cos_sin_cache[positions, half_size:].to(q.dtype)
if current_platform.is_npu():
q_flat = q.reshape(bsz * seqlen, q_heads, d)
k_flat = k.reshape(bsz * seqlen, k_heads, d)
q_rot = apply_rotary_embedding(q_flat, cos, sin, interleaved=not is_neox)
k_rot = apply_rotary_embedding(k_flat, cos, sin, interleaved=not is_neox)
return q_rot.view(bsz, seqlen, q_heads, d), k_rot.view(
bsz, seqlen, k_heads, d
)
def apply_rope_prefix(x: torch.Tensor, num_heads: int) -> torch.Tensor:
x_flat = x.reshape(bsz * seqlen, num_heads, d)
x_rot = x_flat[..., :rope_dim]
out_rot = torch.empty_like(x_rot)
cos_b = cos.unsqueeze(-2)
sin_b = sin.unsqueeze(-2)
if is_neox:
x1, x2 = torch.chunk(x_rot, 2, dim=-1)
out_rot[..., :half_size] = x1 * cos_b - x2 * sin_b
out_rot[..., half_size:] = x2 * cos_b + x1 * sin_b
else:
x1 = x_rot[..., ::2]
x2 = x_rot[..., 1::2]
out_rot[..., ::2] = x1 * cos_b - x2 * sin_b
out_rot[..., 1::2] = x2 * cos_b + x1 * sin_b
if rope_dim == d:
return out_rot.view(bsz, seqlen, num_heads, d)
out = x_flat.clone()
out[..., :rope_dim] = out_rot
return out.view(bsz, seqlen, num_heads, d)
return apply_rope_prefix(q, q_heads), apply_rope_prefix(k, k_heads)
if positions is None:
pos_1d = torch.arange(seqlen, device=q.device, dtype=torch.long)
positions = pos_1d if bsz == 1 else pos_1d.repeat(bsz)
else:
if not (isinstance(positions, torch.Tensor) and positions.dim() == 1):
raise ValueError("positions must be a 1D Tensor")
if positions.numel() != bsz * seqlen:
raise ValueError(
f"positions length must be bsz*seqlen={bsz*seqlen}, got {positions.numel()}"
)
positions = positions.to(device=q.device, dtype=torch.long)
q_flat = q.reshape(bsz * seqlen, q_heads * d).contiguous()
k_flat = k.reshape(bsz * seqlen, k_heads * d).contiguous()
flashinfer_apply_rope_inplace(
positions=positions,
query=q_flat,
key=k_flat,
head_size=d,
cos_sin_cache=cos_sin_cache,
is_neox=is_neox,
)
return q_flat.view(bsz, seqlen, q_heads, d), k_flat.view(bsz, seqlen, k_heads, d)
@torch.compiler.assume_constant_result
def _warn_about_missing_flashinfer():
"""
Function to warn about the missing FlashInfer.
Exists to not cause a graph break during the compilation.
"""
logger.warning_once(
"FlashInfer not available, using Triton fallback for RoPE",
)
@@ -0,0 +1,429 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
import logging
from typing import TYPE_CHECKING
import torch
import torch.distributed as dist
import torch.distributed._functional_collectives as ft_c
from torch.distributed.tensor.experimental._attention import _cp_options
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_sp_group,
get_ulysses_parallel_rank,
get_ulysses_parallel_world_size,
)
from sglang.srt.utils.common import torch_release
_cp_options.enable_load_balance = False
if TYPE_CHECKING:
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import (
AttentionImpl,
)
logger = logging.getLogger(__name__)
def _maybe_wait(tensor: torch.Tensor) -> torch.Tensor:
"""
When tracing the code, the result tensor is not an AsyncCollectiveTensor,
so we cannot call ``wait()``.
"""
if isinstance(tensor, ft_c.AsyncCollectiveTensor):
return tensor.wait()
return tensor
def _usp_all_to_all_single(x: torch.Tensor) -> torch.Tensor:
ulysses_pg = get_sp_group().ulysses_group
assert ulysses_pg is not None, "Ulysses process group is not initialized."
x_shape = x.shape
x = x.flatten().contiguous()
output = torch.empty_like(x)
# USP calls this collective many times per denoising step and waits
# immediately, so avoid the extra wrapper overhead of functional collectives.
torch.distributed.all_to_all_single(output, x, group=ulysses_pg)
return output.reshape(x_shape)
def _usp_all_to_all_single_varlen(
x: torch.Tensor,
output_split_sizes: list[int],
input_split_sizes: list[int],
) -> torch.Tensor:
ulysses_pg = get_sp_group().ulysses_group
assert ulysses_pg is not None, "Ulysses process group is not initialized."
x = x.flatten().contiguous()
output = torch.empty(sum(output_split_sizes), dtype=x.dtype, device=x.device)
dist.all_to_all_single(
output,
x,
output_split_sizes=output_split_sizes,
input_split_sizes=input_split_sizes,
group=ulysses_pg,
)
return output
def _usp_input_all_to_all(x: torch.Tensor, head_dim: int = 1) -> torch.Tensor:
"""
Perform Ulysses-style input all-to-all over the head dimension.
Default layout expects heads at dim=1 and sequence at dim=2:
[b, h, s_local, d] -> [b, h_local, s_global, d]
If heads are at dim=2 (input is [b, s_local, h, d]), set head_dim=2, and the
function returns [b, s_global, h_local, d], preserving the original
head/sequence dim ordering.
Args:
x: A 4D tensor with layout [b, *, *, d] where '*' are sequence and heads
head_dim: Which dimension index corresponds to heads (1 or 2)
Returns:
Tensor with the same dim order as input, with heads sharded and sequence gathered.
"""
world_size = get_ulysses_parallel_world_size()
if world_size <= 1:
return x
assert x.ndim == 4, f"x must have 4 dimensions, got {x.ndim}"
assert head_dim in (1, 2), f"head_dim must be 1 or 2, got {head_dim}"
# Move the dimension to be split (h_global) to dim 0 for all_to_all_single
if head_dim == 1:
b, h_global, s_local, d = x.shape
# Shape transition: [b, h_global, s_local, d] -> [h_global, b, s_local, d]
permute_order = (1, 0, 2, 3)
else: # head_dim == 2
b, s_local, h_global, d = x.shape
# Shape transition: [b, s_local, h_global, d] -> [h_global, b, s_local, d]
permute_order = (2, 0, 1, 3)
assert (
h_global % world_size == 0
), f"h_global ({h_global}) must be divisible by world_size ({world_size})"
h_local, s_global = h_global // world_size, s_local * world_size
x = x.permute(permute_order).contiguous()
x = _usp_all_to_all_single(x)
x = x.reshape(world_size, h_local, b, s_local, d)
# Reorder dims to place 'world_size' adjacent to 's_local' to merge them into 's_global'
if head_dim == 1:
# Shape transition: [world_size, h_local, b, s_local, d] -> [b, h_local, world_size, s_local, d]
x = x.permute(2, 1, 0, 3, 4).contiguous().reshape(b, h_local, s_global, d)
else: # head_dim == 2
# Shape transition: [world_size, h_local, b, s_local, d] -> [b, world_size, s_local, h_local, d]
x = x.permute(2, 0, 3, 1, 4).contiguous().reshape(b, s_global, h_local, d)
return x
def _usp_input_all_to_all_varlen(
x: torch.Tensor, seq_lens: list[int], head_dim: int = 1
) -> torch.Tensor:
"""
Perform Ulysses-style input all-to-all over the head dimension with variable
local sequence lengths.
Default layout expects heads at dim=1 and sequence at dim=2:
[b, h, s_local, d] -> [b, h_local, s_global, d]
If heads are at dim=2 (input is [b, s_local, h, d]), set head_dim=2, and the
function returns [b, s_global, h_local, d], preserving the original
head/sequence dim ordering.
Args:
x: A 4D tensor with layout [b, *, *, d] where '*' are sequence and heads
seq_lens: Local sequence lengths for each rank in the Ulysses group
head_dim: Which dimension index corresponds to heads (1 or 2)
Returns:
Tensor with the same dim order as input, with heads sharded and sequence gathered.
"""
world_size = get_ulysses_parallel_world_size()
if world_size <= 1:
return x
assert x.ndim == 4, f"x must have 4 dimensions, got {x.ndim}"
assert head_dim in (1, 2), f"head_dim must be 1 or 2, got {head_dim}"
assert (
len(seq_lens) == world_size
), f"seq_lens must have length {world_size}, got {len(seq_lens)}"
rank = get_ulysses_parallel_rank()
# Move the dimension to be split (h_global) to dim 0 for all_to_all_single
if head_dim == 1:
b, h_global, s_local, d = x.shape
# Shape transition: [b, h_global, s_local, d] -> [h_global, b, s_local, d]
permute_order = (1, 0, 2, 3)
else: # head_dim == 2
b, s_local, h_global, d = x.shape
# Shape transition: [b, s_local, h_global, d] -> [h_global, b, s_local, d]
permute_order = (2, 0, 1, 3)
assert (
s_local == seq_lens[rank]
), f"s_local ({s_local}) must equal seq_lens[{rank}] ({seq_lens[rank]})"
assert (
h_global % world_size == 0
), f"h_global ({h_global}) must be divisible by world_size ({world_size})"
h_local = h_global // world_size
x = x.permute(permute_order).contiguous()
x = x.reshape(world_size, h_local, b, s_local, d)
input_split_sizes = [h_local * b * s_local * d] * world_size
output_split_sizes = [h_local * b * seq_len * d for seq_len in seq_lens]
x = _usp_all_to_all_single_varlen(x, output_split_sizes, input_split_sizes)
chunks = []
offset = 0
for seq_len, split_size in zip(seq_lens, output_split_sizes):
chunk = x[offset : offset + split_size].reshape(h_local, b, seq_len, d)
chunks.append(chunk)
offset += split_size
x = torch.cat(chunks, dim=2)
if head_dim == 1:
# Shape transition: [h_local, b, s_global, d] -> [b, h_local, s_global, d]
x = x.permute(1, 0, 2, 3).contiguous()
else: # head_dim == 2
# Shape transition: [h_local, b, s_global, d] -> [b, s_global, h_local, d]
x = x.permute(1, 2, 0, 3).contiguous()
return x
def _usp_output_all_to_all(x: torch.Tensor, head_dim: int = 1) -> torch.Tensor:
"""
Perform Ulysses-style output all-to-all over the head dimension (inverse of input).
Default layout expects heads at dim=1 and sequence at dim=2:
[b, h_local, s, d] -> [b, h, s_local, d]
If heads are at dim=2 (input is [b, s_global, h // world_size, d]), set head_dim=2,
and the function returns [b, s_local, h, d], preserving the original head/sequence
dim ordering.
Args:
x: A 4D tensor with layout [b, *, *, d] where '*' are sequence and heads
head_dim: Which dimension index corresponds to heads (1 or 2)
Returns:
Tensor with the same dim order as input, with heads gathered and sequence sharded.
"""
world_size = get_ulysses_parallel_world_size()
if world_size <= 1:
return x
assert x.ndim == 4, f"x must have 4 dimensions, got {x.ndim}"
assert head_dim in (1, 2), f"head_dim must be 1 or 2, got {head_dim}"
# Move the dimension to be split (s_global) to dim 0 for all_to_all_single
if head_dim == 1:
b, h_local, s_global, d = x.shape
# Shape transition: [b, h_local, s_global, d] -> [s_global, b, h_local, d]
permute_order = (2, 0, 1, 3)
else: # head_dim == 2
b, s_global, h_local, d = x.shape
# Shape transition: [b, s_global, h_local, d] -> [s_global, b, h_local, d]
permute_order = (1, 0, 2, 3)
assert (
s_global % world_size == 0
), f"s_global ({s_global}) must be divisible by world_size ({world_size})"
s_local, h_global = s_global // world_size, h_local * world_size
x = x.permute(permute_order).contiguous()
x = _usp_all_to_all_single(x)
x = x.reshape(world_size, s_local, b, h_local, d)
# Reorder dims to place 'world_size' adjacent to 'h_local' to merge them into 'h_global'
if head_dim == 1:
# Shape transition: [world_size, s_local, b, h_local, d] -> [b, world_size, h_local, s_local, d]
x = x.permute(2, 0, 3, 1, 4).contiguous().reshape(b, h_global, s_local, d)
else: # head_dim == 2
# Shape transition: [world_size, s_local, b, h_local, d] -> [b, s_local, world_size, h_local, d]
x = x.permute(2, 1, 0, 3, 4).contiguous().reshape(b, s_local, h_global, d)
return x
def _usp_output_all_to_all_varlen(
x: torch.Tensor, seq_lens: list[int], head_dim: int = 1
) -> torch.Tensor:
"""
Perform Ulysses-style output all-to-all over the head dimension (inverse of input)
with variable local sequence lengths.
Default layout expects heads at dim=1 and sequence at dim=2:
[b, h_local, s, d] -> [b, h, s_local, d]
If heads are at dim=2 (input is [b, s_global, h // world_size, d]), set head_dim=2,
and the function returns [b, s_local, h, d], preserving the original head/sequence
dim ordering.
Args:
x: A 4D tensor with layout [b, *, *, d] where '*' are sequence and heads
seq_lens: Local sequence lengths for each rank in the Ulysses group
head_dim: Which dimension index corresponds to heads (1 or 2)
Returns:
Tensor with the same dim order as input, with heads gathered and sequence sharded.
"""
world_size = get_ulysses_parallel_world_size()
if world_size <= 1:
return x
assert x.ndim == 4, f"x must have 4 dimensions, got {x.ndim}"
assert head_dim in (1, 2), f"head_dim must be 1 or 2, got {head_dim}"
assert (
len(seq_lens) == world_size
), f"seq_lens must have length {world_size}, got {len(seq_lens)}"
rank = get_ulysses_parallel_rank()
# Move the sequence dimension to dim 2 for splitting across seq_lens
if head_dim == 1:
b, h_local, s_global, d = x.shape
# Shape transition: [b, h_local, s_global, d] -> [h_local, b, s_global, d]
permute_order = (1, 0, 2, 3)
else: # head_dim == 2
b, s_global, h_local, d = x.shape
# Shape transition: [b, s_global, h_local, d] -> [h_local, b, s_global, d]
permute_order = (2, 0, 1, 3)
assert s_global == sum(
seq_lens
), f"s_global ({s_global}) must equal sum(seq_lens) ({sum(seq_lens)})"
s_local = seq_lens[rank]
x = x.permute(permute_order).contiguous()
input_chunks = []
start = 0
for seq_len in seq_lens:
end = start + seq_len
input_chunks.append(x[:, :, start:end, :].contiguous().reshape(-1))
start = end
x = torch.cat(input_chunks, dim=0)
input_split_sizes = [h_local * b * seq_len * d for seq_len in seq_lens]
output_split_sizes = [h_local * b * s_local * d] * world_size
x = _usp_all_to_all_single_varlen(x, output_split_sizes, input_split_sizes)
chunks = []
offset = 0
for split_size in output_split_sizes:
chunk = x[offset : offset + split_size].reshape(h_local, b, s_local, d)
chunks.append(chunk)
offset += split_size
x = torch.cat(chunks, dim=0)
if head_dim == 1:
# Shape transition: [h_global, b, s_local, d] -> [b, h_global, s_local, d]
x = x.permute(1, 0, 2, 3).contiguous()
else: # head_dim == 2
# Shape transition: [h_global, b, s_local, d] -> [b, s_local, h_global, d]
x = x.permute(1, 2, 0, 3).contiguous()
return x
def ring_attn(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_impl: "AttentionImpl",
is_causal: bool = False,
dropout_p: float = 0.0,
):
"""
Ring Attention implementation.
This function implements Ring Attention, a strategy for distributed attention
computation that reduces peak memory usage. It accepts a generic attention
implementation (`attn_impl`) which is called by the underlying PyTorch
distributed attention primitive.
Args:
query, key, value: The input tensors for attention.
attn_impl: An instance of an attention implementation backend
(e.g., FlashAttentionImpl) whose `forward` method will be
used as the computational kernel.
is_causal: Whether to apply causal masking.
dropout_p: Dropout probability.
"""
# torch.distributed.tensor.experimental._attention is not a public API,
from torch.distributed.tensor.experimental._attention import (
_templated_ring_attention,
)
ring_pg = get_sp_group().ring_group
assert ring_pg is not None, "Ring process group is not initialized."
# Ring attention primitives expect tensors in [B, H, S, D] layout.
# We permute the inputs here.
query = torch.permute(query, [0, 2, 1, 3]).contiguous()
key = torch.permute(key, [0, 2, 1, 3]).contiguous()
value = torch.permute(value, [0, 2, 1, 3]).contiguous()
# Create an adapter function that matches the signature expected by
# _templated_ring_attention. The `attn_impl` already has dropout and
# causal settings configured during its initialization.
# Note: Please be aware that Attention Backend and Ring Attention may require different QKV tensor shapes.
# For example, FlashAttention expects the format to be BSHD.
def attn_callable_adapter(q, k, v, *args, **kwargs):
# We ignore the dropout_p and is_causal passed by _templated_ring_attention
# and rely on the pre-configured attn_impl.
# The `attn_metadata` is not available here, so we pass None.
# This is a limitation we must accept when using this experimental API.
q = torch.permute(q, [0, 2, 1, 3])
k = torch.permute(k, [0, 2, 1, 3])
v = torch.permute(v, [0, 2, 1, 3])
# logger.warning(f"Warning: return_softmax_lse is only supported for FlashAttentionImpl")
output, softmax_lse, *rest = attn_impl.forward(
q,
k,
v,
attn_metadata=None,
return_softmax_lse=True,
)
output = torch.permute(output, [0, 2, 1, 3])
return output, softmax_lse, *rest
# Starting from torch 2.6.0, _templated_ring_attention expects an integer
# segment_id for the attention function.
use_segment_id = torch_release >= (2, 6)
attn_kwargs = dict(
op=attn_callable_adapter,
dropout_p=dropout_p,
is_causal=is_causal,
query=query,
key=key,
value=value,
group=ring_pg, # https://github.com/pytorch/pytorch/blob/c907c778f42ba2fdaf25b733dd25baf9779c6a12/torch/distributed/tensor/experimental/_context_parallel/_attention.py#L309
)
if use_segment_id:
# For torch >= 2.6, segment_id is required. The value '1' is a placeholder
# as we are not using complex segmentation features.
out, *_ = _templated_ring_attention(
seq_dim=1, # segment_id
**attn_kwargs,
)
else:
out, *_ = _templated_ring_attention(
**attn_kwargs,
)
# Permute the output back to [B, S, H, D] layout.
output = torch.permute(out, [0, 2, 1, 3])
return output
@@ -0,0 +1,267 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/model_executor/layers/utils.py
"""Utility methods for model layers."""
import inspect
from typing import Any, Callable, List, Optional
import torch
from torch.library import Library
from sglang.kernel_api_logging import debug_torch_op
from sglang.multimodal_gen.runtime.platforms import current_platform
def get_group_size(group) -> int:
if hasattr(group, "world_size"):
return group.world_size # GroupCoordinator
elif hasattr(group, "size") and callable(getattr(group, "size", None)):
return group.size() # ProcessGroup
else:
raise ValueError(f"Unsupported group type: {type(group)}")
def get_group_rank(group) -> int:
if hasattr(group, "rank_in_group"):
return group.rank_in_group # GroupCoordinator
elif hasattr(group, "rank") and callable(getattr(group, "rank", None)):
return group.rank() # ProcessGroup
else:
raise ValueError(f"Unsupported group type: {type(group)}")
def get_token_bin_counts_and_mask(
tokens: torch.Tensor,
vocab_size: int,
num_seqs: int,
) -> tuple[torch.Tensor, torch.Tensor]:
# Compute the bin counts for the tokens.
# vocab_size + 1 for padding.
bin_counts = torch.zeros(
(num_seqs, vocab_size + 1), dtype=torch.long, device=tokens.device
)
bin_counts.scatter_add_(1, tokens, torch.ones_like(tokens))
bin_counts = bin_counts[:, :vocab_size]
mask = bin_counts > 0
return bin_counts, mask
sglang_lib = Library("sglang", "FRAGMENT") # noqa
def direct_register_custom_op(
op_name: str,
op_func: Callable,
mutates_args: List[str],
fake_impl: Optional[Callable] = None,
target_lib: Optional[Library] = None,
):
"""
`torch.library.custom_op` can have significant overhead because it
needs to consider complicated dispatching logic. This function
directly registers a custom op and dispatches it to the CUDA backend.
See https://gist.github.com/youkaichao/ecbea9ec9fc79a45d2adce1784d7a9a5
for more details.
By default, the custom op is registered to the vLLM library. If you
want to register it to a different library, you can pass the library
object to the `target_lib` argument.
IMPORTANT: the lifetime of the operator is tied to the lifetime of the
library object. If you want to bind the operator to a different library,
make sure the library object is alive when the operator is used.
Note: This function will silently skip registration if the operator
with the same name is already registered to avoid RuntimeError in
multi-engine scenarios (e.g., VERL framework).
"""
import torch.library
my_lib = target_lib or sglang_lib
# Check if operator is already registered to avoid duplicate registration
# This is important for scenarios where multiple SGLang engines run in the same process
try:
# Try to access the operator to see if it's already registered
lib_name = my_lib.m.name if hasattr(my_lib.m, "name") else "sglang"
if hasattr(torch.ops, lib_name) and hasattr(
getattr(torch.ops, lib_name), op_name
):
# Operator already exists, skip registration
return
except (AttributeError, RuntimeError):
# Operator doesn't exist, proceed with registration
pass
if hasattr(torch.library, "infer_schema"):
schema_str = torch.library.infer_schema(op_func, mutates_args=mutates_args)
else:
# for pytorch 2.4
import torch._custom_op.impl
schema_str = torch._custom_op.impl.infer_schema(op_func, mutates_args)
try:
my_lib.define(op_name + schema_str)
my_lib.impl(
op_name, op_func, "CUDA" if not current_platform.is_npu() else "PrivateUse1"
)
if fake_impl is not None:
my_lib._register_fake(op_name, fake_impl)
except RuntimeError as error:
if "Tried to register an operator" in str(error) and "multiple times" in str(
error
):
# Silently ignore duplicate registration errors
# This can happen in multi-engine scenarios
pass
else:
# Re-raise other RuntimeErrors
raise error
except AttributeError as error:
# Always re-raise AttributeError as it indicates missing dependencies
raise error
class CustomOpWrapper:
def __init__(
self,
op_name: str,
op_func: Callable,
mutates_args: List[str],
**extra_kwargs,
):
self.op_name = op_name
self.op_func = op_func
self.mutates_args = mutates_args
self.extra_kwargs = extra_kwargs
self._impl: Optional[Callable] = None
def __call__(self, *args, **kwargs):
return self.real_impl(*args, **kwargs)
@property
def real_impl(self) -> Callable:
if self._impl is None:
if not hasattr(torch.ops.sglang, self.op_name):
# NOTE(dark): if torch compile fail here, mark the decorator as eager
# lazy registration does not work with torch compile
direct_register_custom_op(
op_name=self.op_name,
op_func=self.op_func,
mutates_args=self.mutates_args,
fake_impl=self.fake_impl,
)
self._impl = debug_torch_op(self.op_func, self.op_name)
assert self._impl is not None
return self._impl
@property
def fake_impl(self) -> Callable:
if "fake_impl" in self.extra_kwargs:
return self.extra_kwargs["fake_impl"]
assert "out_shape" in self.extra_kwargs
signature = inspect.signature(self.op_func)
out_shape = self.extra_kwargs["out_shape"]
# check out_shape in signature
def fake_impl(*args, **kwargs):
if out_shape is None:
return None
bound = signature.bind(*args, **kwargs)
bound.apply_defaults()
try:
return torch.empty_like(
bound.args[out_shape]
if isinstance(out_shape, int)
else bound.arguments[out_shape]
)
except (IndexError, KeyError):
raise RuntimeError(
f"Cannot find output argument at position `{out_shape}` for "
f"custom operator `{self.op_name}` with signature `{signature}`."
)
return fake_impl
# Real implementation
def register_custom_op(
fn: Optional[Callable] = None,
*,
op_name: Optional[str] = None,
mutates_args: Optional[List[str]] = None,
eager: bool = True,
**extra_kwargs,
) -> Any:
"""
A decorator to register a custom operator.
Example usage:
```python
# inplace operator, out_shape is None by default
@register_custom_op(mutates_args=["x"])
def add_1_(x: torch.Tensor) -> None:
x.add_(1)
# operator with output, out_shape indicates the position of output
@register_custom_op(mutates_args=["x"], out_shape=0)
def add(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
return x.add_(y)
```
:param fn: The function to be registered as a custom operator.
If None, return a decorator.
:type fn: Callable
:param op_name: The name of the operator. If None, use the function name
:type op_name: Optional[str]
:param mutates_args: A list of argument names that are mutated in-place.
:type mutates_args: List[str]
:param out_shape: The position (int for positional, str for keyword) of the output-shape tensor.
It is used to generate a fake implementation for torch.compile compatibility.
If the operator is inplace and has no output, set to None.
:type out_shape: Optional[List[Union[int, str]]]
:param fake_impl: A fake implementation for the operator.
Only one of `out_shape` or `fake_impl` should be provided.
:type fake_impl: Optional[Callable]
:param eager: Whether to register the operator eagerly.
If False, the registration will be deferred until the first call.
If you met any issue with torch.compile, try to set eager=True.
Currently, to avoid misuse, we set eager=True by default.
:type eager: bool
:return: The registered JIT custom operator, or a decorator.
NOTE: the real register will occur at the first call of the function.
:rtype: Callable
"""
extra_kwarg_keys = set(extra_kwargs.keys())
expected_kwarg_keys = set({"out_shape", "fake_impl"})
assert (
expected_kwarg_keys >= extra_kwarg_keys
), f"Unexpected extra kwargs: {extra_kwarg_keys - expected_kwarg_keys}"
has_out_shape = "out_shape" in extra_kwargs
has_fake_impl = "fake_impl" in extra_kwargs
assert not (
has_out_shape and has_fake_impl
), "Only one of `out_shape` or `fake_impl` should be provided."
# Assume inplace if neither out_shape nor fake_impl is provided
if not (has_out_shape or has_fake_impl):
extra_kwargs["out_shape"] = None
def decorator(op_func: Callable) -> Callable:
wrapper = CustomOpWrapper(
op_name=op_name or op_func.__name__,
op_func=op_func,
mutates_args=mutates_args or [],
**extra_kwargs,
)
return wrapper.real_impl if eager else wrapper
if fn is not None:
return decorator(fn)
return decorator
@@ -0,0 +1,353 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from diffusers.models.embeddings import (
CombinedTimestepGuidanceTextProjEmbeddings as _CombinedTimestepGuidanceTextProjEmbeddings,
)
from diffusers.models.embeddings import (
CombinedTimestepTextProjEmbeddings as _CombinedTimestepTextProjEmbeddings,
)
from diffusers.models.embeddings import (
PixArtAlphaTextProjection,
TimestepEmbedding,
)
from diffusers.models.embeddings import Timesteps as _Timesteps
from diffusers.models.embeddings import (
get_timestep_embedding as timestep_embedding_diffusers,
)
from sglang.jit_kernel.timestep_embedding import (
timestep_embedding as timestep_embedding_cuda,
)
from sglang.multimodal_gen.runtime.layers.activation import get_act_fn
from sglang.multimodal_gen.runtime.layers.linear import ColumnParallelLinear
from sglang.multimodal_gen.runtime.layers.mlp import MLP
from sglang.multimodal_gen.runtime.platforms import current_platform
_is_cuda = current_platform.is_cuda()
class PatchEmbed(nn.Module):
"""2D Image to Patch Embedding
Image to Patch Embedding using Conv2d
A convolution based approach to patchifying a 2D image w/ embedding projection.
Based on the impl in https://github.com/google-research/vision_transformer
Hacked together by / Copyright 2020 Ross Wightman
Remove the _assert function in forward function to be compatible with multi-resolution images.
"""
def __init__(
self,
patch_size=16,
in_chans=3,
embed_dim=768,
norm_layer=None,
flatten=True,
bias=True,
dtype=None,
prefix: str = "",
):
super().__init__()
if isinstance(patch_size, list | tuple):
if len(patch_size) == 1:
patch_size = (1, patch_size[0], patch_size[0])
elif len(patch_size) == 2:
patch_size = (1, patch_size[0], patch_size[1])
else:
patch_size = (1, patch_size, patch_size)
self.patch_size = patch_size
self.flatten = flatten
self.proj = nn.Conv3d(
in_chans,
embed_dim,
kernel_size=patch_size,
stride=patch_size,
bias=bias,
dtype=dtype,
)
self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity()
def forward(self, x):
if x.dim() == 5:
B, C, T, H, W = x.shape
pt, ph, pw = self.patch_size
if T % pt == 0 and H % ph == 0 and W % pw == 0:
T_ = T // pt
H_ = H // ph
W_ = W // pw
x = x.reshape(B, C, T_, pt, H_, ph, W_, pw)
x = x.permute(0, 2, 4, 6, 1, 3, 5, 7).contiguous()
x = x.reshape(B, T_ * H_ * W_, C * pt * ph * pw)
w = self.proj.weight.reshape(self.proj.weight.shape[0], -1)
x = F.linear(x, w, self.proj.bias) # [B, T'*H'*W', embed_dim]
if not self.flatten:
x = x.reshape(B, T_, H_, W_, -1).permute(0, 4, 1, 2, 3).contiguous()
x = self.norm(x)
return x
# Fallback to Conv3d for non-5D input or indivisible spatial dims.
x = self.proj(x)
if self.flatten:
x = x.flatten(2).transpose(1, 2)
x = self.norm(x)
return x
class WanCamControlPatchEmbedding(nn.Module):
"""Patch embedding used by LingBotWorld camera/plucker controls."""
def __init__(
self,
patch_size=(1, 2, 2),
in_chans=384,
embed_dim=2048,
bias=True,
dtype=None,
prefix: str = "",
):
super().__init__()
del prefix
if isinstance(patch_size, list | tuple):
if len(patch_size) != 3:
raise ValueError(
f"patch_size must have length 3, got {len(patch_size)}"
)
patch_size = tuple(patch_size)
else:
raise ValueError(f"Unsupported patch_size type: {type(patch_size)}")
self.patch_size = patch_size
pt, ph, pw = self.patch_size
self.in_features = in_chans * pt * ph * pw
self.proj = nn.Linear(self.in_features, embed_dim, bias=bias, dtype=dtype)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if x.dim() != 5:
raise ValueError(
f"Expected camera embedding shape [B, C, F, H, W], got {tuple(x.shape)}"
)
bsz, channels, frames, height, width = x.shape
pt, ph, pw = self.patch_size
if (frames % pt) != 0 or (height % ph) != 0 or (width % pw) != 0:
raise ValueError(
f"Input shape {tuple(x.shape)} must be divisible by patch_size {self.patch_size}"
)
x = x.view(
bsz,
channels,
frames // pt,
pt,
height // ph,
ph,
width // pw,
pw,
)
x = x.permute(0, 2, 4, 6, 1, 3, 5, 7).reshape(bsz, -1, self.in_features)
return self.proj(x)
class Timesteps(_Timesteps):
def forward(self, timesteps: torch.Tensor) -> torch.Tensor:
if _is_cuda:
return timestep_embedding_cuda(
timesteps,
self.num_channels,
flip_sin_to_cos=self.flip_sin_to_cos,
downscale_freq_shift=self.downscale_freq_shift,
scale=self.scale,
)
else:
return timestep_embedding_diffusers(
timesteps,
self.num_channels,
flip_sin_to_cos=self.flip_sin_to_cos,
downscale_freq_shift=self.downscale_freq_shift,
scale=self.scale,
)
class CombinedTimestepGuidanceTextProjEmbeddings(
_CombinedTimestepGuidanceTextProjEmbeddings
):
def __init__(self, embedding_dim, pooled_projection_dim):
nn.Module.__init__(self)
# use sgld op
self.time_proj = Timesteps(
num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0
)
# use diffusers op
self.timestep_embedder = TimestepEmbedding(
in_channels=256, time_embed_dim=embedding_dim
)
self.guidance_embedder = TimestepEmbedding(
in_channels=256, time_embed_dim=embedding_dim
)
self.text_embedder = PixArtAlphaTextProjection(
pooled_projection_dim, embedding_dim, act_fn="silu"
)
class CombinedTimestepTextProjEmbeddings(_CombinedTimestepTextProjEmbeddings):
def __init__(self, embedding_dim, pooled_projection_dim):
nn.Module.__init__(self)
# use sgld op
self.time_proj = Timesteps(
num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0
)
# use diffusers op
self.timestep_embedder = TimestepEmbedding(
in_channels=256, time_embed_dim=embedding_dim
)
self.text_embedder = PixArtAlphaTextProjection(
pooled_projection_dim, embedding_dim, act_fn="silu"
)
class TimestepEmbedder(nn.Module):
"""
Embeds scalar timesteps into vector representations.
"""
def __init__(
self,
hidden_size,
act_layer="silu",
frequency_embedding_size=256,
max_period=10000,
dtype=None,
freq_dtype=torch.float32,
prefix: str = "",
):
super().__init__()
self.frequency_embedding_size = frequency_embedding_size
self.max_period = max_period
self.mlp = MLP(
frequency_embedding_size,
hidden_size,
hidden_size,
act_type=act_layer,
dtype=dtype,
)
self.freq_dtype = freq_dtype
def forward(
self, t: torch.Tensor, timestep_seq_len: int | None = None
) -> torch.Tensor:
t_freq = timestep_embedding(
t, self.frequency_embedding_size, self.max_period, dtype=self.freq_dtype
).to(self.mlp.fc_in.weight.dtype)
if timestep_seq_len is not None:
assert (
t_freq.shape[0] % timestep_seq_len == 0
), "timestep length is not divisible by timestep_seq_len"
batch_size = t_freq.shape[0] // timestep_seq_len
t_freq = t_freq.unflatten(0, (batch_size, timestep_seq_len))
# t_freq = t_freq.to(self.mlp.fc_in.weight.dtype)
t_emb = self.mlp(t_freq)
return t_emb
def timestep_embedding(
t: torch.Tensor,
dim: int,
max_period: int = 10000,
dtype: torch.dtype = torch.float32,
) -> torch.Tensor:
"""
Create sinusoidal timestep embeddings.
Args:
t: Tensor of shape [B] with timesteps
dim: Embedding dimension
max_period: Controls the minimum frequency of the embeddings
Returns:
Tensor of shape [B, dim] with embeddings
"""
half = dim // 2
freqs = torch.exp(
-math.log(max_period)
* torch.arange(start=0, end=half, dtype=dtype, device=t.device)
/ half
)
args = t[:, None].float() * freqs[None]
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
if dim % 2:
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
return embedding
class ModulateProjection(nn.Module):
"""Modulation layer for DiT blocks."""
def __init__(
self,
hidden_size: int,
factor: int = 2,
act_layer: str = "silu",
dtype: torch.dtype | None = None,
prefix: str = "",
):
super().__init__()
self.factor = factor
self.hidden_size = hidden_size
self.linear = ColumnParallelLinear(
hidden_size,
hidden_size * factor,
bias=True,
gather_output=True,
params_dtype=dtype,
)
self.act = get_act_fn(act_layer)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.act(x)
x, _ = self.linear(x)
return x
def unpatchify(x, t, h, w, patch_size, channels) -> torch.Tensor:
"""
Convert patched representation back to image space.
Args:
x: Tensor of shape [B, T*H*W, C*P_t*P_h*P_w]
t, h, w: Temporal and spatial dimensions
Returns:
Unpatchified tensor of shape [B, C, T*P_t, H*P_h, W*P_w]
"""
assert x.ndim == 3, f"x.ndim: {x.ndim}"
assert len(patch_size) == 3, f"patch_size: {patch_size}"
assert t * h * w == x.shape[1], f"t * h * w: {t * h * w}, x.shape[1]: {x.shape[1]}"
c = channels
pt, ph, pw = patch_size
x = x.reshape(shape=(x.shape[0], t, h, w, c, pt, ph, pw))
x = torch.einsum("nthwcopq->nctohpwq", x)
imgs = x.reshape(shape=(x.shape[0], c, t * pt, h * ph, w * pw))
return imgs
@@ -0,0 +1,490 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from collections.abc import Sequence
from dataclasses import dataclass
import torch
import torch.distributed as dist
import torch.nn.functional as F
from torch.nn.parameter import Parameter, UninitializedParameter
from sglang.multimodal_gen.runtime.distributed import (
divide,
get_tp_group,
tensor_model_parallel_all_reduce,
)
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
QuantizeMethodBase,
method_has_implemented_embedding,
)
from sglang.multimodal_gen.runtime.layers.utils import get_group_rank, get_group_size
from sglang.multimodal_gen.runtime.models.parameter import BasevLLMParameter
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.weight_attrs import set_weight_attrs
DEFAULT_VOCAB_PADDING_SIZE = 64
class UnquantizedEmbeddingMethod(QuantizeMethodBase):
"""Unquantized method for embeddings."""
def create_weights(
self,
layer: torch.nn.Module,
input_size_per_partition: int,
output_partition_sizes: list[int],
input_size: int,
output_size: int,
params_dtype: torch.dtype,
**extra_weight_attrs,
):
"""Create weights for embedding layer."""
weight = Parameter(
torch.empty(
sum(output_partition_sizes),
input_size_per_partition,
dtype=params_dtype,
),
requires_grad=False,
)
set_weight_attrs(weight, {"input_dim": 1, "output_dim": 0})
layer.register_parameter("weight", weight)
set_weight_attrs(weight, extra_weight_attrs)
def apply(
self, layer: torch.nn.Module, x: torch.Tensor, bias: torch.Tensor | None = None
) -> torch.Tensor:
return F.linear(x, layer.weight, bias)
def embedding(self, layer: torch.nn.Module, input_: torch.Tensor) -> torch.Tensor:
return F.embedding(input_, layer.weight)
def pad_vocab_size(vocab_size: int, pad_to: int = DEFAULT_VOCAB_PADDING_SIZE) -> int:
"""Pad the vocab size to the given value."""
return ((vocab_size + pad_to - 1) // pad_to) * pad_to
def vocab_range_from_per_partition_vocab_size(
per_partition_vocab_size: int, rank: int, offset: int = 0
) -> Sequence[int]:
index_f = rank * per_partition_vocab_size
index_l = index_f + per_partition_vocab_size
return index_f + offset, index_l + offset
def vocab_range_from_global_vocab_size(
global_vocab_size: int, rank: int, world_size: int, offset: int = 0
) -> Sequence[int]:
per_partition_vocab_size = divide(global_vocab_size, world_size)
return vocab_range_from_per_partition_vocab_size(
per_partition_vocab_size, rank, offset=offset
)
@dataclass
class VocabParallelEmbeddingShardIndices:
"""Indices for a shard of a vocab parallel embedding."""
padded_org_vocab_start_index: int
padded_org_vocab_end_index: int
padded_added_vocab_start_index: int
padded_added_vocab_end_index: int
org_vocab_start_index: int
org_vocab_end_index: int
added_vocab_start_index: int
added_vocab_end_index: int
@property
def num_org_elements(self) -> int:
return self.org_vocab_end_index - self.org_vocab_start_index
@property
def num_added_elements(self) -> int:
return self.added_vocab_end_index - self.added_vocab_start_index
@property
def num_org_elements_padded(self) -> int:
return self.padded_org_vocab_end_index - self.padded_org_vocab_start_index
@property
def num_added_elements_padded(self) -> int:
return self.padded_added_vocab_end_index - self.padded_added_vocab_start_index
@property
def num_org_vocab_padding(self) -> int:
return self.num_org_elements_padded - self.num_org_elements
@property
def num_added_vocab_padding(self) -> int:
return self.num_added_elements_padded - self.num_added_elements
@property
def num_elements_padded(self) -> int:
return self.num_org_elements_padded + self.num_added_elements_padded
def __post_init__(self):
# sanity checks
assert self.padded_org_vocab_start_index <= self.padded_org_vocab_end_index
assert self.padded_added_vocab_start_index <= self.padded_added_vocab_end_index
assert self.org_vocab_start_index <= self.org_vocab_end_index
assert self.added_vocab_start_index <= self.added_vocab_end_index
assert self.org_vocab_start_index <= self.padded_org_vocab_start_index
assert self.added_vocab_start_index <= self.padded_added_vocab_start_index
assert self.org_vocab_end_index <= self.padded_org_vocab_end_index
assert self.added_vocab_end_index <= self.padded_added_vocab_end_index
assert self.num_org_elements <= self.num_org_elements_padded
assert self.num_added_elements <= self.num_added_elements_padded
@torch.compile(
dynamic=True,
backend=current_platform.simple_compile_backend,
disable=current_platform.is_npu(),
)
def get_masked_input_and_mask(
input_: torch.Tensor,
org_vocab_start_index: int,
org_vocab_end_index: int,
num_org_vocab_padding: int,
added_vocab_start_index: int,
added_vocab_end_index: int,
) -> tuple[torch.Tensor, torch.Tensor]:
# torch.compile will fuse all of the pointwise ops below
# into a single kernel, making it very fast
org_vocab_mask = (input_ >= org_vocab_start_index) & (input_ < org_vocab_end_index)
added_vocab_mask = (input_ >= added_vocab_start_index) & (
input_ < added_vocab_end_index
)
added_offset = (
added_vocab_start_index
- (org_vocab_end_index - org_vocab_start_index)
- num_org_vocab_padding
)
valid_offset = (org_vocab_start_index * org_vocab_mask) + (
added_offset * added_vocab_mask
)
vocab_mask = org_vocab_mask | added_vocab_mask
input_ = vocab_mask * (input_ - valid_offset)
return input_, ~vocab_mask
class VocabParallelEmbedding(torch.nn.Module):
"""Embedding parallelized in the vocabulary dimension.
Adapted from torch.nn.Embedding, note that we pad the vocabulary size to
make sure it is divisible by the number of model parallel GPUs.
In order to support various loading methods, we ensure that LoRA-added
embeddings are always at the end of TP-sharded tensors. In other words,
we shard base embeddings and LoRA embeddings separately (both padded),
and place them in the same tensor.
In this example, we will have the original vocab size = 1010,
added vocab size = 16 and padding to 64. Therefore, the total
vocab size with padding will be 1088 (because we first pad 1010 to
1024, add 16, and then pad to 1088).
Therefore, the tensor format looks like the following:
TP1, rank 0 (no sharding):
|< --------BASE-------- >|< -BASE PADDING-- >|< -----LORA------ >|< -LORA PADDING-- >|
corresponding token_id: | 0 | 1 | ... | 1009 | -1 | ... | -1 | 1010 | ... | 1015 | -1 | ... | -1 |
index: | 0 | 1 | ... | 1009 | 1010 | ... | 1023 | 1024 | ... | 1039 | 1040 | ... | 1087 |
TP2, rank 0:
|< --------------------BASE--------------------- >|< -----LORA------ >|< -LORA PADDING- >|
corresponding token_id: | 0 | 1 | 2 | ... | 497 | 498 | ... | 511 | 1000 | ... | 1015 | -1 | ... | -1 |
index: | 0 | 1 | 2 | ... | 497 | 498 | ... | 511 | 512 | ... | 527 | 520 | ... | 543 |
TP2, rank 1:
|< -----------BASE----------- >|< -BASE PADDING- >|< -----------LORA PADDING----------- >|
corresponding token_id: | 512 | 513 | 514 | ... | 1009 | -1 | ... | -1 | -1 | ... | -1 | -1 | ... | -1 |
index: | 0 | 1 | 2 | ... | 497 | 498 | ... | 511 | 512 | ... | 519 | 520 | ... | 543 |
Args:
num_embeddings: vocabulary size.
embedding_dim: size of hidden state.
params_dtype: type of the parameters.
org_num_embeddings: original vocabulary size (without LoRA).
padding_size: padding size for the vocabulary.
quant_config: quant config for the layer
prefix: full name of the layer in the state dict
""" # noqa: E501
def __init__(
self,
num_embeddings: int,
embedding_dim: int,
params_dtype: torch.dtype | None = None,
org_num_embeddings: int | None = None,
padding_size: int = DEFAULT_VOCAB_PADDING_SIZE,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
tp_group: dist.ProcessGroup = None,
):
super().__init__()
# Keep the input dimensions.
tp_group = tp_group or get_tp_group()
tp_rank = get_group_rank(tp_group)
self.tp_size = get_group_size(tp_group)
self.tp_group = tp_group
self.num_embeddings = num_embeddings
self.padding_size = padding_size
self.org_vocab_size = org_num_embeddings or num_embeddings
num_added_embeddings = num_embeddings - self.org_vocab_size
self.org_vocab_size_padded = pad_vocab_size(
self.org_vocab_size, self.padding_size
)
self.num_embeddings_padded = pad_vocab_size(
self.org_vocab_size_padded + num_added_embeddings, self.padding_size
)
assert self.org_vocab_size_padded <= self.num_embeddings_padded
self.shard_indices = self._get_indices(
self.num_embeddings_padded,
self.org_vocab_size_padded,
self.num_embeddings,
self.org_vocab_size,
tp_rank,
self.tp_size,
)
self.embedding_dim = embedding_dim
quant_method = None
if quant_config is not None:
quant_method = quant_config.get_quant_method(self, prefix=prefix)
if quant_method is None:
quant_method = UnquantizedEmbeddingMethod()
# If we are making an embedding layer, then our quantization linear
# method must implement the embedding operation. If we are another
# layer type like ParallelLMHead, this is not important.
is_embedding_layer = type(self.__class__) is VocabParallelEmbedding
quant_method_implements_embedding = method_has_implemented_embedding(
type(quant_method)
)
if is_embedding_layer and not quant_method_implements_embedding:
raise NotImplementedError(
f"The class {type(quant_method).__name__} must implement "
"the 'embedding' method, see UnquantizedEmbeddingMethod."
)
self.quant_method: QuantizeMethodBase = quant_method
if params_dtype is None:
params_dtype = torch.get_default_dtype()
# Divide the weight matrix along the vocaburaly dimension.
self.num_added_embeddings = self.num_embeddings - self.org_vocab_size
self.num_embeddings_per_partition = divide(
self.num_embeddings_padded, self.tp_size
)
assert (
self.shard_indices.num_elements_padded == self.num_embeddings_per_partition
)
self.num_org_embeddings_per_partition = (
self.shard_indices.org_vocab_end_index
- self.shard_indices.org_vocab_start_index
)
self.num_added_embeddings_per_partition = (
self.shard_indices.added_vocab_end_index
- self.shard_indices.added_vocab_start_index
)
self.quant_method.create_weights(
self,
self.embedding_dim,
[self.num_embeddings_per_partition],
self.embedding_dim,
self.num_embeddings_padded,
params_dtype=params_dtype,
weight_loader=self.weight_loader,
)
@classmethod
def _get_indices(
cls,
vocab_size_padded: int,
org_vocab_size_padded: int,
vocab_size: int,
org_vocab_size: int,
tp_rank: int,
tp_size: int,
) -> VocabParallelEmbeddingShardIndices:
"""Get start and end indices for vocab parallel embedding, following the
layout outlined in the class docstring, based on the given tp_rank and
tp_size."""
num_added_embeddings_padded = vocab_size_padded - org_vocab_size_padded
padded_org_vocab_start_index, padded_org_vocab_end_index = (
vocab_range_from_global_vocab_size(org_vocab_size_padded, tp_rank, tp_size)
)
padded_added_vocab_start_index, padded_added_vocab_end_index = (
vocab_range_from_global_vocab_size(
num_added_embeddings_padded, tp_rank, tp_size, offset=org_vocab_size
)
)
# remove padding
org_vocab_start_index = min(padded_org_vocab_start_index, org_vocab_size)
org_vocab_end_index = min(padded_org_vocab_end_index, org_vocab_size)
added_vocab_start_index = min(padded_added_vocab_start_index, vocab_size)
added_vocab_end_index = min(padded_added_vocab_end_index, vocab_size)
return VocabParallelEmbeddingShardIndices(
padded_org_vocab_start_index,
padded_org_vocab_end_index,
padded_added_vocab_start_index,
padded_added_vocab_end_index,
org_vocab_start_index,
org_vocab_end_index,
added_vocab_start_index,
added_vocab_end_index,
)
def get_sharded_to_full_mapping(self) -> list[int] | None:
"""Get a mapping that can be used to reindex the gathered
logits for sampling.
During sampling, we gather logits from all ranks. The relationship
of index->token_id will follow the same format as outlined in the class
docstring. However, after the gather, we want to reindex the final
logits tensor to map index->token_id one-to-one (the index is always
equal the token_id it corresponds to). The indices returned by this
method allow us to do that.
"""
if self.tp_size < 2:
return None
base_embeddings: list[int] = []
added_embeddings: list[int] = []
padding: list[int] = []
for tp_rank in range(self.tp_size):
shard_indices = self._get_indices(
self.num_embeddings_padded,
self.org_vocab_size_padded,
self.num_embeddings,
self.org_vocab_size,
tp_rank,
self.tp_size,
)
range_start = self.num_embeddings_per_partition * tp_rank
range_end = self.num_embeddings_per_partition * (tp_rank + 1)
base_embeddings.extend(
range(range_start, range_start + shard_indices.num_org_elements)
)
padding.extend(
range(
range_start + shard_indices.num_org_elements,
range_start + shard_indices.num_org_elements_padded,
)
)
added_embeddings.extend(
range(
range_start + shard_indices.num_org_elements_padded,
range_start
+ shard_indices.num_org_elements_padded
+ shard_indices.num_added_elements,
)
)
padding.extend(
range(
range_start
+ shard_indices.num_org_elements_padded
+ shard_indices.num_added_elements,
range_start
+ shard_indices.num_org_elements_padded
+ shard_indices.num_added_elements_padded,
)
)
assert (
range_start
+ shard_indices.num_org_elements_padded
+ shard_indices.num_added_elements_padded
== range_end
)
ret = base_embeddings + added_embeddings + padding
assert len(ret) == self.num_embeddings_padded
return ret
def weight_loader(self, param: Parameter, loaded_weight: torch.Tensor):
output_dim = getattr(param, "output_dim", None)
packed_dim = getattr(param, "packed_dim", None)
# If the parameter is a gguf weight, then load it directly.
if getattr(param, "is_gguf_weight_type", None):
param.data.copy_(loaded_weight)
param.weight_type = loaded_weight.item()
return
elif isinstance(param, UninitializedParameter):
shape = list(loaded_weight.shape)
if output_dim is not None:
shape[output_dim] = self.num_embeddings_per_partition
param.materialize(tuple(shape), dtype=loaded_weight.dtype)
# If parameter does not have output dim, then it should
# be copied onto all gpus (e.g. g_idx for act_order gptq).
if output_dim is None:
assert param.data.shape == loaded_weight.shape
param.data.copy_(loaded_weight)
return
# Shard indexes for loading the weight
start_idx = self.shard_indices.org_vocab_start_index
shard_size = self.shard_indices.org_vocab_end_index - start_idx
# If param packed on the same dim we are sharding on, then
# need to adjust offsets of loaded weight by pack_factor.
if packed_dim is not None and packed_dim == output_dim:
packed_factor = (
param.packed_factor
if isinstance(param, BasevLLMParameter)
else param.pack_factor
)
assert loaded_weight.shape[output_dim] == (
self.org_vocab_size // param.packed_factor
)
start_idx = start_idx // packed_factor
shard_size = shard_size // packed_factor
else:
assert loaded_weight.shape[output_dim] == self.org_vocab_size
# Copy the data. Select chunk corresponding to current shard.
loaded_weight = loaded_weight.narrow(output_dim, start_idx, shard_size)
param[: loaded_weight.shape[0]].data.copy_(loaded_weight)
param[loaded_weight.shape[0] :].data.fill_(0)
def forward(self, input_):
if self.tp_size > 1:
# Build the mask.
masked_input, input_mask = get_masked_input_and_mask(
input_,
self.shard_indices.org_vocab_start_index,
self.shard_indices.org_vocab_end_index,
self.shard_indices.num_org_vocab_padding,
self.shard_indices.added_vocab_start_index,
self.shard_indices.added_vocab_end_index,
)
else:
masked_input = input_
# Get the embeddings.
output_parallel = self.quant_method.embedding(self, masked_input.long())
# Mask the output embedding.
if self.tp_size > 1:
output_parallel.masked_fill_(input_mask.unsqueeze(-1), 0)
# Reduce across all the model parallel GPUs.
output = tensor_model_parallel_all_reduce(
output_parallel, tp_group=self.tp_group
)
return output
def extra_repr(self) -> str:
s = f"num_embeddings={self.num_embeddings_per_partition}"
s += f", embedding_dim={self.embedding_dim}"
s += f", org_vocab_size={self.org_vocab_size}"
s += f", num_embeddings_padded={self.num_embeddings_padded}"
s += f", tp_size={self.tp_size}"
return s