mirror of
https://github.com/NVIDIA/cutlass.git
synced 2026-07-17 17:17:40 +00:00
404 lines
16 KiB
Python
404 lines
16 KiB
Python
# Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
|
|
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
# list of conditions and the following disclaimer.
|
|
|
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
# and/or other materials provided with the distribution.
|
|
|
|
# 3. Neither the name of the copyright holder nor the names of its
|
|
# contributors may be used to endorse or promote products derived from
|
|
# this software without specific prior written permission.
|
|
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import torch
|
|
|
|
import cutlass
|
|
import cutlass.torch
|
|
from cutlass.utils.gemm.tensor_utils import decode_float4e2m1fn, unpack_scale_factors
|
|
|
|
import cutlass.operators as ops
|
|
from cutlass.operators.utils.common import ceil_div
|
|
from cutlass.operators.utils.device import device_or_env_target_sm
|
|
from cutlass.operators.utils.dtype import torch_storage_packing_factor
|
|
|
|
from . import reference_check
|
|
|
|
################################################################################
|
|
# Reference computation
|
|
################################################################################
|
|
|
|
|
|
def clamp_to_finite_range(reference: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
|
|
"""
|
|
Clamps the reference tensor to the range of the given dtype if the
|
|
dtype is determined to be a narrow precision dtype.
|
|
"""
|
|
if dtype not in reference_check.get_clamp_torch_dtypes():
|
|
return reference
|
|
if dtype.is_floating_point:
|
|
info = torch.finfo(dtype)
|
|
else:
|
|
raise ValueError(f"Unsupported dtype: {dtype}")
|
|
return torch.clamp(reference, max=info.max, min=info.min)
|
|
|
|
|
|
def dense_gemm_reference(
|
|
args: ops.GemmArguments, ref_as_acc_dtype: bool = False
|
|
) -> torch.Tensor:
|
|
"""
|
|
Computes the reference result for a dense GEMM operation.
|
|
|
|
Args:
|
|
args (cutlass.operators.arguments.GemmArguments): The arguments for the GEMM operation
|
|
ref_as_acc_dtype (bool): Option to return reference tensor as accumulator dtype. The default
|
|
behavior is to return the reference tensor as the output dtype.
|
|
|
|
Returns:
|
|
The reference result for the GEMM operation
|
|
"""
|
|
if not isinstance(args.A, ops.DenseTensor):
|
|
raise ValueError(f"Expected args.A to be a DenseTensor, got {type(args.A)}")
|
|
if not isinstance(args.B, ops.DenseTensor):
|
|
raise ValueError(f"Expected args.B to be a DenseTensor, got {type(args.B)}")
|
|
if not isinstance(args.out, ops.DenseTensor):
|
|
raise ValueError(f"Expected args.out to be a DenseTensor, got {type(args.out)}")
|
|
|
|
A_tensor = args.A.tensor.runtime_tensor
|
|
match A_tensor:
|
|
case torch.Tensor():
|
|
B_tensor = args.B.tensor.runtime_tensor
|
|
assert isinstance(B_tensor, torch.Tensor)
|
|
reference = A_tensor.to(torch.float32) @ B_tensor.to(torch.float32)
|
|
if not ref_as_acc_dtype:
|
|
# pytorch and numpy, following the IEEE standard, converts to Inf or NaN
|
|
# but our kernels use satfinite. To make them the same output we have to
|
|
# clamp on narrow precision output dtypes.
|
|
torch_dtype = cutlass.torch.dtype(args.out.dtype)
|
|
reference = clamp_to_finite_range(reference, torch_dtype).to(
|
|
torch_dtype
|
|
)
|
|
return reference
|
|
case _:
|
|
raise NotImplementedError(f"Unsupported tensor type: {type(A_tensor)}")
|
|
|
|
|
|
def _decode_fp4_packed(tensor: torch.Tensor, pack_dim: int) -> torch.Tensor:
|
|
"""Decode a ``float4_e2m1fn_x2`` tensor to float32 via :func:`decode_float4e2m1fn`.
|
|
|
|
CuTe DSL's :func:`decode_float4e2m1fn` expects 2x-overallocated ``uint8``
|
|
tensors (one byte per logical element) as produced by
|
|
``create_gemm_tensor_torch``. Operator API tests use PyTorch's
|
|
tightly-packed ``float4_e2m1fn_x2`` (two values per byte, ``K_packed =
|
|
K / 2``). This adapter flattens, pads to the over-allocated size, decodes,
|
|
and reshapes back.
|
|
"""
|
|
u8 = tensor.view(torch.uint8)
|
|
ndim = u8.dim()
|
|
|
|
if pack_dim != ndim - 1:
|
|
perm = [i for i in range(ndim) if i != pack_dim] + [pack_dim]
|
|
u8 = u8.permute(perm).contiguous()
|
|
inv_perm = [0] * ndim
|
|
for i, p in enumerate(perm):
|
|
inv_perm[p] = i
|
|
else:
|
|
inv_perm = list(range(ndim))
|
|
|
|
total = u8.numel()
|
|
padded = torch.zeros(1, 2 * total, 1, dtype=torch.uint8, device=tensor.device)
|
|
padded[0, :total, 0] = u8.flatten()
|
|
decoded = decode_float4e2m1fn(padded).flatten()
|
|
|
|
out_shape = list(u8.shape)
|
|
out_shape[-1] *= 2
|
|
return decoded.reshape(out_shape).permute(inv_perm)
|
|
|
|
|
|
def _emulated_scaled_dense_gemm_reference(
|
|
args: ops.GemmArguments,
|
|
ref_as_acc_dtype: bool = False,
|
|
) -> torch.Tensor:
|
|
"""Emulated block-scaled GEMM reference: dequantize, apply scales, matmul.
|
|
|
|
Works for any data type / scale factor combination without relying on
|
|
``torch._scaled_mm``. All arithmetic is performed in float32.
|
|
|
|
The computation is::
|
|
|
|
A_scaled = A_f32 * expand(SFA) # (L, M, K)
|
|
B_scaled = B_f32 * expand(SFB) # (L, K, N)
|
|
D = A_scaled @ B_scaled # (L, M, N)
|
|
|
|
When block-scaled output is needed in the future, a ``scale_out`` parameter
|
|
can be added here without restructuring the computation.
|
|
"""
|
|
A = args.A.quantized.tensor.runtime_tensor
|
|
B = args.B.quantized.tensor.runtime_tensor
|
|
scale_A = args.A.scale.tensor.runtime_tensor
|
|
scale_B = args.B.scale.tensor.runtime_tensor
|
|
out_dtype = cutlass.torch.dtype(args.out.dtype)
|
|
|
|
M, N = args.out.shape[-2:]
|
|
packing_A = torch_storage_packing_factor(args.A.quantized.dtype)
|
|
K = A.shape[-1] * packing_A
|
|
is_2d = A.dim() == 2
|
|
L = 1 if is_2d else A.shape[0]
|
|
sf_vec_size = ops.ScaleMode.numel(args.A.mode)
|
|
|
|
# --- dequantize A and B to float32 ---
|
|
# float4_e2m1fn_x2 is a sub-byte type: .to(float32) triggers a CUDA
|
|
# device-side assert in PyTorch's fetch_and_cast. Decode via CuTe DSL's
|
|
# decode_float4e2m1fn (adapted for PyTorch's tightly-packed format).
|
|
# Each operand is checked independently to support mixed FP4 x FP8.
|
|
A_f32 = (
|
|
_decode_fp4_packed(A, pack_dim=A.dim() - 1)
|
|
if A.dtype == torch.float4_e2m1fn_x2
|
|
else A.to(torch.float32)
|
|
)
|
|
B_f32 = (
|
|
_decode_fp4_packed(B, pack_dim=B.dim() - 2)
|
|
if B.dtype == torch.float4_e2m1fn_x2
|
|
else B.to(torch.float32)
|
|
)
|
|
|
|
if is_2d:
|
|
A_f32 = A_f32.unsqueeze(0) # (1, M, K)
|
|
B_f32 = B_f32.unsqueeze(0) # (1, K, N)
|
|
|
|
# --- obtain dense scale factors ---
|
|
# unpack_scale_factors inverts the Swizzle32x4x4 layout directly from
|
|
# the scale tensor in args, returning (MN, K, L) float32.
|
|
# Permute to (L, MN, K) to match our (L, M, K) / (L, K, N) convention.
|
|
device = A_f32.device
|
|
sfa_expanded = (
|
|
unpack_scale_factors(scale_A.to(torch.float32), sf_vec_size, M, K, L)
|
|
.to(device)
|
|
.permute(2, 0, 1)[:, :M, :K]
|
|
) # (L, M, K)
|
|
sfb_expanded = (
|
|
unpack_scale_factors(scale_B.to(torch.float32), sf_vec_size, N, K, L)
|
|
.to(device)
|
|
.permute(2, 0, 1)[:, :N, :K]
|
|
) # (L, N, K)
|
|
|
|
# --- apply scale factors and matmul ---
|
|
A_scaled = A_f32 * sfa_expanded # (L, M, K)
|
|
B_scaled = B_f32 * sfb_expanded.transpose(1, 2) # (L, K, N)
|
|
reference = A_scaled @ B_scaled # (L, M, N)
|
|
|
|
if is_2d:
|
|
reference = reference.squeeze(0)
|
|
|
|
if not ref_as_acc_dtype:
|
|
reference = clamp_to_finite_range(reference, out_dtype).to(out_dtype)
|
|
|
|
return reference
|
|
|
|
|
|
def scaled_dense_gemm_reference(
|
|
args: ops.GemmArguments, ref_as_acc_dtype: bool = False
|
|
) -> torch.Tensor:
|
|
"""
|
|
Computes the reference result for a scaled dense GEMM operation.
|
|
|
|
Args:
|
|
args (cutlass.operators.arguments.GemmArguments): The arguments for the GEMM operation
|
|
ref_as_acc_dtype (bool): Option to return reference tensor as accumulator dtype. The default
|
|
behavior is to return the reference tensor as the output dtype.
|
|
|
|
Returns:
|
|
The reference result for the scaled dense GEMM operation
|
|
"""
|
|
|
|
if not isinstance(args.A, ops.ScaledOperand):
|
|
raise ValueError(f"Expected args.A to be a ScaledOperand, got {type(args.A)}")
|
|
if not isinstance(args.B, ops.ScaledOperand):
|
|
raise ValueError(f"Expected args.B to be a ScaledOperand, got {type(args.B)}")
|
|
if not isinstance(args.out, ops.DenseTensor):
|
|
raise ValueError(f"Expected args.out to be a DenseTensor, got {type(args.out)}")
|
|
if not isinstance(args.A.scale, ops.DenseTensor):
|
|
raise ValueError(
|
|
f"Expected args.A.scale to be a DenseTensor, got {type(args.A.scale)}"
|
|
)
|
|
if not isinstance(args.B.scale, ops.DenseTensor):
|
|
raise ValueError(
|
|
f"Expected args.B.scale to be a DenseTensor, got {type(args.B.scale)}"
|
|
)
|
|
|
|
A = args.A.quantized.tensor.runtime_tensor
|
|
B = args.B.quantized.tensor.runtime_tensor
|
|
scale_A = args.A.scale.tensor.runtime_tensor
|
|
scale_B = args.B.scale.tensor.runtime_tensor
|
|
acc_dtype = cutlass.torch.dtype(args.accumulator_type)
|
|
out_dtype = cutlass.torch.dtype(args.out.dtype)
|
|
|
|
if not isinstance(A, torch.Tensor):
|
|
raise NotImplementedError(f"Unsupported tensor type: {type(A)}")
|
|
if not isinstance(B, torch.Tensor):
|
|
raise NotImplementedError(f"Unsupported tensor type: {type(B)}")
|
|
if not isinstance(scale_A, torch.Tensor):
|
|
raise NotImplementedError(f"Unsupported tensor type: {type(scale_A)}")
|
|
if not isinstance(scale_B, torch.Tensor):
|
|
raise NotImplementedError(f"Unsupported tensor type: {type(scale_B)}")
|
|
if not isinstance(out_dtype, torch.dtype):
|
|
raise NotImplementedError(f"Unsupported dtype type: {type(out_dtype)}")
|
|
|
|
# torch._scaled_mm supports FP4 only with Float8E4M3FN scale factors
|
|
# (matching CuTe DSL's is_emulated_dtype). Configs torch can't handle (e.g.
|
|
# FP4 + Float8E8M0FNU, mixed FP4 x FP8) use the emulated reference, which
|
|
# supports 2D/3D operands and arbitrary N -- returning here avoids the 3D-only
|
|
# N-padding below. Each operand is checked independently for mixed configs.
|
|
use_emulated = (
|
|
A.dtype == torch.float4_e2m1fn_x2 and scale_A.dtype != torch.float8_e4m3fn
|
|
) or (B.dtype == torch.float4_e2m1fn_x2 and scale_B.dtype != torch.float8_e4m3fn)
|
|
if use_emulated:
|
|
return _emulated_scaled_dense_gemm_reference(args, ref_as_acc_dtype)
|
|
|
|
# torch._scaled_mm currently requires K and N to be divisible by 16.
|
|
# Given that torch also only supports TN layout for now, and data types are
|
|
# FP8 or smaller, K divisibility >= (alignment_bytes * 8 // dtype.width) = (16 * 8 // 8) = 16.
|
|
# Thus, we only need to pad N. (Operands here are 3D.)
|
|
N = B.shape[-1]
|
|
padded_N = ceil_div(N, 16) * 16
|
|
if padded_N != N:
|
|
N_pad = padded_N - N
|
|
if B.dtype == torch.float4_e2m1fn_x2:
|
|
# Packed FP4: pad at byte level since float32 round-trip changes shape.
|
|
# float4_e2m1fn_x2 and int8 share 1-byte element size, so view is safe.
|
|
B_padded = torch.zeros(
|
|
(B.shape[0], B.shape[1], padded_N),
|
|
dtype=torch.int8,
|
|
device=B.device,
|
|
)
|
|
B_padded[:, :, :N] = B.view(dtype=torch.int8)
|
|
B_padded = B_padded.view(dtype=torch.float4_e2m1fn_x2)
|
|
else:
|
|
B_padded = (
|
|
torch.nn.functional.pad(
|
|
B.transpose(1, 2).to(torch.float32),
|
|
(0, 0, 0, N_pad),
|
|
mode="constant",
|
|
value=0,
|
|
)
|
|
.to(B.dtype)
|
|
.transpose(1, 2)
|
|
)
|
|
else:
|
|
B_padded = B
|
|
|
|
# torch._scaled_mm does not support batch mode. Iterate through each
|
|
# problem in the batch.
|
|
L, M, N = A.shape[0], A.shape[1], B.shape[2]
|
|
scale_A = scale_A.view(L, -1).contiguous()
|
|
scale_B = scale_B.view(L, -1).contiguous()
|
|
reference = torch.empty((L, M, padded_N), device=A.device, dtype=acc_dtype)
|
|
|
|
for l_idx in range(L):
|
|
# Use out type of F32 and then convert to out_dtype due to cuBLAS
|
|
# errors occasionally thrown with F8 types.
|
|
# See https://github.com/pytorch/pytorch/issues/160816
|
|
try:
|
|
reference[l_idx, :, :] = torch._scaled_mm(
|
|
A[l_idx, :, :],
|
|
B_padded[l_idx, :, :],
|
|
scale_a=scale_A[l_idx, :],
|
|
scale_b=scale_B[l_idx, :],
|
|
out_dtype=acc_dtype,
|
|
)
|
|
except RuntimeError:
|
|
# cuBLAS may not have an algorithm for this configuration;
|
|
# fall back to emulated reference.
|
|
return _emulated_scaled_dense_gemm_reference(args, ref_as_acc_dtype)
|
|
|
|
if not ref_as_acc_dtype:
|
|
reference = clamp_to_finite_range(reference, out_dtype).to(out_dtype)
|
|
|
|
# Remove padding from reference
|
|
reference = reference[:, :, :N]
|
|
|
|
return reference
|
|
|
|
|
|
def reference(args: ops.GemmArguments, ref_as_acc_dtype: bool = False) -> torch.Tensor:
|
|
"""
|
|
Computes the reference result for a GEMM operation.
|
|
|
|
Args:
|
|
args (cutlass.operators.arguments.GemmArguments): The arguments for the GEMM operation
|
|
ref_as_acc_dtype (bool): Option to return reference tensor as accumulator dtype. The default
|
|
behavior is to return the reference tensor as the output dtype.
|
|
|
|
Returns:
|
|
The reference result for the GEMM operation
|
|
"""
|
|
if (
|
|
isinstance(args.A, ops.DenseTensor)
|
|
and isinstance(args.B, ops.DenseTensor)
|
|
and isinstance(args.out, ops.DenseTensor)
|
|
):
|
|
return dense_gemm_reference(args, ref_as_acc_dtype=ref_as_acc_dtype)
|
|
elif (
|
|
isinstance(args.A, ops.ScaledOperand)
|
|
and isinstance(args.B, ops.ScaledOperand)
|
|
and isinstance(args.out, ops.DenseTensor)
|
|
):
|
|
return scaled_dense_gemm_reference(args, ref_as_acc_dtype=ref_as_acc_dtype)
|
|
else:
|
|
raise ValueError(f"No reference implementation found for {args}")
|
|
|
|
|
|
# Byte patterns whose two FP4 (E2M1) nibbles each decode to a small integer in
|
|
# {0, +1, -1} (0x0 -> 0.0, 0x2 -> 1.0, 0xA -> -1.0).
|
|
_FP4_INT_BYTE_PATTERNS = [0x00, 0x02, 0x0A, 0x20, 0x22, 0x2A, 0xA0, 0xA2, 0xAA]
|
|
|
|
|
|
def make_mxfp4_kmajor(rows: int, k: int) -> torch.Tensor:
|
|
"""Build a K-major, tightly-packed MXFP4 operand with integer values in {-1, 0, 1}.
|
|
|
|
Returns a ``(rows, k // 2)`` ``torch.float4_e2m1fn_x2`` CUDA tensor (logical
|
|
``(rows, k)``, K-major). With integer operands and power-of-two scales (see
|
|
make_random_pow2_scale every dequantized product and partial sum is
|
|
an exact integer in float32, so the kernel and scaled_dense_gemm_reference
|
|
accumulate bit-identically.
|
|
|
|
Args:
|
|
rows (int): The non-K (M or N) dimension size.
|
|
k (int): The K dimension size.
|
|
|
|
Raises:
|
|
ValueError: If ``k`` is odd.
|
|
"""
|
|
if k % 2 != 0:
|
|
raise ValueError(f"K must be even to tightly pack FP4, got {k}")
|
|
patterns = torch.tensor(_FP4_INT_BYTE_PATTERNS, dtype=torch.uint8, device="cuda")
|
|
idx = torch.randint(0, patterns.numel(), (rows, k // 2), device="cuda")
|
|
return patterns[idx].view(torch.float4_e2m1fn_x2)
|
|
|
|
|
|
def make_random_pow2_scale(numel: int) -> torch.Tensor:
|
|
"""Random E8M0 scale factors drawn from the powers of two ``{1, 2, 4}``.
|
|
|
|
Power-of-two scales have no fractional bits, so scaling integer operands keeps
|
|
every product and partial sum an exact integer in float32. Accumulation is
|
|
then order-independent.
|
|
"""
|
|
lut = torch.tensor([1.0, 2.0, 4.0], device="cuda")
|
|
idx = torch.randint(0, lut.numel(), (numel,), device="cuda")
|
|
return lut[idx].to(torch.float8_e8m0fnu)
|