Skip to content

Commit 09f3361

Browse files
committed
Add MXFP8 H3 FFN inference
1 parent ca280e7 commit 09f3361

7 files changed

Lines changed: 479 additions & 8 deletions

File tree

examples/inference/basic/basic_fasth3_simplified_profile.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
OutputConfig,
2121
ParallelismConfig,
2222
PipelineSelection,
23+
QuantizationConfig,
2324
SamplingConfig,
2425
)
2526
from fastvideo.profiler import nvtx_range
@@ -77,6 +78,7 @@ def main() -> None:
7778
parallelism=ParallelismConfig(tp_size=1, sp_size=4),
7879
offload=OffloadConfig(dit=False, dit_layerwise=False),
7980
compile=CompileConfig(vae_enabled=True),
81+
quantization=QuantizationConfig(transformer_quant="MXFP8"),
8082
),
8183
)
8284
)

fastvideo/layers/mxfp8linear.py

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
"""MXFP8 block quantization and linear operations for Blackwell inference."""
3+
4+
from __future__ import annotations
5+
6+
import torch
7+
import torch.nn.functional as F
8+
import triton
9+
import triton.language as tl
10+
from quack.mx_utils import to_blocked, to_mx
11+
12+
MXFP8_BLOCK_SIZE = 32
13+
_BLOCKS_PER_PROGRAM = 16
14+
15+
16+
def _validate_mxfp8_matrix(matrix: torch.Tensor) -> None:
17+
"""Validate one row-major matrix before MXFP8 quantization."""
18+
if matrix.ndim != 2:
19+
raise ValueError(f"MXFP8 quantization requires a 2D tensor, got shape {tuple(matrix.shape)}.")
20+
if matrix.dtype not in (torch.bfloat16, torch.float32):
21+
raise TypeError(f"MXFP8 quantization requires BF16 or FP32 input, got {matrix.dtype}.")
22+
if matrix.shape[1] % MXFP8_BLOCK_SIZE:
23+
raise ValueError(
24+
f"MXFP8 reduction dimension must be divisible by {MXFP8_BLOCK_SIZE}, got {matrix.shape[1]}.")
25+
26+
27+
@torch.compile(dynamic=True, fullgraph=True)
28+
def _quantize_mxfp8_weight_blockwise(matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
29+
values, natural_scales = to_mx(matrix.contiguous(), MXFP8_BLOCK_SIZE)
30+
return values, to_blocked(natural_scales)
31+
32+
33+
def quantize_mxfp8_weight_blockwise(matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
34+
"""Prequantize a weight with Quack and return hardware-blocked scales."""
35+
_validate_mxfp8_matrix(matrix)
36+
return _quantize_mxfp8_weight_blockwise(matrix)
37+
38+
39+
@triton.jit
40+
def _store_mxfp8_blocks(
41+
matrix,
42+
quantized_ptr,
43+
scale_ptr,
44+
row,
45+
first_block,
46+
row_count,
47+
column_count,
48+
scale_column_count,
49+
BLOCKS_PER_PROGRAM: tl.constexpr,
50+
):
51+
"""Quantize row blocks and store scales in cuBLAS's 32x4x4 layout."""
52+
element_offsets = tl.arange(0, BLOCKS_PER_PROGRAM * 32)
53+
columns = first_block * 32 + element_offsets
54+
valid_elements = (row < row_count) & (columns < column_count)
55+
matrix = tl.reshape(matrix, (BLOCKS_PER_PROGRAM, 32))
56+
57+
# FLOOR scaling encodes exponent(max_abs) - 8 as one E8M0 byte per 32 values.
58+
max_abs = tl.max(tl.abs(matrix), axis=1)
59+
exponent = (max_abs.to(tl.int32, bitcast=True) >> 23) & 0xFF
60+
scale_biased = tl.maximum(tl.minimum(exponent - 8, 255), 0)
61+
scale_biased = tl.where(max_abs != max_abs, 255, scale_biased)
62+
scale_bits = scale_biased.to(tl.int32) << 23
63+
scale = tl.maximum(scale_bits.to(tl.float32, bitcast=True), 1.1754943508222875e-38)
64+
65+
scaled_matrix = tl.maximum(tl.minimum(matrix / scale[:, None], 448.0), -448.0)
66+
tl.store(
67+
quantized_ptr + row * column_count + columns,
68+
tl.reshape(scaled_matrix.to(tl.float8e4nv), (BLOCKS_PER_PROGRAM * 32, )),
69+
mask=valid_elements,
70+
)
71+
72+
blocks = first_block + tl.arange(0, BLOCKS_PER_PROGRAM)
73+
# Map natural [row, K / 32] coordinates into cuBLAS's padded 128x4 tiles.
74+
row_block = row // 128
75+
row_in_block = row % 128
76+
column_block = blocks // 4
77+
column_in_block = blocks % 4
78+
scale_offset = (
79+
(row_block * (scale_column_count // 4) + column_block) * 512
80+
+ (row_in_block % 32) * 16
81+
+ (row_in_block // 32) * 4
82+
+ column_in_block
83+
)
84+
tl.store(scale_ptr + scale_offset, scale_biased.to(tl.uint8), mask=blocks < scale_column_count)
85+
86+
87+
@triton.jit
88+
def _quantize_mxfp8_kernel(
89+
matrix_ptr,
90+
quantized_ptr,
91+
scale_ptr,
92+
row_count,
93+
column_count,
94+
scale_column_count,
95+
BLOCKS_PER_PROGRAM: tl.constexpr,
96+
):
97+
"""Load BF16 activation blocks for direct MXFP8 quantization."""
98+
row = tl.program_id(0)
99+
first_block = tl.program_id(1) * BLOCKS_PER_PROGRAM
100+
element_offsets = tl.arange(0, BLOCKS_PER_PROGRAM * 32)
101+
columns = first_block * 32 + element_offsets
102+
matrix = tl.load(
103+
matrix_ptr + row * column_count + columns,
104+
mask=(row < row_count) & (columns < column_count),
105+
other=0.0,
106+
).to(tl.float32)
107+
_store_mxfp8_blocks(
108+
matrix,
109+
quantized_ptr,
110+
scale_ptr,
111+
row,
112+
first_block,
113+
row_count,
114+
column_count,
115+
scale_column_count,
116+
BLOCKS_PER_PROGRAM,
117+
)
118+
119+
120+
@triton.jit
121+
def _swiglu_quantize_mxfp8_kernel(
122+
preactivation_ptr,
123+
quantized_ptr,
124+
scale_ptr,
125+
row_count,
126+
column_count,
127+
scale_column_count,
128+
BLOCKS_PER_PROGRAM: tl.constexpr,
129+
):
130+
"""Apply value-first SwiGLU and directly quantize its BF16 result."""
131+
row = tl.program_id(0)
132+
first_block = tl.program_id(1) * BLOCKS_PER_PROGRAM
133+
element_offsets = tl.arange(0, BLOCKS_PER_PROGRAM * 32)
134+
columns = first_block * 32 + element_offsets
135+
valid_elements = (row < row_count) & (columns < column_count)
136+
packed_row_offset = row * (2 * column_count)
137+
values = tl.load(
138+
preactivation_ptr + packed_row_offset + columns,
139+
mask=valid_elements,
140+
other=0.0,
141+
).to(tl.float32)
142+
gates = tl.load(
143+
preactivation_ptr + packed_row_offset + column_count + columns,
144+
mask=valid_elements,
145+
other=0.0,
146+
).to(tl.float32)
147+
postactivation = (values * gates * tl.sigmoid(gates)).to(tl.bfloat16).to(tl.float32)
148+
_store_mxfp8_blocks(
149+
postactivation,
150+
quantized_ptr,
151+
scale_ptr,
152+
row,
153+
first_block,
154+
row_count,
155+
column_count,
156+
scale_column_count,
157+
BLOCKS_PER_PROGRAM,
158+
)
159+
160+
161+
def _allocate_mxfp8_outputs(matrix: torch.Tensor, column_count: int) -> tuple[torch.Tensor, torch.Tensor, int]:
162+
"""Allocate contiguous values and a complete padded blocked-scale buffer."""
163+
row_count = matrix.shape[0]
164+
scale_column_count = triton.cdiv(column_count // MXFP8_BLOCK_SIZE, 4) * 4
165+
blocked_scale_size = triton.cdiv(row_count, 128) * 128 * scale_column_count
166+
quantized = torch.empty((row_count, column_count), dtype=torch.float8_e4m3fn, device=matrix.device)
167+
scale_storage = torch.empty(blocked_scale_size, dtype=torch.uint8, device=matrix.device)
168+
return quantized, scale_storage.view(torch.float8_e8m0fnu), scale_column_count
169+
170+
171+
def quantize_mxfp8_blockwise(matrix: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
172+
"""Quantize activation rows and write hardware-blocked scales directly."""
173+
_validate_mxfp8_matrix(matrix)
174+
matrix = matrix.contiguous()
175+
row_count, column_count = matrix.shape
176+
quantized, blocked_scales, scale_column_count = _allocate_mxfp8_outputs(matrix, column_count)
177+
grid = (triton.cdiv(row_count, 128) * 128, triton.cdiv(scale_column_count, _BLOCKS_PER_PROGRAM))
178+
_quantize_mxfp8_kernel[grid](
179+
matrix,
180+
quantized,
181+
blocked_scales.view(torch.uint8),
182+
row_count,
183+
column_count,
184+
scale_column_count,
185+
BLOCKS_PER_PROGRAM=_BLOCKS_PER_PROGRAM,
186+
num_warps=8,
187+
)
188+
return quantized, blocked_scales
189+
190+
191+
def swiglu_quantize_mxfp8_blockwise(preactivation: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
192+
"""Apply H3 value-first SwiGLU and quantize the BF16 result to MXFP8."""
193+
if preactivation.ndim != 2:
194+
raise ValueError(f"MXFP8 SwiGLU requires a 2D tensor, got shape {tuple(preactivation.shape)}.")
195+
if preactivation.dtype != torch.bfloat16:
196+
raise TypeError(f"MXFP8 SwiGLU requires BF16 input, got {preactivation.dtype}.")
197+
if preactivation.shape[1] % (2 * MXFP8_BLOCK_SIZE):
198+
raise ValueError(
199+
"MXFP8 SwiGLU requires each packed half to be divisible by "
200+
f"{MXFP8_BLOCK_SIZE}, got packed width {preactivation.shape[1]}.")
201+
preactivation = preactivation.contiguous()
202+
row_count = preactivation.shape[0]
203+
column_count = preactivation.shape[1] // 2
204+
quantized, blocked_scales, scale_column_count = _allocate_mxfp8_outputs(preactivation, column_count)
205+
grid = (triton.cdiv(row_count, 128) * 128, triton.cdiv(scale_column_count, _BLOCKS_PER_PROGRAM))
206+
_swiglu_quantize_mxfp8_kernel[grid](
207+
preactivation,
208+
quantized,
209+
blocked_scales.view(torch.uint8),
210+
row_count,
211+
column_count,
212+
scale_column_count,
213+
BLOCKS_PER_PROGRAM=_BLOCKS_PER_PROGRAM,
214+
num_warps=8,
215+
)
216+
return quantized, blocked_scales
217+
218+
219+
def mxfp8_scaled_mm(
220+
activation_values: torch.Tensor,
221+
activation_scales: torch.Tensor,
222+
weight_values: torch.Tensor,
223+
weight_scales: torch.Tensor,
224+
bias: torch.Tensor | None,
225+
) -> torch.Tensor:
226+
"""Multiply two MXFP8 matrices and return a BF16 matrix."""
227+
return F.scaled_mm(
228+
mat_a=activation_values,
229+
mat_b=weight_values.mT,
230+
scale_a=activation_scales,
231+
scale_recipe_a=F.ScalingType.BlockWise1x32,
232+
scale_b=weight_scales,
233+
scale_recipe_b=F.ScalingType.BlockWise1x32,
234+
swizzle_a=F.SwizzleType.SWIZZLE_32_4_4,
235+
swizzle_b=F.SwizzleType.SWIZZLE_32_4_4,
236+
bias=bias,
237+
output_dtype=torch.bfloat16,
238+
)
239+
240+
241+
def _resolve_merged_linear(linear: torch.nn.Module) -> torch.nn.Module:
242+
"""Return a linear whose weight includes every active inference adapter."""
243+
base_layer = getattr(linear, "base_layer", linear)
244+
if base_layer is linear:
245+
return base_layer
246+
if not getattr(linear, "merged", False) and not getattr(linear, "disable_lora", False):
247+
raise RuntimeError("MXFP8 feed-forward requires active LoRA weights to be merged before inference.")
248+
return base_layer
249+
250+
251+
def mxfp8_swiglu_feed_forward(
252+
hidden_states: torch.Tensor,
253+
fc_in: torch.nn.Module,
254+
fc_out: torch.nn.Module,
255+
) -> torch.Tensor:
256+
"""Run the MiniMax-H3 feed-forward network with MXFP8 GEMMs."""
257+
fc_in_base = _resolve_merged_linear(fc_in)
258+
fc_out_base = _resolve_merged_linear(fc_out)
259+
fc_in_method = fc_in_base.quant_method
260+
fc_out_method = fc_out_base.quant_method
261+
262+
preactivation = fc_in_method.apply(fc_in_base, hidden_states, fc_in_base.bias)
263+
output_shape = (*hidden_states.shape[:-1], fc_out_base.output_size)
264+
preactivation_2d = preactivation.reshape(-1, preactivation.shape[-1])
265+
activation_values, activation_scales = swiglu_quantize_mxfp8_blockwise(preactivation_2d)
266+
output_2d = fc_out_method.apply_quantized(
267+
fc_out_base,
268+
activation_values,
269+
activation_scales,
270+
fc_out_base.bias,
271+
)
272+
return output_2d.reshape(output_shape)
273+
274+
275+
__all__ = [
276+
"MXFP8_BLOCK_SIZE",
277+
"mxfp8_scaled_mm",
278+
"mxfp8_swiglu_feed_forward",
279+
"quantize_mxfp8_blockwise",
280+
"quantize_mxfp8_weight_blockwise",
281+
"swiglu_quantize_mxfp8_blockwise",
282+
]

fastvideo/layers/quantization/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22

33
from fastvideo.layers.quantization.base_config import QuantizationConfig
44

5-
QuantizationMethods = Literal[None, "AbsMaxFP8", "FP8", "NVFP4", "nvfp4_qat", "nvfp4_qat_train", "fp8_qat_train"]
5+
QuantizationMethods = Literal[
6+
None,
7+
"AbsMaxFP8",
8+
"FP8",
9+
"MXFP8",
10+
"NVFP4",
11+
"nvfp4_qat",
12+
"nvfp4_qat_train",
13+
"fp8_qat_train",
14+
]
615

716
QUANTIZATION_METHODS: list[str] = list(get_args(QuantizationMethods))
817

@@ -52,6 +61,7 @@ def get_quantization_config(quantization: str) -> type[QuantizationConfig]:
5261
# lazy import to avoid triggering `torch.compile` too early
5362
from .absmax_fp8 import AbsMaxFP8Config
5463
from .fp8_config import FP8Config
64+
from .mxfp8_config import MXFP8Config
5565
from .nvfp4_config import NVFP4Config
5666
from .nvfp4_qat_config import NVFP4QATConfig
5767
from .nvfp4_qat_train_config import NVFP4QATTrainConfig
@@ -60,6 +70,7 @@ def get_quantization_config(quantization: str) -> type[QuantizationConfig]:
6070
method_to_config: dict[str, type[QuantizationConfig]] = {
6171
"AbsMaxFP8": AbsMaxFP8Config,
6272
"FP8": FP8Config,
73+
"MXFP8": MXFP8Config,
6374
"NVFP4": NVFP4Config,
6475
"nvfp4_qat": NVFP4QATConfig,
6576
"nvfp4_qat_train": NVFP4QATTrainConfig,

0 commit comments

Comments
 (0)