[FIX] Always support TP > 4 for FP4 Gemm (#17300)
This commit is contained in:
@@ -158,6 +158,103 @@ CUTEDSL_MOE_SCALAR_INPUT_SCALE = get_bool_env_var(
|
|||||||
"SGLANG_CUTEDSL_MOE_SCALAR_INPUT_SCALE", "true"
|
"SGLANG_CUTEDSL_MOE_SCALAR_INPUT_SCALE", "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# FP4 GEMM alignment constant - CUTLASS/FlashInfer kernels require dimensions divisible by 32
|
||||||
|
FP4_GEMM_ALIGNMENT = 32
|
||||||
|
|
||||||
|
|
||||||
|
def round_up_to_multiple(x: int, m: int) -> int:
|
||||||
|
"""Round up x to the nearest multiple of m."""
|
||||||
|
return (x + m - 1) // m * m
|
||||||
|
|
||||||
|
|
||||||
|
def pad_nvfp4_weight(
|
||||||
|
weight: torch.Tensor,
|
||||||
|
n_alignment: int = FP4_GEMM_ALIGNMENT,
|
||||||
|
k_alignment: int = FP4_GEMM_ALIGNMENT,
|
||||||
|
) -> tuple[torch.Tensor, int]:
|
||||||
|
"""
|
||||||
|
Pad packed NVFP4 weights to satisfy alignment constraints for FP4 GEMM kernels.
|
||||||
|
|
||||||
|
Different backends have different alignment requirements:
|
||||||
|
- CUTLASS/cuDNN: N % 32 == 0, K % 32 == 0
|
||||||
|
- TRTLLM: N % 128 == 0 (for shuffle_matrix_sf_a), K padding handled separately
|
||||||
|
|
||||||
|
Args:
|
||||||
|
weight: Packed FP4 weight tensor of shape [N, K//2] (2 FP4 values per byte)
|
||||||
|
n_alignment: Required alignment for N dimension (default 32, use 128 for TRTLLM)
|
||||||
|
k_alignment: Required alignment for K dimension (default 32, use 0 to skip)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (padded_weight, weights_padding_cols) where weights_padding_cols
|
||||||
|
is the number of columns added for K-dimension padding (in bytes).
|
||||||
|
"""
|
||||||
|
weight_current_rows = weight.shape[0] # N dimension
|
||||||
|
weight_current_col_bytes = weight.shape[1] # K//2 (packed)
|
||||||
|
|
||||||
|
# Calculate padding for N dimension (rows)
|
||||||
|
pad_rows = 0
|
||||||
|
if n_alignment > 0 and weight_current_rows % n_alignment != 0:
|
||||||
|
total_rows = round_up_to_multiple(weight_current_rows, n_alignment)
|
||||||
|
pad_rows = total_rows - weight_current_rows
|
||||||
|
|
||||||
|
# Calculate padding for K dimension (columns)
|
||||||
|
# 2 FP4 items are packed per byte in the input dimension
|
||||||
|
weight_current_col_elements = weight_current_col_bytes * 2
|
||||||
|
pad_cols_bytes = 0
|
||||||
|
if k_alignment > 0 and weight_current_col_elements % k_alignment != 0:
|
||||||
|
total_cols = round_up_to_multiple(weight_current_col_elements, k_alignment)
|
||||||
|
pad_cols = total_cols - weight_current_col_elements
|
||||||
|
# pad_cols is in elements, but padding is in bytes (2 elements per byte)
|
||||||
|
pad_cols_bytes = pad_cols // 2
|
||||||
|
|
||||||
|
# Apply padding in a single operation if needed
|
||||||
|
# For 2D tensor, pad argument is (pad_left, pad_right, pad_top, pad_bottom)
|
||||||
|
if pad_rows > 0 or pad_cols_bytes > 0:
|
||||||
|
weight = torch.nn.functional.pad(
|
||||||
|
weight, (0, pad_cols_bytes, 0, pad_rows)
|
||||||
|
).contiguous()
|
||||||
|
|
||||||
|
return weight, pad_cols_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def pad_nvfp4_activation_for_cutlass(
|
||||||
|
x_fp4: torch.Tensor,
|
||||||
|
weights_padding_cols: int,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Pad packed FP4 activations to match the K-dimension padding applied to weights.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x_fp4: Packed FP4 activation tensor
|
||||||
|
weights_padding_cols: Number of padding columns (in bytes) from weight padding
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Padded activation tensor
|
||||||
|
"""
|
||||||
|
if weights_padding_cols > 0:
|
||||||
|
return torch.nn.functional.pad(x_fp4, (0, weights_padding_cols)).contiguous()
|
||||||
|
return x_fp4
|
||||||
|
|
||||||
|
|
||||||
|
def slice_nvfp4_output(
|
||||||
|
out: torch.Tensor,
|
||||||
|
output_size: int,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Slice the output tensor to remove padding in N dimension if weight was padded.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
out: Output tensor from FP4 GEMM
|
||||||
|
output_size: Original output size before padding
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Sliced output tensor with padding removed
|
||||||
|
"""
|
||||||
|
if out.shape[-1] != output_size:
|
||||||
|
return out[..., :output_size].contiguous()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# TODO make it true by default when the DeepEP PR is merged
|
# TODO make it true by default when the DeepEP PR is merged
|
||||||
MOE_NVFP4_DISPATCH = envs.SGLANG_MOE_NVFP4_DISPATCH.get()
|
MOE_NVFP4_DISPATCH = envs.SGLANG_MOE_NVFP4_DISPATCH.get()
|
||||||
# Supported activation schemes for the current configuration
|
# Supported activation schemes for the current configuration
|
||||||
@@ -1059,26 +1156,66 @@ class ModelOptFp4LinearMethod(LinearMethodBase):
|
|||||||
layer.input_scale_inv = Parameter(
|
layer.input_scale_inv = Parameter(
|
||||||
(1 / input_scale_2).to(torch.float32), requires_grad=False
|
(1 / input_scale_2).to(torch.float32), requires_grad=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Store original output size before any padding
|
||||||
|
layer.output_size_per_partition = layer.weight.shape[0]
|
||||||
|
|
||||||
if get_fp4_gemm_runner_backend().is_flashinfer_trtllm():
|
if get_fp4_gemm_runner_backend().is_flashinfer_trtllm():
|
||||||
# FlashInfer TRTLLM FP4 GEMM requires a different weight layout.
|
# FlashInfer TRTLLM FP4 GEMM requires a different weight layout.
|
||||||
# FlashInfer provides nvfp4_quantize to quantize + shuffle the
|
# FlashInfer provides nvfp4_quantize to quantize + shuffle the
|
||||||
# layout but we use our own quantization so we have to call
|
# layout but we use our own quantization so we have to call
|
||||||
# shuffles ourselves.
|
# shuffles ourselves.
|
||||||
|
#
|
||||||
|
# Alignment requirements:
|
||||||
|
# - shuffle_matrix_a: weight.shape[0] (N) % 32 == 0
|
||||||
|
# - shuffle_matrix_sf_a: scale.shape[0] (N) % 128 == 0, scale.shape[1] (K/16) % 4 == 0
|
||||||
|
# We pad N to multiple of 128 and K/16 to multiple of 4.
|
||||||
from flashinfer import shuffle_matrix_a, shuffle_matrix_sf_a
|
from flashinfer import shuffle_matrix_a, shuffle_matrix_sf_a
|
||||||
|
|
||||||
weight = layer.weight
|
# Pad weight N dimension to 128
|
||||||
|
weight, _ = pad_nvfp4_weight(
|
||||||
|
layer.weight.data, n_alignment=128, k_alignment=0
|
||||||
|
)
|
||||||
|
# Pad scale N dimension to match weight
|
||||||
scale = layer.weight_scale
|
scale = layer.weight_scale
|
||||||
|
if scale.shape[0] != weight.shape[0]:
|
||||||
|
pad_n = weight.shape[0] - scale.shape[0]
|
||||||
|
scale = torch.nn.functional.pad(scale, (0, 0, 0, pad_n))
|
||||||
|
|
||||||
|
# Pad K dimension: scale K/16 must be multiple of 4
|
||||||
|
scale_k = scale.shape[1] # K/16
|
||||||
|
weights_padding_cols = 0
|
||||||
|
if scale_k % 4 != 0:
|
||||||
|
padded_scale_k = round_up_to_multiple(scale_k, 4)
|
||||||
|
pad_scale_k = padded_scale_k - scale_k
|
||||||
|
# Pad scale K/16 dimension
|
||||||
|
scale = torch.nn.functional.pad(scale, (0, pad_scale_k, 0, 0))
|
||||||
|
# Pad weight K/2 dimension correspondingly (K/2 = K/16 * 8)
|
||||||
|
pad_weight_k = pad_scale_k * 8
|
||||||
|
weight = torch.nn.functional.pad(weight, (0, pad_weight_k, 0, 0))
|
||||||
|
# Store K padding for activation padding in apply()
|
||||||
|
weights_padding_cols = pad_weight_k
|
||||||
|
|
||||||
|
# Shuffle for TRTLLM layout
|
||||||
epilogue_tile_m = 128
|
epilogue_tile_m = 128
|
||||||
|
shuffled_scale_shape = scale.shape
|
||||||
weight = shuffle_matrix_a(weight.view(torch.uint8), epilogue_tile_m)
|
weight = shuffle_matrix_a(weight.view(torch.uint8), epilogue_tile_m)
|
||||||
scale = (
|
scale = (
|
||||||
shuffle_matrix_sf_a(scale.view(torch.uint8), epilogue_tile_m)
|
shuffle_matrix_sf_a(scale.view(torch.uint8), epilogue_tile_m)
|
||||||
.reshape(scale.shape)
|
.reshape(shuffled_scale_shape)
|
||||||
.view(torch.float8_e4m3fn)
|
.view(torch.float8_e4m3fn)
|
||||||
)
|
)
|
||||||
|
|
||||||
layer.weight_scale_interleaved = Parameter(scale, requires_grad=False)
|
layer.weight_scale_interleaved = Parameter(scale, requires_grad=False)
|
||||||
layer.weight = Parameter(weight, requires_grad=False)
|
layer.weight = Parameter(weight, requires_grad=False)
|
||||||
|
layer.weights_padding_cols = weights_padding_cols
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Pad weights for CUTLASS/FlashInfer kernel alignment (K and N divisible by 32)
|
||||||
|
weight, weights_padding_cols = pad_nvfp4_weight(layer.weight.data)
|
||||||
|
layer.weights_padding_cols = weights_padding_cols
|
||||||
|
layer.weight = Parameter(weight, requires_grad=False)
|
||||||
|
|
||||||
# Pad and blockwise interleave weight_scale
|
# Pad and blockwise interleave weight_scale
|
||||||
scales = layer.weight_scale
|
scales = layer.weight_scale
|
||||||
scale_ndim = scales.ndim
|
scale_ndim = scales.ndim
|
||||||
@@ -1086,9 +1223,8 @@ class ModelOptFp4LinearMethod(LinearMethodBase):
|
|||||||
scales = scales.unsqueeze(0)
|
scales = scales.unsqueeze(0)
|
||||||
assert scales.ndim == 3
|
assert scales.ndim == 3
|
||||||
B, M, K = scales.shape
|
B, M, K = scales.shape
|
||||||
round_up_multiple = lambda x, m: (x + m - 1) // m * m
|
M_padded = round_up_to_multiple(M, 128)
|
||||||
M_padded = round_up_multiple(M, 128)
|
K_padded = round_up_to_multiple(K, 4)
|
||||||
K_padded = round_up_multiple(K, 4)
|
|
||||||
padded_scales = torch.zeros((B, M_padded, K_padded), dtype=scales.dtype)
|
padded_scales = torch.zeros((B, M_padded, K_padded), dtype=scales.dtype)
|
||||||
padded_scales[:B, :M, :K] = scales
|
padded_scales[:B, :M, :K] = scales
|
||||||
batches, rows, cols = padded_scales.shape
|
batches, rows, cols = padded_scales.shape
|
||||||
@@ -1112,8 +1248,11 @@ class ModelOptFp4LinearMethod(LinearMethodBase):
|
|||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
output_dtype = x.dtype
|
output_dtype = x.dtype
|
||||||
x_m, _ = x.shape
|
x_m, _ = x.shape
|
||||||
|
|
||||||
|
# Get original output size (before padding) and padded weight size
|
||||||
|
output_size = layer.output_size_per_partition
|
||||||
w_n, _ = layer.weight.shape
|
w_n, _ = layer.weight.shape
|
||||||
output_shape = [x_m, w_n]
|
output_shape = [x_m, output_size]
|
||||||
|
|
||||||
# Quantize BF16 or FP16 to (FP4 and interleaved block scale)
|
# Quantize BF16 or FP16 to (FP4 and interleaved block scale)
|
||||||
x_fp4, x_scale_interleaved = fp4_quantize(x, layer.input_scale_inv)
|
x_fp4, x_scale_interleaved = fp4_quantize(x, layer.input_scale_inv)
|
||||||
@@ -1123,11 +1262,16 @@ class ModelOptFp4LinearMethod(LinearMethodBase):
|
|||||||
assert layer.weight_scale_interleaved.dtype == torch.float8_e4m3fn
|
assert layer.weight_scale_interleaved.dtype == torch.float8_e4m3fn
|
||||||
assert layer.alpha.dtype == torch.float32
|
assert layer.alpha.dtype == torch.float32
|
||||||
|
|
||||||
|
# Pad activations to match weight K-dimension padding
|
||||||
|
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 = layer.weight
|
||||||
w_scale_interleaved = layer.weight_scale_interleaved
|
w_scale_interleaved = layer.weight_scale_interleaved
|
||||||
if enable_flashinfer_fp4_gemm:
|
if enable_flashinfer_fp4_gemm:
|
||||||
w = layer.weight.T
|
w = layer.weight.T
|
||||||
w_scale_interleaved = layer.weight_scale_interleaved.T
|
w_scale_interleaved = layer.weight_scale_interleaved.T
|
||||||
|
|
||||||
out = fp4_gemm(
|
out = fp4_gemm(
|
||||||
x_fp4,
|
x_fp4,
|
||||||
w,
|
w,
|
||||||
@@ -1137,6 +1281,10 @@ class ModelOptFp4LinearMethod(LinearMethodBase):
|
|||||||
output_dtype,
|
output_dtype,
|
||||||
w_n,
|
w_n,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Slice output to remove N-dimension padding
|
||||||
|
out = slice_nvfp4_output(out, output_size)
|
||||||
|
|
||||||
if bias is not None:
|
if bias is not None:
|
||||||
out = out + bias
|
out = out + bias
|
||||||
return out.view(*output_shape)
|
return out.view(*output_shape)
|
||||||
|
|||||||
Reference in New Issue
Block a user