Files
hao-ai-lab-FastVideo/fastvideo/layers/mxfp8linear.py
T

318 lines
12 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""MXFP8 block quantization and linear operations for Blackwell inference."""
from __future__ import annotations
import torch
import torch.nn.functional as F
import triton
import triton.language as tl
# Quack moved these helpers to quack.blockscaled.quantize, and some images ship neither.
try:
from quack.mx_utils import to_blocked, to_mx
except ImportError:
try:
from quack.blockscaled.quantize import to_blocked, to_mx
except ImportError:
to_blocked = None
to_mx = None
MXFP8_BLOCK_SIZE = 32
_BLOCKS_PER_PROGRAM = 16
def _validate_mxfp8_matrix(matrix: torch.Tensor) -> None:
"""Validate one row-major matrix before MXFP8 quantization."""
if matrix.ndim != 2:
raise ValueError(f"MXFP8 quantization requires a 2D tensor, got shape {tuple(matrix.shape)}.")
if matrix.dtype not in (torch.bfloat16, torch.float32):
raise TypeError(f"MXFP8 quantization requires BF16 or FP32 input, got {matrix.dtype}.")
if matrix.shape[1] % MXFP8_BLOCK_SIZE:
raise ValueError(f"MXFP8 reduction dimension must be divisible by {MXFP8_BLOCK_SIZE}, got {matrix.shape[1]}.")
@torch.compile(dynamic=True, fullgraph=True)
def _quantize_mxfp8_weight_blockwise(matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
values, natural_scales = to_mx(matrix.contiguous(), MXFP8_BLOCK_SIZE)
return values, to_blocked(natural_scales)
def quantize_mxfp8_weight_blockwise(matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""Prequantize a weight with Quack and return hardware-blocked scales."""
if to_mx is None or to_blocked is None:
raise ImportError("MXFP8 weight quantization requires Quack's to_mx/to_blocked helpers "
"(quack.mx_utils in older releases, quack.blockscaled.quantize in newer "
"ones). Install a compatible quack to use the MXFP8 weight path; the "
"Triton activation and SwiGLU quantizers do not need it.")
_validate_mxfp8_matrix(matrix)
return _quantize_mxfp8_weight_blockwise(matrix)
@triton.jit
def _store_mxfp8_blocks(
matrix,
quantized_ptr,
scale_ptr,
row,
first_block,
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM: tl.constexpr,
):
"""Quantize row blocks and store scales in cuBLAS's 32x4x4 layout."""
element_offsets = tl.arange(0, BLOCKS_PER_PROGRAM * 32)
columns = first_block * 32 + element_offsets
valid_elements = (row < row_count) & (columns < column_count)
matrix = tl.reshape(matrix, (BLOCKS_PER_PROGRAM, 32))
# FLOOR scaling encodes exponent(max_abs) - 8 as one E8M0 byte per 32 values.
max_abs = tl.max(tl.abs(matrix), axis=1)
exponent = (max_abs.to(tl.int32, bitcast=True) >> 23) & 0xFF
scale_biased = tl.maximum(tl.minimum(exponent - 8, 255), 0)
scale_biased = tl.where(max_abs != max_abs, 255, scale_biased)
scale_bits = scale_biased.to(tl.int32) << 23
scale = tl.maximum(scale_bits.to(tl.float32, bitcast=True), 1.1754943508222875e-38)
scaled_matrix = tl.maximum(tl.minimum(matrix / scale[:, None], 448.0), -448.0)
tl.store(
quantized_ptr + row * column_count + columns,
tl.reshape(scaled_matrix.to(tl.float8e4nv), (BLOCKS_PER_PROGRAM * 32, )),
mask=valid_elements,
)
blocks = first_block + tl.arange(0, BLOCKS_PER_PROGRAM)
# Map natural [row, K / 32] coordinates into cuBLAS's padded 128x4 tiles.
row_block = row // 128
row_in_block = row % 128
column_block = blocks // 4
column_in_block = blocks % 4
scale_offset = ((row_block * (scale_column_count // 4) + column_block) * 512 + (row_in_block % 32) * 16 +
(row_in_block // 32) * 4 + column_in_block)
tl.store(scale_ptr + scale_offset, scale_biased.to(tl.uint8), mask=blocks < scale_column_count)
@triton.jit
def _quantize_mxfp8_kernel(
matrix_ptr,
quantized_ptr,
scale_ptr,
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM: tl.constexpr,
):
"""Load BF16 activation blocks for direct MXFP8 quantization."""
row = tl.program_id(0)
first_block = tl.program_id(1) * BLOCKS_PER_PROGRAM
element_offsets = tl.arange(0, BLOCKS_PER_PROGRAM * 32)
columns = first_block * 32 + element_offsets
matrix = tl.load(
matrix_ptr + row * column_count + columns,
mask=(row < row_count) & (columns < column_count),
other=0.0,
).to(tl.float32)
_store_mxfp8_blocks(
matrix,
quantized_ptr,
scale_ptr,
row,
first_block,
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM,
)
@triton.jit
def _swiglu_quantize_mxfp8_kernel(
preactivation_ptr,
quantized_ptr,
scale_ptr,
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM: tl.constexpr,
):
"""Apply value-first SwiGLU and directly quantize its BF16 result."""
row = tl.program_id(0)
first_block = tl.program_id(1) * BLOCKS_PER_PROGRAM
element_offsets = tl.arange(0, BLOCKS_PER_PROGRAM * 32)
columns = first_block * 32 + element_offsets
valid_elements = (row < row_count) & (columns < column_count)
packed_row_offset = row * (2 * column_count)
values = tl.load(
preactivation_ptr + packed_row_offset + columns,
mask=valid_elements,
other=0.0,
).to(tl.float32)
gates = tl.load(
preactivation_ptr + packed_row_offset + column_count + columns,
mask=valid_elements,
other=0.0,
).to(tl.float32)
postactivation = (values * gates * tl.sigmoid(gates)).to(tl.bfloat16).to(tl.float32)
_store_mxfp8_blocks(
postactivation,
quantized_ptr,
scale_ptr,
row,
first_block,
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM,
)
def _allocate_mxfp8_outputs(matrix: torch.Tensor, column_count: int) -> tuple[torch.Tensor, torch.Tensor, int]:
"""Allocate contiguous values and a complete padded blocked-scale buffer."""
row_count = matrix.shape[0]
scale_column_count = triton.cdiv(column_count // MXFP8_BLOCK_SIZE, 4) * 4
blocked_scale_size = triton.cdiv(row_count, 128) * 128 * scale_column_count
quantized = torch.empty((row_count, column_count), dtype=torch.float8_e4m3fn, device=matrix.device)
scale_storage = torch.empty(blocked_scale_size, dtype=torch.uint8, device=matrix.device)
return quantized, scale_storage.view(torch.float8_e8m0fnu), scale_column_count
def quantize_mxfp8_blockwise(matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""Quantize activation rows and write hardware-blocked scales directly."""
_validate_mxfp8_matrix(matrix)
matrix = matrix.contiguous()
row_count, column_count = matrix.shape
quantized, blocked_scales, scale_column_count = _allocate_mxfp8_outputs(matrix, column_count)
grid = (triton.cdiv(row_count, 128) * 128, triton.cdiv(scale_column_count, _BLOCKS_PER_PROGRAM))
_quantize_mxfp8_kernel[grid](
matrix,
quantized,
blocked_scales.view(torch.uint8),
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM=_BLOCKS_PER_PROGRAM,
num_warps=8,
)
return quantized, blocked_scales
def swiglu_quantize_mxfp8_blockwise(preactivation: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""Apply H3 value-first SwiGLU and quantize the BF16 result to MXFP8."""
if preactivation.ndim != 2:
raise ValueError(f"MXFP8 SwiGLU requires a 2D tensor, got shape {tuple(preactivation.shape)}.")
if preactivation.dtype != torch.bfloat16:
raise TypeError(f"MXFP8 SwiGLU requires BF16 input, got {preactivation.dtype}.")
if preactivation.shape[1] % (2 * MXFP8_BLOCK_SIZE):
raise ValueError("MXFP8 SwiGLU requires each packed half to be divisible by "
f"{MXFP8_BLOCK_SIZE}, got packed width {preactivation.shape[1]}.")
preactivation = preactivation.contiguous()
row_count = preactivation.shape[0]
column_count = preactivation.shape[1] // 2
quantized, blocked_scales, scale_column_count = _allocate_mxfp8_outputs(preactivation, column_count)
grid = (triton.cdiv(row_count, 128) * 128, triton.cdiv(scale_column_count, _BLOCKS_PER_PROGRAM))
_swiglu_quantize_mxfp8_kernel[grid](
preactivation,
quantized,
blocked_scales.view(torch.uint8),
row_count,
column_count,
scale_column_count,
BLOCKS_PER_PROGRAM=_BLOCKS_PER_PROGRAM,
num_warps=8,
)
return quantized, blocked_scales
def _as_e8m0_scales(scales: torch.Tensor) -> torch.Tensor:
# F.scaled_mm rejects flashinfer's uint8 E8M0 scale buffers; they hold the same bytes as float8_e8m0fnu.
if scales.dtype == torch.uint8:
return scales.view(torch.float8_e8m0fnu)
return scales
def mxfp8_scaled_mm(
activation_values: torch.Tensor,
activation_scales: torch.Tensor,
weight_values: torch.Tensor,
weight_scales: torch.Tensor,
bias: torch.Tensor | None,
) -> torch.Tensor:
"""Multiply two MXFP8 matrices and return a BF16 matrix."""
return F.scaled_mm(
mat_a=activation_values,
mat_b=weight_values.mT,
scale_a=_as_e8m0_scales(activation_scales),
scale_recipe_a=F.ScalingType.BlockWise1x32,
scale_b=_as_e8m0_scales(weight_scales),
scale_recipe_b=F.ScalingType.BlockWise1x32,
swizzle_a=F.SwizzleType.SWIZZLE_32_4_4,
swizzle_b=F.SwizzleType.SWIZZLE_32_4_4,
bias=bias,
output_dtype=torch.bfloat16,
)
def _resolve_merged_linear(linear: torch.nn.Module) -> torch.nn.Module:
"""Return the base linear that owns the packed weight for the active LoRA state.
The packed MXFP8 buffers are rebuilt after every LoRA merge and unmerge, so the
packed weight always reflects the current base weight.
"""
return getattr(linear, "base_layer", linear)
def _runtime_lora_delta_pending(linear: torch.nn.Module) -> bool:
"""Whether *linear* is a LoRA wrapper whose forward adds a runtime adapter delta.
BaseLayerWithLoRA.forward adds the delta exactly when the adapter is active but not
merged into the base weight (fastvideo/layers/lora/linear.py).
"""
if getattr(linear, "base_layer", None) is None:
return False
return (not getattr(linear, "merged", False) and not getattr(linear, "disable_lora", False)
and getattr(linear, "lora_A", None) is not None)
def mxfp8_swiglu_feed_forward(
hidden_states: torch.Tensor,
fc_in: torch.nn.Module,
fc_out: torch.nn.Module,
) -> torch.Tensor:
"""Run the MiniMax-H3 feed-forward network with MXFP8 GEMMs."""
if _runtime_lora_delta_pending(fc_in) or _runtime_lora_delta_pending(fc_out):
# An unmerged adapter must keep affecting the output through its runtime delta,
# matching BaseLayerWithLoRA.forward and the unfused MiniMaxH3FeedForward branch.
# The fused GEMMs below only see the packed base weight, so fall back to the
# wrapper path while a delta is pending.
preactivation, _ = fc_in(hidden_states)
value, gate = preactivation.chunk(2, dim=-1)
output, _ = fc_out(value * F.silu(gate))
return output
fc_in_base = _resolve_merged_linear(fc_in)
fc_out_base = _resolve_merged_linear(fc_out)
fc_in_method = fc_in_base.quant_method
fc_out_method = fc_out_base.quant_method
preactivation = fc_in_method.apply(fc_in_base, hidden_states, fc_in_base.bias)
output_shape = (*hidden_states.shape[:-1], fc_out_base.output_size)
preactivation_2d = preactivation.reshape(-1, preactivation.shape[-1])
activation_values, activation_scales = swiglu_quantize_mxfp8_blockwise(preactivation_2d)
output_2d = fc_out_method.apply_quantized(
fc_out_base,
activation_values,
activation_scales,
fc_out_base.bias,
)
return output_2d.reshape(output_shape)
__all__ = [
"MXFP8_BLOCK_SIZE",
"mxfp8_scaled_mm",
"mxfp8_swiglu_feed_forward",
"quantize_mxfp8_blockwise",
"quantize_mxfp8_weight_blockwise",
"swiglu_quantize_mxfp8_blockwise",
]