This commit is contained in:
Peiyuan Zhang
2025-12-23 01:31:21 +00:00
committed by RandNMR73
parent 3a9e4d3b89
commit b628b5fc7e
10 changed files with 819 additions and 345 deletions
@@ -4,8 +4,8 @@ set -euo pipefail
############################################
# Single node, 6 GPUs
############################################
NUM_GPUS=6
export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5
NUM_GPUS=4
export CUDA_VISIBLE_DEVICES=0,1,2,3
export WANDB_MODE="online"
@@ -24,7 +24,7 @@ echo "CUDA_VISIBLE_DEVICES: $CUDA_VISIBLE_DEVICES"
############################################
MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers"
DATA_DIR=data/Wan-Syn_77x448x832_600k/train
VALIDATION_DATASET_FILE=data/Wan-Syn_77x448x832_600k/validation_6.json
VALIDATION_DATASET_FILE=data/Wan-Syn_77x448x832_600k/validation_4.json
OUTPUT_DIR="checkpoints/sage3_distill"
# Training arguments
@@ -35,21 +35,21 @@ training_args=(
--train_batch_size 1
--train_sp_batch_size 1
--gradient_accumulation_steps 1
--num_latent_t 21
--num_latent_t 12
--num_height 480
--num_width 832
--num_frames 81
--num_frames 45
--enable_gradient_checkpointing_type "full"
--generator_4bit_attn True
)
# Parallel arguments (adjusted to 6 GPUs)
parallel_args=(
--num_gpus 6
--num_gpus 4
--sp_size 1
--tp_size 1
--hsdp_replicate_dim 1
--hsdp_shard_dim 6
--hsdp_shard_dim 4
)
# Model arguments
@@ -7,8 +7,8 @@ export TOKENIZERS_PARALLELISM=false
MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers"
DATA_DIR="data/Wan-Syn_77x448x832_600k/train/"
VALIDATION_DATASET_FILE="data/Wan-Syn_77x448x832_600k/validation_6.json"
NUM_GPUS=6
VALIDATION_DATASET_FILE="data/Wan-Syn_77x448x832_600k/validation_4.json"
NUM_GPUS=4
# export CUDA_VISIBLE_DEVICES=4,5
@@ -20,10 +20,10 @@ training_args=(
--train_batch_size 1
--train_sp_batch_size 1
--gradient_accumulation_steps 1
--num_latent_t 6
--num_latent_t 12
--num_height 480
--num_width 832
--num_frames 21
--num_frames 45
--enable_gradient_checkpointing_type "full"
--generator_4bit_attn True
)
@@ -53,7 +53,7 @@ dataset_args=(
validation_args=(
--log_validation
--validation_dataset_file $VALIDATION_DATASET_FILE
--validation_steps 200
--validation_steps 50
--validation_sampling_steps "50"
--validation_guidance_scale "6.0"
)
+10 -3
View File
@@ -2,7 +2,10 @@
from curses import KEY_B2
import torch
from sageattn import sageattn_blackwell
try:
from sageattn import sageattn_blackwell
except ImportError:
sageattn_blackwell = None
from fastvideo.attention.backends.abstract import (AttentionBackend,
AttentionImpl,
@@ -13,8 +16,12 @@ from fastvideo.logger import init_logger
logger = init_logger(__name__)
from math import sqrt
try:
from flash_attn.flash_attn_interface import _wrapped_flash_attn_forward, _wrapped_flash_attn_backward
except ImportError:
_wrapped_flash_attn_forward = None
_wrapped_flash_attn_backward = None
from flash_attn.flash_attn_interface import _wrapped_flash_attn_forward, _wrapped_flash_attn_backward
logger = init_logger(__name__)
from flashinfer import SfLayout, mm_fp4, nvfp4_quantize
@@ -186,7 +193,7 @@ def flash_attn_backward(q_BLHD, k_BLHD, v_BLHD, out_BLHD, grad_out_BLHD, is_caus
)
return dq_BLHD, dk_BLHD, dv_BLHD, None, None
USE_TORCH_4BIT_FWD = False
USE_TORCH_4BIT_FWD = True
USE_TORCH_16BIT_BWD = True
class _SageAttnBlackwellWith16bitBwd(torch.autograd.Function):
@staticmethod
+70 -70
View File
@@ -928,85 +928,85 @@ class TrainingPipeline(LoRAPipeline, ABC):
num_sp_groups = world_group.world_size // self.sp_group.world_size
# Process each validation prompt for each validation step
# for num_inference_steps in validation_steps:
# logger.info("rank: %s: num_inference_steps: %s",
# self.global_rank,
# num_inference_steps,
# local_main_process_only=False)
# step_videos: list[np.ndarray] = []
# step_captions: list[str] = []
for num_inference_steps in validation_steps:
logger.info("rank: %s: num_inference_steps: %s",
self.global_rank,
num_inference_steps,
local_main_process_only=False)
step_videos: list[np.ndarray] = []
step_captions: list[str] = []
# for validation_batch in validation_dataloader:
# batch = self._prepare_validation_batch(sampling_param,
# training_args,
# validation_batch,
# num_inference_steps)
# logger.info("rank: %s: rank_in_sp_group: %s, batch.prompt: %s",
# self.global_rank,
# self.rank_in_sp_group,
# batch.prompt,
# local_main_process_only=False)
for validation_batch in validation_dataloader:
batch = self._prepare_validation_batch(sampling_param,
training_args,
validation_batch,
num_inference_steps)
logger.info("rank: %s: rank_in_sp_group: %s, batch.prompt: %s",
self.global_rank,
self.rank_in_sp_group,
batch.prompt,
local_main_process_only=False)
# assert batch.prompt is not None and isinstance(
# batch.prompt, str)
# step_captions.append(batch.prompt)
assert batch.prompt is not None and isinstance(
batch.prompt, str)
step_captions.append(batch.prompt)
# # Run validation inference
# output_batch = self.validation_pipeline.forward(
# batch, training_args)
# samples = output_batch.output
# Run validation inference
output_batch = self.validation_pipeline.forward(
batch, training_args)
samples = output_batch.output
# if self.rank_in_sp_group != 0:
# continue
if self.rank_in_sp_group != 0:
continue
# # Process outputs
# video = rearrange(samples, "b c t h w -> t b c h w")
# frames = []
# for x in video:
# x = torchvision.utils.make_grid(x, nrow=6)
# x = x.transpose(0, 1).transpose(1, 2).squeeze(-1)
# frames.append((x * 255).numpy().astype(np.uint8))
# step_videos.append(frames)
# Process outputs
video = rearrange(samples, "b c t h w -> t b c h w")
frames = []
for x in video:
x = torchvision.utils.make_grid(x, nrow=6)
x = x.transpose(0, 1).transpose(1, 2).squeeze(-1)
frames.append((x * 255).numpy().astype(np.uint8))
step_videos.append(frames)
# # Only sp_group leaders (rank_in_sp_group == 0) need to send their
# # results to global rank 0
# if self.rank_in_sp_group == 0:
# if self.global_rank == 0:
# # Global rank 0 collects results from all sp_group leaders
# all_videos = step_videos # Start with own results
# all_captions = step_captions
# Only sp_group leaders (rank_in_sp_group == 0) need to send their
# results to global rank 0
if self.rank_in_sp_group == 0:
if self.global_rank == 0:
# Global rank 0 collects results from all sp_group leaders
all_videos = step_videos # Start with own results
all_captions = step_captions
# # Receive from other sp_group leaders
# for sp_group_idx in range(1, num_sp_groups):
# src_rank = sp_group_idx * self.sp_world_size # Global rank of other sp_group leaders
# recv_videos = world_group.recv_object(src=src_rank)
# recv_captions = world_group.recv_object(src=src_rank)
# all_videos.extend(recv_videos)
# all_captions.extend(recv_captions)
# Receive from other sp_group leaders
for sp_group_idx in range(1, num_sp_groups):
src_rank = sp_group_idx * self.sp_world_size # Global rank of other sp_group leaders
recv_videos = world_group.recv_object(src=src_rank)
recv_captions = world_group.recv_object(src=src_rank)
all_videos.extend(recv_videos)
all_captions.extend(recv_captions)
# video_filenames = []
# for i, (video, caption) in enumerate(
# zip(all_videos, all_captions, strict=True)):
# os.makedirs(training_args.output_dir, exist_ok=True)
# filename = os.path.join(
# training_args.output_dir,
# f"validation_step_{global_step}_inference_steps_{num_inference_steps}_video_{i}.mp4"
# )
# imageio.mimsave(filename, video, fps=sampling_param.fps)
# video_filenames.append(filename)
video_filenames = []
for i, (video, caption) in enumerate(
zip(all_videos, all_captions, strict=True)):
os.makedirs(training_args.output_dir, exist_ok=True)
filename = os.path.join(
training_args.output_dir,
f"validation_step_{global_step}_inference_steps_{num_inference_steps}_video_{i}.mp4"
)
imageio.mimsave(filename, video, fps=sampling_param.fps)
video_filenames.append(filename)
# logs = {
# f"validation_videos_{num_inference_steps}_steps": [
# wandb.Video(filename, caption=caption)
# for filename, caption in zip(
# video_filenames, all_captions, strict=True)
# ]
# }
# wandb.log(logs, step=global_step)
# else:
# # Other sp_group leaders send their results to global rank 0
# world_group.send_object(step_videos, dst=0)
# world_group.send_object(step_captions, dst=0)
logs = {
f"validation_videos_{num_inference_steps}_steps": [
wandb.Video(filename, caption=caption)
for filename, caption in zip(
video_filenames, all_captions, strict=True)
]
}
wandb.log(logs, step=global_step)
else:
# Other sp_group leaders send their results to global rank 0
world_group.send_object(step_videos, dst=0)
world_group.send_object(step_captions, dst=0)
# Re-enable gradients for training
training_args.inference_mode = False
+96 -13
View File
@@ -12,7 +12,90 @@ import numpy as np
from flashinfer import SfLayout, mm_fp4, nvfp4_quantize
import unittest
from typing import Tuple, Dict, Any
from fake_quant_fp4 import fake_quant_fp4
import torch
import triton
import triton.language as tl
import triton.profiler as proton
from triton.tools.tensor_descriptor import TensorDescriptor
from triton.tools.mxfp import MXFP4Tensor, MXScaleTensor
def generate_fake_quant(a_f32: torch.Tensor,
b_f32: torch.Tensor,
vec_size: int = 16,
max_mxf4: float = 6.0,
eps: float = 1e-8):
"""
Fake-quantize fp32 A (M,K) and B (K,N) with nvfp4 (E2M1) using FP8 E4M3 (finite-only) block scales.
Returns dequantized fp32 tensors (A_qdq, B_qdq). Assumes:
- M % 128 == 0, N % 128 == 0, K % (vec_size * 4) == 0
- A is (M, K), B is (K, N)
- Scales are uniform per 128x64 (A) and 64x128 (B) tiles (matching your kernel’s layout)
"""
assert a_f32.is_cuda and b_f32.is_cuda
M, K = a_f32.shape
Kb, N = b_f32.shape
assert Kb == K, "A and B inner dims must match (A:(M,K), B:(K,N))"
assert M % 128 == 0 and N % 128 == 0, "M,N must be multiples of 128"
assert K % (vec_size * 4) == 0, f"K must be multiple of {vec_size*4}"
device = a_f32.device
# ---- tile-scale helpers (produce FP8 E4M3 scales with shape [*, *, 32, 16]) ----
def _tile_scales_A(a_fp32):
m_chunks = M // 128
k_chunks = K // (vec_size * 4) # 64-wide tiles along K
x = a_fp32.view(m_chunks, 128, k_chunks, 4, vec_size)
tile_max = x.abs().amax(dim=(1, 3, 4)) # [m_chunks, k_chunks]
scale = torch.clamp(tile_max / max_mxf4, min=eps) # fp32
s = scale[:, :, None, None].expand(m_chunks, k_chunks, 32, 16)
return s.to(torch.float8_e4m3fn) # [m_chunks, k_chunks, 32, 16]
def _tile_scales_B(b_fp32):
k_chunks = K // (vec_size * 4)
n_chunks = N // 128
x = b_fp32.view(k_chunks, 4, vec_size, n_chunks, 128)
tile_max = x.abs().amax(dim=(1, 2, 4)) # [k_chunks, n_chunks]
scale = torch.clamp(tile_max / max_mxf4, min=eps).t() # [n_chunks, k_chunks]
s = scale[:, :, None, None].expand(n_chunks, k_chunks, 32, 16)
return s.to(torch.float8_e4m3fn) # [n_chunks, k_chunks, 32, 16]
# ---- expand tile scales to per-element fp32 ----
def _expand_A_scale_full(a_scale_fp8):
m_chunks, k_chunks, _, _ = a_scale_fp8.shape
base = a_scale_fp8[..., 0, 0].to(a_f32.dtype) # [m_chunks, k_chunks]
tile = base[:, :, None, None].expand(m_chunks, k_chunks, 128, 64)
return tile.permute(0, 2, 1, 3).reshape(M, k_chunks * 64).contiguous() # [M,K]
def _expand_B_scale_full(b_scale_fp8):
n_chunks, k_chunks, _, _ = b_scale_fp8.shape
base = b_scale_fp8[..., 0, 0].to(b_f32.dtype) # [n_chunks, k_chunks]
tile = base[:, :, None, None].expand(n_chunks, k_chunks, 64, 128)
return tile.permute(1, 2, 0, 3).reshape(k_chunks * 64, n_chunks * 128).contiguous() # [K,N]
# ---- 1) compute FP8 E4M3 tile scales from fp32 ----
a_scale_fp8 = _tile_scales_A(a_f32) # [M//128, K//64, 32, 16]
b_scale_fp8 = _tile_scales_B(b_f32) # [N//128, K//64, 32, 16]
# ---- 2) expand scales to full fp32 for quant/dequant math ----
a_scale_full = _expand_A_scale_full(a_scale_fp8) # [M, K] fp32
b_scale_full = _expand_B_scale_full(b_scale_fp8) # [K, N] fp32
# ---- 3) quantize to nvfp4 codes (E2M1) ----
a_codes = MXFP4Tensor(data=(a_f32 / a_scale_full)).data # [M, K] uint8 (low nibble)
b_codes = MXFP4Tensor(data=(b_f32 / b_scale_full)).data # [K, N] uint8
# ---- 4) dequantize back to fp32 (fake-quant output) ----
a_qdq = MXFP4Tensor(size=(M, K), device=device); a_qdq.data = a_codes
b_qdq = MXFP4Tensor(size=(K, N), device=device); b_qdq.data = b_codes
a_qdq = a_qdq.to(torch.float32).to(a_f32.dtype) * a_scale_full
b_qdq = b_qdq.to(torch.float32).to(b_f32.dtype) * b_scale_full
return a_qdq, b_qdq
class QuantizationPrecisionTest:
"""Test class for comparing quantization precision methods."""
@@ -38,7 +121,8 @@ class QuantizationPrecisionTest:
Returns:
Tuple of (quantized_matrix, inverse_scale_factor)
"""
scale_factor = self._compute_scale_factor(matrix)
# scale_factor = self._compute_scale_factor(matrix)
scale_factor = torch.tensor(1.0, device=matrix.device, dtype=torch.float32)
quantized, inv_scale = nvfp4_quantize(
matrix,
scale_factor,
@@ -127,18 +211,18 @@ class QuantizationPrecisionTest:
"""
# Quantize both matrices
# X_quant, X_inv_scale, X_global_scale_factor = self.quantize_matrix(X)
# W_quant, W_inv_scale, W_global_scale_factor = self.quantize_matrix(W)
# # Dequantize both matrices
# X_dequant = self.dequantize_matrix(X.shape, X_quant, X_inv_scale, X_global_scale_factor)
# W_dequant = self.dequantize_matrix(W.shape, W_quant, W_inv_scale, W_global_scale_factor)
X_quant, X_inv_scale, X_global_scale_factor = self.quantize_matrix(X)
W_quant, W_inv_scale, W_global_scale_factor = self.quantize_matrix(W)
# Dequantize both matrices
X_dequant = self.dequantize_matrix(X.shape, X_quant, X_inv_scale, X_global_scale_factor)
W_dequant = self.dequantize_matrix(W.shape, W_quant, W_inv_scale, W_global_scale_factor)
# result = torch.matmul(X_dequant, W_dequant.T)
result = torch.matmul(X_dequant, W_dequant.T)
X_fake_quant = fake_quant_fp4(X, stochastic_rounding=True, block_size=16, scale_format='e4m3')
W_fake_quant = fake_quant_fp4(W, stochastic_rounding=True, block_size=16, scale_format='e4m3')
result = torch.matmul(X_fake_quant, W_fake_quant.T)
# X_fake_quant, W_fake_quant_T = generate_fake_quant(X, W.T.contiguous())
# import pdb; pdb.set_trace()
# result = torch.matmul(X_fake_quant, W_fake_quant_T)
return result
@@ -161,7 +245,6 @@ class QuantizationPrecisionTest:
result1 = self.real_quant_matmul(X, W)
result2 = self.fake_quant_matmul(X, W)
import pdb; pdb.set_trace()
# Compute reference result (no quantization)
reference = torch.matmul(X, W.T)
# Calculate differences
@@ -206,7 +289,7 @@ class QuantizationPrecisionTest:
# Test cases with different sizes and distributions
test_cases = [
# (X_shape, W_shape, distribution_name)
((64, 128), (256, 128), "small_matrices"),
((128, 128), (256, 128), "small_matrices"),
((256, 512), (1024, 512), "medium_matrices"),
((1024, 2048), (4096, 2048), "large_matrices"),
]
-235
View File
@@ -1,235 +0,0 @@
#!/usr/bin/env python3
"""
Simplified precision test for comparing quantization methods using FlashInfer API.
This test compares:
1. dequant(quant(X) @ quant(W)) - quantized matrix multiplication
2. dequant(quant(X)) @ dequant(quant(W)) - dequantized matrix multiplication
Both methods use 4-bit quantization and produce bf16 matrices.
"""
import torch
import numpy as np
from flashinfer import SfLayout, mm_fp4, nvfp4_quantize
def compute_scale_factor(tensor: torch.Tensor) -> torch.Tensor:
"""Compute scale factor for quantization using FastVideo's method."""
maxabs = tensor.float().abs().nan_to_num().max()
maxabs = torch.maximum(maxabs, torch.tensor(1e-12, device=tensor.device, dtype=maxabs.dtype))
return (448.0 * 6.0) / maxabs
def quantize_matrix(matrix: torch.Tensor, sf_layout: SfLayout = SfLayout.layout_128x4):
"""Quantize matrix to 4-bit using FlashInfer."""
scale_factor = compute_scale_factor(matrix)
quantized, inv_scale = nvfp4_quantize(
matrix,
scale_factor,
sfLayout=sf_layout,
do_shuffle=False
)
return quantized, inv_scale
def method1_quantized_matmul(X: torch.Tensor, W: torch.Tensor, device: str = "cuda", dtype: torch.dtype = torch.bfloat16):
"""
Method 1: dequant(quant(X) @ quant(W))
Perform matrix multiplication in quantized space.
"""
# Quantize both matrices
X_quant, X_inv_scale = quantize_matrix(X)
W_quant, W_inv_scale = quantize_matrix(W)
# Compute alpha scaling factor
alpha = 1.0 / (X_inv_scale * W_inv_scale)
# Perform quantized matrix multiplication
result = torch.empty((X.shape[0], W.shape[0]), device=device, dtype=dtype)
mm_fp4(
X_quant, W_quant.T, X_inv_scale, W_inv_scale.T, alpha,
dtype, result,
block_size=16,
use_8x4_sf_layout=False,
backend="cutlass"
)
return result
def method2_dequantized_matmul(X: torch.Tensor, W: torch.Tensor, device: str = "cuda", dtype: torch.dtype = torch.bfloat16):
"""
Method 2: dequant(quant(X)) @ dequant(quant(W))
Dequantize both matrices first, then perform matrix multiplication.
Note: Since FlashInfer doesn't provide direct dequantization, we approximate
this by using the quantized matrices with identity matrices to extract
the dequantized values.
"""
# Quantize both matrices
X_quant, X_inv_scale = quantize_matrix(X)
W_quant, W_inv_scale = quantize_matrix(W)
# Approximate dequantization by multiplying with identity matrices
# This extracts the dequantized values
X_dequant = torch.empty_like(X)
W_dequant = torch.empty_like(W)
# Dequantize X
identity_X = torch.eye(X.shape[1], device=device, dtype=dtype)
mm_fp4(
X_quant, identity_X, X_inv_scale, X_inv_scale,
torch.tensor(1.0, device=device, dtype=torch.float32),
dtype, X_dequant,
block_size=16,
use_8x4_sf_layout=False,
backend="cutlass"
)
# Dequantize W
identity_W = torch.eye(W.shape[1], device=device, dtype=dtype)
mm_fp4(
W_quant, identity_W, W_inv_scale, W_inv_scale,
torch.tensor(1.0, device=device, dtype=torch.float32),
dtype, W_dequant,
block_size=16,
use_8x4_sf_layout=False,
backend="cutlass"
)
# Perform standard matrix multiplication
result = torch.matmul(X_dequant, W_dequant)
return result
def compare_quantization_methods(X: torch.Tensor, W: torch.Tensor, device: str = "cuda", dtype: torch.dtype = torch.bfloat16):
"""
Compare the two quantization methods and return detailed analysis.
"""
# Ensure matrices are on correct device and dtype
X = X.to(device=device, dtype=dtype)
W = W.to(device=device, dtype=dtype)
# Compute results using both methods
result1 = method1_quantized_matmul(X, W, device, dtype)
result2 = method2_dequantized_matmul(X, W, device, dtype)
# Compute reference result (no quantization)
reference = torch.matmul(X, W)
# Calculate differences
diff_methods = torch.abs(result1 - result2)
diff1_ref = torch.abs(result1 - reference)
diff2_ref = torch.abs(result2 - reference)
# Statistical analysis
analysis = {
'method1_result': result1,
'method2_result': result2,
'reference_result': reference,
'method_difference': {
'max': torch.max(diff_methods).item(),
'mean': torch.mean(diff_methods).item(),
'std': torch.std(diff_methods).item(),
'relative_max': torch.max(diff_methods / (torch.abs(reference) + 1e-8)).item(),
'relative_mean': torch.mean(diff_methods / (torch.abs(reference) + 1e-8)).item(),
},
'method1_vs_reference': {
'max': torch.max(diff1_ref).item(),
'mean': torch.mean(diff1_ref).item(),
'relative_max': torch.max(diff1_ref / (torch.abs(reference) + 1e-8)).item(),
},
'method2_vs_reference': {
'max': torch.max(diff2_ref).item(),
'mean': torch.mean(diff2_ref).item(),
'relative_max': torch.max(diff2_ref / (torch.abs(reference) + 1e-8)).item(),
},
'input_shapes': {
'X': list(X.shape),
'W': list(W.shape),
}
}
return analysis
def run_test_suite():
"""Run comprehensive test suite."""
print("Quantization Precision Test")
print("=" * 50)
# Check device availability
if torch.cuda.is_available():
device = "cuda"
print(f"Using CUDA device: {torch.cuda.get_device_name()}")
else:
device = "cpu"
print("CUDA not available, using CPU")
dtype = torch.bfloat16
# Test cases
test_cases = [
# (X_shape, W_shape, description)
((32, 64), (64, 128), "Small matrices"),
((128, 256), (256, 512), "Medium matrices"),
((256, 512), (512, 1024), "Large matrices"),
((64, 128), (128, 64), "Rectangular matrices"),
]
distributions = [
("Normal", lambda shape: torch.randn(shape, device=device)),
("Uniform", lambda shape: torch.rand(shape, device=device) * 2 - 1),
("Small values", lambda shape: torch.randn(shape, device=device) * 0.1),
("Large values", lambda shape: torch.randn(shape, device=device) * 10),
]
all_max_diffs = []
all_mean_diffs = []
for x_shape, w_shape, size_desc in test_cases:
print(f"\n{size_desc} ({x_shape} x {w_shape}):")
print("-" * 40)
for dist_name, dist_func in distributions:
# Generate test matrices
X = dist_func(x_shape).to(dtype=dtype)
W = dist_func(w_shape).to(dtype=dtype)
# Run comparison
analysis = compare_quantization_methods(X, W, device, dtype)
max_diff = analysis['method_difference']['max']
mean_diff = analysis['method_difference']['mean']
rel_max_diff = analysis['method_difference']['relative_max']
all_max_diffs.append(max_diff)
all_mean_diffs.append(mean_diff)
print(f" {dist_name:12} | Max diff: {max_diff:.6f} | Mean diff: {mean_diff:.6f} | Rel max: {rel_max_diff:.6f}")
# Summary
print(f"\nSummary:")
print(f" Overall max difference: {max(all_max_diffs):.6f}")
print(f" Overall mean difference: {np.mean(all_mean_diffs):.6f}")
print(f" Overall std difference: {np.std(all_mean_diffs):.6f}")
# Test assertions
print(f"\nTest Results:")
print(f" ✓ Max difference < 0.01: {max(all_max_diffs) < 0.01}")
print(f" ✓ Mean difference < 0.001: {np.mean(all_mean_diffs) < 0.001}")
return all_max_diffs, all_mean_diffs
if __name__ == "__main__":
try:
max_diffs, mean_diffs = run_test_suite()
print(f"\n✅ Test completed successfully!")
print(f" Both quantization methods produce similar results within acceptable precision.")
except Exception as e:
print(f"\n❌ Test failed with error: {e}")
import traceback
traceback.print_exc()
+276
View File
@@ -0,0 +1,276 @@
import torch
from triton.tools.tensor_descriptor import TensorDescriptor
from triton.tools.mxfp import MXFP4Tensor
from triton_fp4 import block_scaled_matmul
from flashinfer import SfLayout, mm_fp4, nvfp4_quantize
def triton_nvfp4(A: torch.Tensor,
B: torch.Tensor,
out_dtype: torch.dtype = torch.float16):
"""
Block-scaled nvfp4 GEMM: (A @ B) with A:(M,K), B:(K,N) given in fp32/bf16.
Returns the GEMM output in `out_dtype`.
Requirements (match your kernel):
- M % 128 == 0, N % 128 == 0, K % 64 == 0 (since vec_size=16 and 4 groups → 64)
- Device: CUDA, Blackwell (SM=10)
"""
assert A.is_cuda and B.is_cuda, "Inputs must be on CUDA"
assert A.dtype in (torch.float32, torch.bfloat16), "A must be fp32/bf16"
assert B.dtype in (torch.float32, torch.bfloat16), "B must be fp32/bf16"
M, K = A.shape
Kb, N = B.shape
assert Kb == K, "Inner dims must match"
assert M % 128 == 0 and N % 128 == 0 and K % 64 == 0, "Shape must respect tiling"
# ---- kernel constants / config ----
BLOCK_M = 128
BLOCK_N = 256
BLOCK_K = 256
VEC_SIZE = 16
ELEM_PER_BYTE_A = 2
ELEM_PER_BYTE_B = 2
MAX_MXF4 = 6.0 # nvfp4 (E2M1) max magnitude
EPS = 1e-8
device = A.device
# ---- tile-scale helpers (FP8 E4M3, finite-only) ----
# A tiles are 128 x 64 along (M,K)
def _tile_scales_A(a_fp32):
m_chunks = M // 128
k_chunks = K // (VEC_SIZE * 4) # 64 wide
x = a_fp32.view(m_chunks, 128, k_chunks, 4, VEC_SIZE)
tile_max = x.abs().amax(dim=(1, 3, 4)) # [m_chunks, k_chunks]
scale = torch.clamp(tile_max / MAX_MXF4, min=EPS) # fp32
s = scale[:, :, None, None].expand(m_chunks, k_chunks, 32, 16)
return s.to(torch.float8_e4m3fn) # [m_chunks, k_chunks, 32, 16]
# B tiles are 64 x 128 over (K,N)
def _tile_scales_B(b_fp32):
k_chunks = K // (VEC_SIZE * 4)
n_chunks = N // 128
x = b_fp32.view(k_chunks, 4, VEC_SIZE, n_chunks, 128)
tile_max = x.abs().amax(dim=(1, 2, 4)) # [k_chunks, n_chunks]
scale = torch.clamp(tile_max / MAX_MXF4, min=EPS).t() # [n_chunks, k_chunks]
s = scale[:, :, None, None].expand(n_chunks, k_chunks, 32, 16)
return s.to(torch.float8_e4m3fn) # [n_chunks, k_chunks, 32, 16]
def _expand_A_scale_full(a_scale_fp8):
m_chunks, k_chunks, _, _ = a_scale_fp8.shape
base = a_scale_fp8[..., 0, 0].to(A.dtype) # [m_chunks, k_chunks]
tile = base[:, :, None, None].expand(m_chunks, k_chunks, 128, 64)
return tile.permute(0, 2, 1, 3).reshape(M, k_chunks * 64).contiguous() # [M,K]
def _expand_B_scale_full(b_scale_fp8):
n_chunks, k_chunks, _, _ = b_scale_fp8.shape
base = b_scale_fp8[..., 0, 0].to(B.dtype) # [n_chunks, k_chunks]
tile = base[:, :, None, None].expand(n_chunks, k_chunks, 64, 128)
return tile.permute(1, 2, 0, 3).reshape(k_chunks * 64, n_chunks * 128).contiguous() # [K,N]
# ---- 1) scales from fp32 ----
a_scale_fp8 = _tile_scales_A(A) # [M//128, K//64, 32, 16]
b_scale_fp8 = _tile_scales_B(B) # [N//128, K//64, 32, 16]
a_scale_full = _expand_A_scale_full(a_scale_fp8) # [M,K] fp32
b_scale_full = _expand_B_scale_full(b_scale_fp8) # [K,N] fp32
# ---- 2) quantize to nvfp4 codes (E2M1) ----
a_codes = MXFP4Tensor(data=(A / a_scale_full)).data # [M,K] uint8 (low nibble used)
b_codes_KN = MXFP4Tensor(data=(B / b_scale_full)).data # [K,N] uint8
# Kernel expects B laid out as (N, K) packed along K (like the original example)
b_codes_NK = b_codes_KN.transpose(0, 1).contiguous() # [N,K]
# ---- 3) pack to 2-per-byte along K and build descriptors ----
a_fp4 = MXFP4Tensor(size=(M, K), device=device); a_fp4.data = a_codes
b_fp4_desc = MXFP4Tensor(size=(N, K), device=device); b_fp4_desc.data = b_codes_NK
a_packed = a_fp4.to_packed_tensor(dim=1) # (M, K/2)
b_packed = b_fp4_desc.to_packed_tensor(dim=1) # (N, K/2)
a_desc = TensorDescriptor.from_tensor(a_packed, [BLOCK_M, BLOCK_K // ELEM_PER_BYTE_A])
b_desc = TensorDescriptor.from_tensor(b_packed, [BLOCK_N, BLOCK_K // ELEM_PER_BYTE_B])
# reps for scale swizzle
rep_m = BLOCK_M // 128
rep_n = BLOCK_N // 128
rep_k = BLOCK_K // VEC_SIZE // 4
# reshape FP8 scales into 5D TMA blocks [1, rep_*, rep_k, 2, 256]
a_scale_5d = a_scale_fp8.reshape(1, a_scale_fp8.shape[0], a_scale_fp8.shape[1], 2, 256)
b_scale_5d = b_scale_fp8.reshape(1, b_scale_fp8.shape[0], b_scale_fp8.shape[1], 2, 256)
a_scale_desc = TensorDescriptor.from_tensor(a_scale_5d, [1, rep_m, rep_k, 2, 256])
b_scale_desc = TensorDescriptor.from_tensor(b_scale_5d, [1, rep_n, rep_k, 2, 256])
a_packed_cutlass, a_scale_fp8_cutlass = nvfp4_quantize(
A, torch.tensor(1.0, device=device, dtype=torch.float32), sfLayout=SfLayout.layout_128x4, do_shuffle=False
)
import pdb; pdb.set_trace()
# ---- 4) launch GEMM ----
configs = {
"BLOCK_SIZE_M": BLOCK_M,
"BLOCK_SIZE_N": BLOCK_N,
"BLOCK_SIZE_K": BLOCK_K,
"num_stages": 4,
"ELEM_PER_BYTE_A": ELEM_PER_BYTE_A,
"ELEM_PER_BYTE_B": ELEM_PER_BYTE_B,
"VEC_SIZE": VEC_SIZE,
}
# Uses your already-defined block_scaled_matmul(...)
C = block_scaled_matmul(
a_desc, a_scale_desc,
b_desc, b_scale_desc,
out_dtype, M, N, K,
rep_m, rep_n, rep_k, configs
)
return C
def cutlass_nvfp4(A: torch.Tensor,
B: torch.Tensor,
out_dtype: torch.dtype = torch.bfloat16,
sf_layout: SfLayout = SfLayout.layout_128x4,
block_size: int = 16,
do_shuffle: bool = False,
scale_factor_A: torch.Tensor | float | None = None,
scale_factor_B: torch.Tensor | float | None = None) -> torch.Tensor:
"""
FlashInfer/CUTLASS nvfp4 GEMM: returns (A @ B) using 4-bit E2M1 operands with per-tile scaling.
Args:
A: (M, K) tensor, fp32 or bf16, CUDA.
B: (K, N) tensor, fp32 or bf16, CUDA.
out_dtype: torch.float32, torch.float16, or torch.bfloat16 (recommended).
sf_layout: FlashInfer scale-factor layout (default SfLayout.layout_128x4).
block_size: internal tiling, default 16.
do_shuffle: pass-through to nvfp4_quantize.
scale_factor_A, scale_factor_B: optional global scale factors (float or 0-dim tensor).
If None, defaults to 1.0 (FlashInfer still applies its internal per-tile scales).
Returns:
C: (M, N) tensor in out_dtype
"""
assert A.is_cuda and B.is_cuda, "Inputs must be CUDA tensors"
assert A.dim() == 2 and B.dim() == 2, "A and B must be 2D"
M, K = A.shape
Kb, N = B.shape
assert K == Kb, f"Incompatible shapes: A is (M={M},K={K}), B is (K={Kb},N={N})"
# Cast inputs to supported accumulation input types for FlashInfer quantizer.
# (fp32/bf16 are fine; we keep user dtype if already bf16 to save bandwidth.)
A_in = A if A.dtype in (torch.bfloat16, torch.float32) else A.float()
B_in = B if B.dtype in (torch.bfloat16, torch.float32) else B.float()
B_in = B_in.T.contiguous()
# Choose global scaling (FlashInfer also uses per-tile scales internally).
if scale_factor_A is None:
scale_factor_A = torch.tensor(1.0, device=A.device, dtype=torch.float32)
elif not torch.is_tensor(scale_factor_A):
scale_factor_A = torch.tensor(float(scale_factor_A), device=A.device, dtype=torch.float32)
if scale_factor_B is None:
scale_factor_B = torch.tensor(1.0, device=B.device, dtype=torch.float32)
elif not torch.is_tensor(scale_factor_B):
scale_factor_B = torch.tensor(float(scale_factor_B), device=B.device, dtype=torch.float32)
# Quantize A and B to nvfp4; inv scales are FlashInfer’s per-tile inverse scales.
# Returned quantized matrices are FP4-packed formats understood by mm_fp4.
A_q, A_inv_sf = nvfp4_quantize(
A_in, scale_factor_A, sfLayout=sf_layout, do_shuffle=do_shuffle
)
B_q, B_inv_sf = nvfp4_quantize(
B_in, scale_factor_B, sfLayout=sf_layout, do_shuffle=do_shuffle
)
# Alpha rescales the product of the two global scale factors.
alpha = 1.0 / (scale_factor_A * scale_factor_B)
# Pick FlashInfer flag for the scale-factor layout
use_8x4_sf_layout = (sf_layout == SfLayout.layout_8x4)
# Allocate output and call CUTLASS-backed FP4 GEMM.
C = torch.empty((M, N), device=A.device, dtype=out_dtype)
mm_fp4(
A_q, B_q.T, # note the transpose on B_q (expecting KxN → NxK in kernel)
A_inv_sf, B_inv_sf.T, # matching transposition of per-tile inverse scales
alpha, # global alpha
out_dtype, C, # output dtype & buffer
block_size=block_size,
use_8x4_sf_layout=use_8x4_sf_layout,
backend="cutlass"
)
return C
def main():
import argparse, math
torch.manual_seed(0)
parser = argparse.ArgumentParser()
parser.add_argument("--M", type=int, default=256, help="Rows of A / C (multiple of 128)")
parser.add_argument("--N", type=int, default=256, help="Cols of B / C (multiple of 128)")
parser.add_argument("--K", type=int, default=512, help="Inner dim (multiple of 64)")
parser.add_argument("--dtype", type=str, choices=["fp32", "bf16"], default="bf16")
parser.add_argument("--trials", type=int, default=1)
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args()
if not torch.cuda.is_available():
raise RuntimeError("CUDA required")
M, N, K = args.M, args.N, args.K
assert M % 128 == 0 and N % 128 == 0 and K % 64 == 0, "Shape must satisfy: M%128==0, N%128==0, K%64==0."
dtype = torch.bfloat16 if args.dtype == "bf16" else torch.float32
device = "cuda"
def stats(name, X):
X = X.float()
return f"{name}: max={X.abs().max().item():.6e} mean={X.abs().mean().item():.6e} std={X.std().item():.6e}"
for t in range(args.trials):
# Inputs
A = torch.randn((M, K), device=device, dtype=dtype)
B = torch.randn((K, N), device=device, dtype=dtype)
# GEMMs
C_triton = triton_nvfp4(A, B, out_dtype=torch.bfloat16 if dtype is torch.bfloat16 else torch.float32)
C_cutlass = cutlass_nvfp4(A, B, out_dtype=torch.bfloat16 if dtype is torch.bfloat16 else torch.float32)
# Bring to fp32 for error analysis
Ct = C_triton.float()
Cc = C_cutlass.float()
Cref = (A.float() @ B.float())
# Triton vs Cutlass
diff_tc = (Ct - Cc)
denom_tc = Cc.abs().clamp_min(1e-8)
rel_tc = (diff_tc.abs() / denom_tc)
# Each vs reference (optional sanity)
diff_t_ref = (Ct - Cref)
diff_c_ref = (Cc - Cref)
rel_t_ref = diff_t_ref.abs() / Cref.abs().clamp_min(1e-8)
rel_c_ref = diff_c_ref.abs() / Cref.abs().clamp_min(1e-8)
print("\nTriton vs Cutlass:")
print(f" abs diff -> max={diff_tc.abs().max().item():.6e} mean={diff_tc.abs().mean().item():.6e}")
print(f" rel diff -> max={rel_tc.max().item():.6e} mean={rel_tc.mean().item():.6e}")
print("\nTriton vs Reference:")
print(f" abs diff -> max={diff_t_ref.abs().max().item():.6e} mean={diff_t_ref.abs().mean().item():.6e}")
print(f" rel diff -> max={rel_t_ref.max().item():.6e} mean={rel_t_ref.mean().item():.6e}")
print("\nCutlass vs Reference:")
print(f" abs diff -> max={diff_c_ref.abs().max().item():.6e} mean={diff_c_ref.abs().mean().item():.6e}")
print(f" rel diff -> max={rel_c_ref.max().item():.6e} mean={rel_c_ref.mean().item():.6e}")
if args.verbose:
# a tiny sample print to eyeball values
i, j = 0, 0
print(f"\nSample C[0,0]: triton={Ct[i,j].item():.6e}, cutlass={Cc[i,j].item():.6e}, ref={Cref[i,j].item():.6e}")
if __name__ == "__main__":
main()
+33
View File
@@ -0,0 +1,33 @@
from tensorrt_llm import LLM, SamplingParams
def main():
# Model could accept HF model name, a path to local HF model,
# or TensorRT Model Optimizer's quantized checkpoints like nvidia/Llama-3.1-8B-Instruct-FP8 on HF.
llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0")
# Sample prompts.
prompts = [
"Hello, my name is",
"The capital of France is",
"The future of AI is",
]
# Create a sampling params.
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
for output in llm.generate(prompts, sampling_params):
print(
f"Prompt: {output.prompt!r}, Generated text: {output.outputs[0].text!r}"
)
# Got output like
# Prompt: 'Hello, my name is', Generated text: '\n\nJane Smith. I am a student pursuing my degree in Computer Science at [university]. I enjoy learning new things, especially technology and programming'
# Prompt: 'The president of the United States is', Generated text: 'likely to nominate a new Supreme Court justice to fill the seat vacated by the death of Antonin Scalia. The Senate should vote to confirm the'
# Prompt: 'The capital of France is', Generated text: 'Paris.'
# Prompt: 'The future of AI is', Generated text: 'an exciting time for us. We are constantly researching, developing, and improving our platform to create the most advanced and efficient model available. We are'
if __name__ == '__main__':
main()
+20 -12
View File
@@ -6,7 +6,7 @@ import triton.language as tl
import triton.profiler as proton
from triton.tools.tensor_descriptor import TensorDescriptor
from triton.tools.mxfp import MXFP4Tensor, MXScaleTensor
from test_quantization_precision import generate_fake_quant
VEC_SIZE = 16
MAX_MXF4 = 6.0 # max magnitude for E2M1 (nvfp4) with M∈{0,1}, E∈{0..3}
@@ -142,7 +142,7 @@ def block_scaled_matmul(a_desc, a_scale_desc, b_desc, b_scale_desc, dtype_dst, M
configs["num_stages"],
)
return output
def initialize_block_scaled(M, N, K, block_scale_type="nvfp4", compute_reference=False):
def initialize_block_scaled(M, N, K, block_scale_type="nvfp4", compute_reference=False, input_output_dtype=torch.bfloat16):
assert block_scale_type == "nvfp4", f"Only nvfp4 supported here (got {block_scale_type})"
# --- constants ---
@@ -182,20 +182,20 @@ def initialize_block_scaled(M, N, K, block_scale_type="nvfp4", compute_reference
# --- expand tile scales to per-element fp32 (for quant/dequant math) ---
def _expand_A_scale_full(a_scale_fp8):
m_chunks, k_chunks, _, _ = a_scale_fp8.shape
base = a_scale_fp8[..., 0, 0].to(torch.float32) # [m_chunks, k_chunks]
base = a_scale_fp8[..., 0, 0].to(input_output_dtype) # [m_chunks, k_chunks]
tile = base[:, :, None, None].expand(m_chunks, k_chunks, 128, 64)
return tile.permute(0, 2, 1, 3).reshape(M, k_chunks * 64).contiguous() # [M, K]
def _expand_B_scale_full(b_scale_fp8):
n_chunks, k_chunks, _, _ = b_scale_fp8.shape
base = b_scale_fp8[..., 0, 0].to(torch.float32) # [n_chunks, k_chunks]
base = b_scale_fp8[..., 0, 0].to(input_output_dtype) # [n_chunks, k_chunks]
tile = base[:, :, None, None].expand(n_chunks, k_chunks, 64, 128)
return tile.permute(1, 2, 0, 3).reshape(k_chunks * 64, n_chunks * 128).contiguous() # [K, N]
# --- 1) fp32 sources (pre-quant) ---
torch.manual_seed(0)
a_f32_pre = torch.randn((M, K), device=device, dtype=torch.float32) * 0.5
b_f32_pre = torch.randn((K, N), device=device, dtype=torch.float32) * 0.5
a_f32_pre = torch.randn((M, K), device=device, dtype=input_output_dtype)
b_f32_pre = torch.randn((K, N), device=device, dtype=input_output_dtype)
# --- 2) tile scales (FP8 E4M3, finite-only) ---
a_scale_fp8 = _tile_scales_A(a_f32_pre) # [M//128, K//64, 32, 16]
@@ -237,8 +237,7 @@ def initialize_block_scaled(M, N, K, block_scale_type="nvfp4", compute_reference
a_ref_fp4 = MXFP4Tensor(size=(M, K), device=device); a_ref_fp4.data = a_codes
b_ref_fp4 = MXFP4Tensor(size=(K, N), device=device); b_ref_fp4.data = b_codes_KN
a_ref_f32 = a_ref_fp4.to(torch.float32) * a_scale_full # [M, K]
b_ref_f32 = b_ref_fp4.to(torch.float32) * b_scale_full # [K, N]
a_ref_f32, b_ref_f32 = generate_fake_quant(a_f32_pre, b_f32_pre)
reference = a_ref_f32 @ b_ref_f32 if compute_reference else None
@@ -255,11 +254,11 @@ def initialize_block_scaled(M, N, K, block_scale_type="nvfp4", compute_reference
return a_desc, a_scale_desc, b_desc, b_scale_desc, rep_m, rep_n, rep_k, configs, reference
def validate_block_scaled(M, N, K, block_scale_type="nvfp4"):
def validate_block_scaled(M, N, K, block_scale_type="nvfp4", input_output_dtype=torch.bfloat16):
a_desc, a_scale, b_desc, b_scale, rep_m, rep_n, rep_k, configs, reference = initialize_block_scaled(
M, N, K, block_scale_type, compute_reference=True)
output = block_scaled_matmul(a_desc, a_scale, b_desc, b_scale, torch.float32, M, N, K, rep_m, rep_n, rep_k, configs)
torch.testing.assert_close(reference, output.to(torch.float32), atol=1e-3, rtol=1e-3)
output = block_scaled_matmul(a_desc, a_scale, b_desc, b_scale, input_output_dtype, M, N, K, rep_m, rep_n, rep_k, configs)
torch.testing.assert_close(reference, output.to(input_output_dtype), atol=1e-3, rtol=1e-3)
print(f"✅ (pass {block_scale_type})")
@@ -297,6 +296,7 @@ if __name__ == "__main__":
parser.add_argument("--K_step", type=int, default=512)
parser.add_argument("--bench", action="store_true", default=True)
parser.add_argument("--format", type=str, choices=["nvfp4"], default="nvfp4")
parser.add_argument("--input_output_dtype", type=str, choices=["float32", "float16", "bfloat16"], default="bfloat16")
args = parser.parse_args()
if not supports_block_scaling():
@@ -307,6 +307,14 @@ if __name__ == "__main__":
args.K_step = 1 # doesn't matter as long as it's not 0
torch.manual_seed(42)
dtype =torch.float32 if args.input_output_dtype == "float32" else torch.float16 if args.input_output_dtype == "float16" else torch.bfloat16
validate_block_scaled(8192, 8192, 8192, block_scale_type=args.format, input_output_dtype=dtype)
validate_block_scaled(8192, 8192, 8192, block_scale_type=args.format)
if args.bench:
proton.start("block_scaled_matmul", hook="triton")
proton.deactivate(0) # Skip argument creation
for K in range(args.K_range[0], args.K_range[1] + 1, args.K_step):
bench_block_scaled(K, reps=10000, block_scale_type=args.format)
proton.finalize()
show_profile("block_scaled_matmul")
+302
View File
@@ -0,0 +1,302 @@
import argparse
import torch
import triton
import triton.language as tl
import triton.profiler as proton
from triton.tools.tensor_descriptor import TensorDescriptor
from triton.tools.mxfp import MXFP4Tensor, MXScaleTensor
def is_cuda():
return triton.runtime.driver.active.get_current_target().backend == "cuda"
def supports_block_scaling():
return is_cuda() and torch.cuda.get_device_capability()[0] == 10
def _matmul_launch_metadata(grid, kernel, args):
ret = {}
M, N, K = args["M"], args["N"], args["K"]
kernel_name = kernel.name
if "ELEM_PER_BYTE_A" and "ELEM_PER_BYTE_B" and "VEC_SIZE" in args:
if args["ELEM_PER_BYTE_A"] == 1 and args["ELEM_PER_BYTE_B"] == 1:
kernel_name += "_mxfp8"
elif args["ELEM_PER_BYTE_A"] == 1 and args["ELEM_PER_BYTE_B"] == 2:
kernel_name += "_mixed"
elif args["ELEM_PER_BYTE_A"] == 2 and args["ELEM_PER_BYTE_B"] == 2:
if args["VEC_SIZE"] == 16:
kernel_name += "_nvfp4"
elif args["VEC_SIZE"] == 32:
kernel_name += "_mxfp4"
ret["name"] = f"{kernel_name} [M={M}, N={N}, K={K}]"
ret["flops"] = 2.0 * M * N * K
return ret
@triton.jit(launch_metadata=_matmul_launch_metadata)
def block_scaled_matmul_kernel( #
a_desc, #
a_scale_desc, #
b_desc, #
b_scale_desc, #
c_desc, #
M: tl.constexpr, #
N: tl.constexpr, #
K: tl.constexpr, #
output_type: tl.constexpr, #
ELEM_PER_BYTE_A: tl.constexpr, #
ELEM_PER_BYTE_B: tl.constexpr, #
VEC_SIZE: tl.constexpr, #
BLOCK_M: tl.constexpr, #
BLOCK_N: tl.constexpr, #
BLOCK_K: tl.constexpr, #
rep_m: tl.constexpr, #
rep_n: tl.constexpr, #
rep_k: tl.constexpr, #
NUM_STAGES: tl.constexpr, #
): #
if output_type == 0:
output_dtype = tl.float32
elif output_type == 1:
output_dtype = tl.float16
elif output_type == 2:
output_dtype = tl.float8e4nv
pid = tl.program_id(axis=0)
num_pid_m = tl.cdiv(M, BLOCK_M)
pid_m = pid % num_pid_m
pid_n = pid // num_pid_m
offs_am = pid_m * BLOCK_M
offs_bn = pid_n * BLOCK_N
offs_k_a = 0
offs_k_b = 0
offs_scale_m = pid_m * rep_m
offs_scale_n = pid_n * rep_n
offs_scale_k = 0
MIXED_PREC: tl.constexpr = ELEM_PER_BYTE_A == 1 and ELEM_PER_BYTE_B == 2
accumulator = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
for k in tl.range(0, tl.cdiv(K, BLOCK_K), num_stages=NUM_STAGES):
a = a_desc.load([offs_am, offs_k_a])
b = b_desc.load([offs_bn, offs_k_b])
scale_a = a_scale_desc.load([0, offs_scale_m, offs_scale_k, 0, 0])
scale_b = b_scale_desc.load([0, offs_scale_n, offs_scale_k, 0, 0])
scale_a = scale_a.reshape(rep_m, rep_k, 32, 4, 4).trans(0, 3, 2, 1, 4).reshape(BLOCK_M, BLOCK_K // VEC_SIZE)
scale_b = scale_b.reshape(rep_n, rep_k, 32, 4, 4).trans(0, 3, 2, 1, 4).reshape(BLOCK_N, BLOCK_K // VEC_SIZE)
if MIXED_PREC:
accumulator = tl.dot_scaled(a, scale_a, "e4m3", b.T, scale_b, "e2m1", accumulator)
elif ELEM_PER_BYTE_A == 2 and ELEM_PER_BYTE_B == 2:
accumulator = tl.dot_scaled(a, scale_a, "e2m1", b.T, scale_b, "e2m1", accumulator)
else:
accumulator = tl.dot_scaled(a, scale_a, "e4m3", b.T, scale_b, "e4m3", accumulator)
offs_k_a += BLOCK_K // ELEM_PER_BYTE_A
offs_k_b += BLOCK_K // ELEM_PER_BYTE_B
offs_scale_k += rep_k
c_desc.store([offs_am, offs_bn], accumulator.to(output_dtype))
def block_scaled_matmul(a_desc, a_scale_desc, b_desc, b_scale_desc, dtype_dst, M, N, K, rep_m, rep_n, rep_k, configs):
output = torch.empty((M, N), dtype=dtype_dst, device="cuda")
if dtype_dst == torch.float32:
dtype_dst = 0
elif dtype_dst == torch.float16:
dtype_dst = 1
elif dtype_dst == torch.float8_e4m3fn:
dtype_dst = 2
else:
raise ValueError(f"Unsupported dtype: {dtype_dst}")
BLOCK_M = configs["BLOCK_SIZE_M"]
BLOCK_N = configs["BLOCK_SIZE_N"]
c_desc = TensorDescriptor.from_tensor(output, [BLOCK_M, BLOCK_N])
grid = (triton.cdiv(M, BLOCK_M) * triton.cdiv(N, BLOCK_N), 1)
block_scaled_matmul_kernel[grid](
a_desc,
a_scale_desc,
b_desc,
b_scale_desc,
c_desc,
M,
N,
K,
dtype_dst,
configs["ELEM_PER_BYTE_A"],
configs["ELEM_PER_BYTE_B"],
configs["VEC_SIZE"],
configs["BLOCK_SIZE_M"],
configs["BLOCK_SIZE_N"],
configs["BLOCK_SIZE_K"],
rep_m,
rep_n,
rep_k,
configs["num_stages"],
)
return output
def initialize_block_scaled(M, N, K, block_scale_type="nvfp4", compute_reference=False):
BLOCK_M = 128
BLOCK_N = 256
BLOCK_K = 256 if "fp4" in block_scale_type else 128
VEC_SIZE = 16 if block_scale_type == "nvfp4" else 32
assert block_scale_type in ["nvfp4", "mxfp4", "mxfp8", "mixed"], f"Invalid block scale type: {block_scale_type}"
ELEM_PER_BYTE_A = 2 if "fp4" in block_scale_type else 1
ELEM_PER_BYTE_B = 1 if block_scale_type == "mxfp8" else 2
device = "cuda"
a_ref = MXFP4Tensor(size=(M, K), device=device).random()
# Similar to Hopper's wgmma symmetric fp8 instruction, the RHS is expected
# to be in col-major layout for Blackwell's tcgen05.mma when using fp4 operands.
# To conform to the expected semantics of tl.dot_scaled, (M, K) x (K, N),
# the data is generated in col-major layout, packed along K for fp4, and then
# logically transposed. Note that if one operand is of fp8 precision, unlike Hopper,
# Blackwell supports both row-major and col-major layouts for the RHS matrix.
# For the mixed-precision case, the fp4 RHS can be either in row or col-major layout.
# But for performance reason, it is recommended to use col-major layout. If TMA is used
# for the fp4 RHS operand load in mixed-precision dot, as in this tutorial, it must be
# in col-major layout.
b_ref = MXFP4Tensor(size=(N, K), device=device).random()
if block_scale_type in ["mxfp8", "mixed"]:
a_ref = a_ref.to(torch.float32)
a = a_ref.to(torch.float8_e4m3fn)
else:
# Pack two fp4 elements per byte along K
a = a_ref.to_packed_tensor(dim=1)
if block_scale_type == "mxfp8":
b_ref = b_ref.to(torch.float32)
b = b_ref.to(torch.float8_e4m3fn)
else:
b = b_ref.to_packed_tensor(dim=1)
b_ref = b_ref.to(torch.float32).T
a_desc = TensorDescriptor.from_tensor(a, [BLOCK_M, BLOCK_K // ELEM_PER_BYTE_A])
b_desc = TensorDescriptor.from_tensor(b, [BLOCK_N, BLOCK_K // ELEM_PER_BYTE_B])
a_scale_shape = [M // 128, K // VEC_SIZE // 4, 32, 16]
b_scale_shape = [N // 128, K // VEC_SIZE // 4, 32, 16]
epsilon = 1e-8
a_scale = torch.rand(a_scale_shape, device=device) + epsilon
b_scale = torch.rand(b_scale_shape, device=device) + epsilon
if block_scale_type == "nvfp4":
a_scale = a_scale.to(torch.float8_e4m3fn)
b_scale = b_scale.to(torch.float8_e4m3fn)
a_scale_ref = a_scale
b_scale_ref = b_scale
elif block_scale_type in ["mxfp4", "mxfp8", "mixed"]:
a_scale_ref = MXScaleTensor(a_scale)
b_scale_ref = MXScaleTensor(b_scale)
a_scale = a_scale_ref.data
b_scale = b_scale_ref.data
rep_m = BLOCK_M // 128
rep_n = BLOCK_N // 128
rep_k = BLOCK_K // VEC_SIZE // 4
# Use 5D TMA descriptor [1, rep_m, rep_k, 2, 256] with uint8 elements.
# With 256 elements we better utilize the L2 and don't require the TMA
# engine to emit many small messages (16B) messages as with 32x16xu8.
a_scale_block_shape = [1, rep_m, rep_k, 2, 256]
b_scale_block_shape = [1, rep_n, rep_k, 2, 256]
a_scale = a_scale.reshape(1, a_scale_shape[0], a_scale.shape[1], 2, 256)
b_scale = b_scale.reshape(1, b_scale_shape[0], b_scale.shape[1], 2, 256)
a_scale_desc = TensorDescriptor.from_tensor(a_scale, block_shape=a_scale_block_shape)
b_scale_desc = TensorDescriptor.from_tensor(b_scale, block_shape=b_scale_block_shape)
reference = None
if compute_reference:
a_scale_ref = a_scale_ref.to(torch.float32)
b_scale_ref = b_scale_ref.to(torch.float32)
def unpack_scale(packed):
packed = packed.reshape(*packed.shape[:-2], 32, 4, 4)
num_chunk_m, num_chunk_k, _, _, _ = packed.shape
return packed.permute(0, 3, 2, 1, 4).reshape(num_chunk_m * 128, num_chunk_k * 4).contiguous()
a_scale_ref = unpack_scale(a_scale_ref).repeat_interleave(VEC_SIZE, dim=1)[:M, :K]
b_scale_ref = unpack_scale(b_scale_ref).repeat_interleave(VEC_SIZE, dim=1).T.contiguous()[:K, :N]
reference = torch.matmul(a_ref.to(torch.float32) * a_scale_ref, b_ref * b_scale_ref)
configs = {
"BLOCK_SIZE_M": BLOCK_M,
"BLOCK_SIZE_N": BLOCK_N,
"BLOCK_SIZE_K": BLOCK_K,
"num_stages": 4,
"ELEM_PER_BYTE_A": ELEM_PER_BYTE_A,
"ELEM_PER_BYTE_B": ELEM_PER_BYTE_B,
"VEC_SIZE": VEC_SIZE,
}
return a_desc, a_scale_desc, b_desc, b_scale_desc, rep_m, rep_n, rep_k, configs, reference
def validate_block_scaled(M, N, K, block_scale_type="nvfp4"):
a_desc, a_scale, b_desc, b_scale, rep_m, rep_n, rep_k, configs, reference = initialize_block_scaled(
M, N, K, block_scale_type, compute_reference=True)
output = block_scaled_matmul(a_desc, a_scale, b_desc, b_scale, torch.float16, M, N, K, rep_m, rep_n, rep_k, configs)
torch.testing.assert_close(reference, output.to(torch.float32), atol=1e-3, rtol=1e-3)
print(f"✅ (pass {block_scale_type})")
def bench_block_scaled(K, block_scale_type="nvfp4", reps=10):
assert K % 128 == 0
M = 8192
N = 8192
print(f"Problem Shape = {M}x{N}x{K}")
a_desc, a_scale, b_desc, b_scale, rep_m, rep_n, rep_k, configs, _ = initialize_block_scaled(
M, N, K, block_scale_type, compute_reference=False)
_ = block_scaled_matmul(a_desc, a_scale, b_desc, b_scale, torch.float16, M, N, K, rep_m, rep_n, rep_k, configs)
proton.activate(0)
for _ in range(reps):
_ = block_scaled_matmul(a_desc, a_scale, b_desc, b_scale, torch.float16, M, N, K, rep_m, rep_n, rep_k, configs)
proton.deactivate(0)
print("Done benchmarking")
def show_profile(profile_name):
import triton.profiler.viewer as proton_viewer
metric_names = ["time/ms"]
metric_names = ["tflop/s"] + metric_names
file_name = f"{profile_name}.hatchet"
tree, metrics = proton_viewer.parse(metric_names, file_name)
proton_viewer.print_tree(tree, metrics)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-K", type=int, required=False, default=8192)
parser.add_argument("--K_range", type=int, nargs=2)
parser.add_argument("--K_step", type=int, default=512)
parser.add_argument("--bench", action="store_true", default=True)
parser.add_argument("--format", type=str, choices=["mxfp4", "nvfp4", "mxfp8", "mixed"], default="nvfp4")
args = parser.parse_args()
if not supports_block_scaling():
print("⛔ This example requires GPU support for block scaled matmul")
else:
if args.K and args.K_range is None:
args.K_range = [args.K, args.K]
args.K_step = 1 # doesn't matter as long as it's not 0
torch.manual_seed(42)
validate_block_scaled(8192, 8192, 8192, block_scale_type=args.format)
if args.bench:
proton.start("block_scaled_matmul", hook="triton")
proton.deactivate(0) # Skip argument creation
for K in range(args.K_range[0], args.K_range[1] + 1, args.K_step):
bench_block_scaled(K, reps=10000, block_scale_type=args.format)
proton.finalize()
show_profile("block_scaled_matmul")