Skip to content

Commit 110791f

Browse files
authored
Add MXFP8 support with cuBLASMp (#3145)
* Add MXFP8 support to cuBLASMp GEMM Signed-off-by: Almog Segal <asegal@nvidia.com> * Enable cuBLASMp MXFP8 overlap tests Signed-off-by: Almog Segal <asegal@nvidia.com> * Fix MXFP8 weight-gradient preparation with cuBLASMp Gather columnwise MXFP8 gradients from the original input for weight gradients, and keep Userbuffers-only external overlap out of the cuBLASMp backward path. Signed-off-by: Almog Segal <asegal@nvidia.com> * Require cuBLASMp 0.8.1 for MXFP8 support Signed-off-by: Almog Segal <asegal@nvidia.com> --------- Signed-off-by: Almog Segal <asegal@nvidia.com>
1 parent 73b5970 commit 110791f

5 files changed

Lines changed: 108 additions & 52 deletions

File tree

‎tests/pytorch/distributed/test_comm_gemm_overlap.py‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@
3333
# to avoid numerical tolerance issues of doing comm gemm overlap, limit the number of GPUs used
3434
MAX_GPUS_TO_USE = 4
3535

36+
COMM_GEMM_QUANTIZATION_PARAMS = [
37+
pytest.param(False, "none", id="ub-bf16"),
38+
pytest.param(False, "fp8", id="ub-fp8"),
39+
pytest.param(False, "mxfp8", id="ub-mxfp8"),
40+
pytest.param(True, "none", id="cublasmp-bf16"),
41+
pytest.param(True, "fp8", id="cublasmp-fp8"),
42+
pytest.param(True, "mxfp8", id="cublasmp-mxfp8"),
43+
]
44+
3645
TEST_ROOT = Path(__file__).parent.resolve()
3746
NUM_PROCS: int = min(torch.cuda.device_count(), MAX_GPUS_TO_USE)
3847
LAUNCH_CMD = ["torchrun", f"--nproc_per_node={NUM_PROCS}"]
@@ -102,10 +111,6 @@ def _run_gemm_with_overlap(
102111
if use_cublasmp:
103112
if not tex.nvte_built_with_cublasmp():
104113
pytest.skip("Transformer Engine not built with cuBLASMp (NVTE_WITH_CUBLASMP=0).")
105-
if quantization == "mxfp8":
106-
pytest.skip(
107-
"cuBLASMp comm+GEMM overlap does not yet support MXFP8 (block scaling)."
108-
)
109114
if comm_type == "RS" and not p2p and not tex.device_supports_multicast():
110115
pytest.skip(
111116
"cuBLASMp non-P2P reduce-scatter requires NVSwitch (multicast support)."
@@ -159,8 +164,6 @@ def _run_layer_with_overlap(
159164
if use_cublasmp:
160165
if not tex.nvte_built_with_cublasmp():
161166
pytest.skip("Transformer Engine not built with cuBLASMp (NVTE_WITH_CUBLASMP=0).")
162-
if fp8 and quantization == "mxfp8":
163-
pytest.skip("cuBLASMp comm+GEMM overlap does not yet support MXFP8 (block scaling).")
164167
test_cmd.append("--use-cublasmp")
165168

166169
test_env = os.environ.copy()
@@ -182,8 +185,7 @@ def _run_layer_with_overlap(
182185
_assert_subprocess_succeeded(result)
183186

184187

185-
@pytest.mark.parametrize("use_cublasmp", (False, True))
186-
@pytest.mark.parametrize("quantization", ("none", "fp8", "mxfp8"))
188+
@pytest.mark.parametrize("use_cublasmp,quantization", COMM_GEMM_QUANTIZATION_PARAMS)
187189
@pytest.mark.parametrize("aggregate", (False, True))
188190
def test_split_all_gather_overlaps(quantization, aggregate, use_cublasmp):
189191
"""
@@ -193,8 +195,7 @@ def test_split_all_gather_overlaps(quantization, aggregate, use_cublasmp):
193195
_run_gemm_with_overlap("AG", False, True, False, aggregate, quantization, use_cublasmp)
194196

195197

196-
@pytest.mark.parametrize("use_cublasmp", (False, True))
197-
@pytest.mark.parametrize("quantization", ("none", "fp8", "mxfp8"))
198+
@pytest.mark.parametrize("use_cublasmp,quantization", COMM_GEMM_QUANTIZATION_PARAMS)
198199
@pytest.mark.parametrize("p2p", (False, True))
199200
def test_split_reduce_scatter_overlaps(quantization, p2p, use_cublasmp):
200201
"""

‎transformer_engine/common/CMakeLists.txt‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -427,12 +427,12 @@ if (NVTE_WITH_CUBLASMP)
427427
NAMES nccl libnccl
428428
PATH_SUFFIXES lib
429429
REQUIRED)
430-
# cuBLASMp 0.8 is the first release with CUDA-graph-safe overlap algos,
430+
# cuBLASMp 0.8.1 adds MXFP8 support to the CUDA-graph-safe overlap algos,
431431
# and NCCL 2.30 is the first release with graph-safe one-sided RMA
432432
# primitives (ncclPutSignal/ncclWaitSignal) that those algos use.
433-
if (CUBLASMP_VERSION VERSION_LESS 0.8.0)
433+
if (CUBLASMP_VERSION VERSION_LESS 0.8.1)
434434
message(FATAL_ERROR
435-
"NVTE_WITH_CUBLASMP requires cuBLASMp >= 0.8.0, but found cuBLASMp "
435+
"NVTE_WITH_CUBLASMP requires cuBLASMp >= 0.8.1, but found cuBLASMp "
436436
"${CUBLASMP_VERSION} in ${CUBLASMP_INCLUDE_DIR}/cublasmp.h")
437437
endif()
438438
if (NCCL_VERSION VERSION_LESS 2.30.0)

‎transformer_engine/common/comm_gemm/comm_gemm.cpp‎

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -260,45 +260,75 @@ void cublasmp_gemm(InitMatricesFn init_matrices_fn, NVTECommGemmCtx* ctx, NVTECo
260260
const Tensor* d, const Tensor* bias, const Tensor* pre_act_out, bool transa,
261261
bool transb, bool grad, bool accumulate, int comm_sm_count,
262262
cudaStream_t main_stream) {
263-
for (auto t : {a, b, d}) {
264-
NVTE_CHECK(is_tensor_scaling(t->scaling_mode),
265-
"Unsupported scaling mode: " + std::to_string(t->scaling_mode));
263+
const bool tensor_scaling =
264+
is_tensor_scaling(a->scaling_mode) && is_tensor_scaling(b->scaling_mode);
265+
const bool mxfp8 = is_mxfp_scaling(a->scaling_mode) && is_mxfp_scaling(b->scaling_mode);
266+
267+
NVTE_CHECK(tensor_scaling || mxfp8, "Unsupported scaling modes: A=", to_string(a->scaling_mode),
268+
", B=", to_string(b->scaling_mode));
269+
NVTE_CHECK(is_tensor_scaling(d->scaling_mode),
270+
"Unsupported scaling mode for D: ", to_string(d->scaling_mode));
271+
272+
if (mxfp8) {
273+
#if CUBLASMP_VERSION < 801
274+
NVTE_ERROR("MXFP8 GEMM requires cuBLASMp 0.8.1+.");
275+
#else
276+
NVTE_CHECK(a->with_gemm_swizzled_scales,
277+
"MXFP8 scales for A are not in format expected by GEMM");
278+
NVTE_CHECK(b->with_gemm_swizzled_scales,
279+
"MXFP8 scales for B are not in format expected by GEMM");
280+
#endif
266281
}
267282

268-
// Mirror cublaslt_gemm.cu's CanonicalizeGemmInput for FP8 tensor scaling: depending on the
269-
// architecture and the quantizer's usage modes, the appropriate data + scale_inv
270-
// may live on the rowwise or columnwise side of the tensor.
271-
// * Hopper (!nvte_is_non_tn_fp8_gemm_supported): only TN FP8 GEMMs are supported, so an
272-
// FP8 input not already in TN orientation must be swapped to its columnwise (transposed)
273-
// view and the transpose flag flipped.
274-
// * Blackwell+ (nvte_is_non_tn_fp8_gemm_supported): any FP8 GEMM layout is supported, but
275-
// the quantizer usage may have only been set to columnwise. In that case, fall back to
276-
// the columnwise view and flip the transpose flag so the GEMM sees the matching data and
277-
// scale_inv pair.
278-
// The original tensor is never modified; a new Tensor view aliases the columnwise pointers.
283+
// Mirror cublaslt_gemm.cu's input canonicalization. Tensor FP8 columnwise data is a
284+
// transposed view, while MXFP8 columnwise data keeps the logical shape.
279285
const bool fp8_needs_tn = !nvte_is_non_tn_fp8_gemm_supported();
280-
auto canonicalize_fp8_input = [fp8_needs_tn](const Tensor* t, bool current_trans, bool want_trans,
281-
const char* side) -> std::pair<Tensor, bool> {
286+
auto canonicalize_input = [fp8_needs_tn](const Tensor* t, bool current_trans, bool is_a,
287+
const char* side) -> std::pair<Tensor, bool> {
288+
auto use_columnwise = [t](bool new_trans) -> std::pair<Tensor, bool> {
289+
Tensor view = *t;
290+
view.data = t->columnwise_data;
291+
view.scale_inv = t->columnwise_scale_inv;
292+
view.amax = t->columnwise_amax;
293+
return {view, new_trans};
294+
};
295+
296+
if (is_mxfp_scaling(t->scaling_mode)) {
297+
if (is_a) {
298+
if (current_trans) {
299+
NVTE_CHECK(t->has_data(), "MXFP8 transposed input A is missing row-wise data");
300+
return {*t, current_trans};
301+
}
302+
NVTE_CHECK(t->has_columnwise_data(),
303+
"MXFP8 non-transposed input A is missing column-wise data");
304+
return use_columnwise(current_trans);
305+
}
306+
if (current_trans) {
307+
NVTE_CHECK(t->has_columnwise_data(),
308+
"MXFP8 transposed input B is missing column-wise data");
309+
return use_columnwise(current_trans);
310+
}
311+
NVTE_CHECK(t->has_data(), "MXFP8 non-transposed input B is missing row-wise data");
312+
return {*t, current_trans};
313+
}
314+
282315
if (!is_fp8_dtype(t->dtype())) {
283316
return {*t, current_trans};
284317
}
318+
319+
const bool want_trans = is_a;
285320
const bool hopper_tn_swap = fp8_needs_tn && current_trans != want_trans;
286321
const bool blackwell_missing_rowwise = !fp8_needs_tn && !t->has_data();
287322
if (!hopper_tn_swap && !blackwell_missing_rowwise) {
288323
return {*t, current_trans};
289324
}
290325
NVTE_CHECK(t->has_columnwise_data() && is_fp8_dtype(t->columnwise_data.dtype),
291326
"cuBLASMp FP8 GEMM input ", side, " is missing column-wise usage");
292-
Tensor view;
293-
view.scaling_mode = t->scaling_mode;
294-
view.data = t->columnwise_data;
295-
view.scale_inv = t->columnwise_scale_inv;
296-
// Columnwise data is the transposed view of the original — flip the transpose flag.
297-
return {view, !current_trans};
327+
return use_columnwise(!current_trans);
298328
};
299329

300-
auto [a_used, transa_eff] = canonicalize_fp8_input(a, transa, /*want_trans=*/true, "A");
301-
auto [b_used, transb_eff] = canonicalize_fp8_input(b, transb, /*want_trans=*/false, "B");
330+
auto [a_used, transa_eff] = canonicalize_input(a, transa, /*is_a=*/true, "A");
331+
auto [b_used, transb_eff] = canonicalize_input(b, transb, /*is_a=*/false, "B");
302332
transa = transa_eff;
303333
transb = transb_eff;
304334

@@ -321,20 +351,30 @@ void cublasmp_gemm(InitMatricesFn init_matrices_fn, NVTECommGemmCtx* ctx, NVTECo
321351
sizeof algo_attr));
322352

323353
const cublasMpMatmulMatrixScale_t scale_mode = CUBLASMP_MATMUL_MATRIX_SCALE_SCALAR_FP32;
324-
if (is_fp8_dtype(a_used.dtype())) {
325-
NVTE_CHECK(a_used.scale_inv.dptr, "Scaling must be set for FP8 dtype");
354+
auto get_input_scale_mode = [&](const Tensor& t) {
355+
#if CUBLASMP_VERSION >= 801
356+
if (is_mxfp_scaling(t.scaling_mode)) {
357+
return CUBLASMP_MATMUL_MATRIX_SCALE_VEC32_UE8M0;
358+
}
359+
#endif
360+
return scale_mode;
361+
};
362+
if (is_fp8_dtype(a_used.dtype()) || is_mxfp_scaling(a_used.scaling_mode)) {
363+
const cublasMpMatmulMatrixScale_t input_scale_mode = get_input_scale_mode(a_used);
364+
NVTE_CHECK(a_used.scale_inv.dptr, "Scaling must be set for input A");
326365
NVTE_CHECK_CUBLASMP(cublasMpMatmulDescriptorSetAttribute(
327-
ctx->matmul_desc.get(), CUBLASMP_MATMUL_DESCRIPTOR_ATTRIBUTE_A_SCALE_MODE, &scale_mode,
328-
sizeof scale_mode));
366+
ctx->matmul_desc.get(), CUBLASMP_MATMUL_DESCRIPTOR_ATTRIBUTE_A_SCALE_MODE,
367+
&input_scale_mode, sizeof input_scale_mode));
329368
NVTE_CHECK_CUBLASMP(cublasMpMatmulDescriptorSetAttribute(
330369
ctx->matmul_desc.get(), CUBLASMP_MATMUL_DESCRIPTOR_ATTRIBUTE_A_SCALE_POINTER,
331370
&a_used.scale_inv.dptr, sizeof(void*)));
332371
}
333-
if (is_fp8_dtype(b_used.dtype())) {
334-
NVTE_CHECK(b_used.scale_inv.dptr, "Scaling must be set for FP8 dtype");
372+
if (is_fp8_dtype(b_used.dtype()) || is_mxfp_scaling(b_used.scaling_mode)) {
373+
const cublasMpMatmulMatrixScale_t input_scale_mode = get_input_scale_mode(b_used);
374+
NVTE_CHECK(b_used.scale_inv.dptr, "Scaling must be set for input B");
335375
NVTE_CHECK_CUBLASMP(cublasMpMatmulDescriptorSetAttribute(
336-
ctx->matmul_desc.get(), CUBLASMP_MATMUL_DESCRIPTOR_ATTRIBUTE_B_SCALE_MODE, &scale_mode,
337-
sizeof scale_mode));
376+
ctx->matmul_desc.get(), CUBLASMP_MATMUL_DESCRIPTOR_ATTRIBUTE_B_SCALE_MODE,
377+
&input_scale_mode, sizeof input_scale_mode));
338378
NVTE_CHECK_CUBLASMP(cublasMpMatmulDescriptorSetAttribute(
339379
ctx->matmul_desc.get(), CUBLASMP_MATMUL_DESCRIPTOR_ATTRIBUTE_B_SCALE_POINTER,
340380
&b_used.scale_inv.dptr, sizeof(void*)));

‎transformer_engine/pytorch/module/layernorm_mlp.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,9 +1286,9 @@ def backward(
12861286
# we gather here. Route through the same FP8-aware all-gather as
12871287
# the non-overlap path in
12881288
# ``TransformerEngineBaseModule.grad_output_preprocess`` by passing
1289-
# the grad_output quantizer. Columnwise data needed for fc2_wgrad
1290-
# is produced by ``update_usage(columnwise_usage=True)`` further
1291-
# below.
1289+
# the grad_output quantizer. Per-tensor FP8 can reconstruct columnwise
1290+
# data from the gathered rowwise data; MXFP8 must instead quantize
1291+
# the original gradient columnwise to avoid double quantization.
12921292
if (
12931293
ctx.fc2_weight_requires_grad
12941294
and ctx.ub_overlap_ag
@@ -1297,6 +1297,10 @@ def backward(
12971297
):
12981298
if ctx.fc2_grad_output_quantizer is not None:
12991299
set_quantizer_usage_for_wgrad_all_gather(ctx.fc2_grad_output_quantizer)
1300+
if isinstance(ctx.fc2_grad_output_quantizer, MXFP8Quantizer):
1301+
grad_output = (
1302+
grad_outputs[0].reshape(-1, grad_outputs[0].shape[-1]).contiguous()
1303+
)
13001304
grad_output, _ = gather_along_first_dim(
13011305
grad_output,
13021306
ctx.tp_group,
@@ -1312,7 +1316,11 @@ def backward(
13121316
# Prepare grad output tensor
13131317
# Note: Synchronize tensor-parallel communication and
13141318
# make sure required data is available
1315-
if ctx.ub_overlap_ag and isinstance(ctx.fc2_grad_output_quantizer, MXFP8Quantizer):
1319+
if (
1320+
ctx.ub_overlap_ag
1321+
and isinstance(ctx.fc2_grad_output_quantizer, MXFP8Quantizer)
1322+
and not ub_obj_fc2_dgrad.with_cublasmp()
1323+
):
13161324
# UB does not support pipelined overlapping grad output
13171325
# all-gather with wgrad GEMM. Also, we can't
13181326
# convert row-scaled MXFP8 to column-scaled, so we

‎transformer_engine/pytorch/module/linear.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,8 +1481,9 @@ def _linear_backward_impl(args: LinearBwdArgs) -> Tuple[Union[torch.Tensor, None
14811481
# its persistent buffer; cuBLASMp does not, so we gather here. Route
14821482
# through the same FP8-aware all-gather as the non-overlap path in
14831483
# ``TransformerEngineBaseModule.grad_output_preprocess`` by passing the
1484-
# grad_output quantizer. The columnwise data needed for wgrad is then
1485-
# produced by ``update_usage(columnwise_usage=True)`` further below.
1484+
# grad_output quantizer. Per-tensor FP8 can reconstruct columnwise
1485+
# data from the gathered rowwise data; MXFP8 must instead quantize
1486+
# the original gradient columnwise to avoid double quantization.
14861487
if (
14871488
bwd_args.requires_wgrad
14881489
and bwd_args.ub_overlap_ag
@@ -1491,6 +1492,8 @@ def _linear_backward_impl(args: LinearBwdArgs) -> Tuple[Union[torch.Tensor, None
14911492
):
14921493
if grad_output_quantizer is not None:
14931494
set_quantizer_usage_for_wgrad_all_gather(grad_output_quantizer)
1495+
if isinstance(grad_output_quantizer, MXFP8Quantizer):
1496+
grad_output = grad_output_arg.reshape(-1, grad_output_arg.shape[-1]).contiguous()
14941497
grad_output, _ = gather_along_first_dim(
14951498
grad_output,
14961499
bwd_args.tp_group,
@@ -1520,7 +1523,11 @@ def _linear_backward_impl(args: LinearBwdArgs) -> Tuple[Union[torch.Tensor, None
15201523
# Prepare grad output tensor
15211524
# Note: Synchronize tensor-parallel communication and
15221525
# make sure required data is available
1523-
if bwd_args.ub_overlap_ag and isinstance(grad_output_quantizer, MXFP8Quantizer):
1526+
if (
1527+
bwd_args.ub_overlap_ag
1528+
and isinstance(grad_output_quantizer, MXFP8Quantizer)
1529+
and not ub_obj_dgrad.with_cublasmp()
1530+
):
15241531
# UB does not support pipelined overlapping grad output
15251532
# all-gather with wgrad GEMM. Also, we can't
15261533
# convert row-scaled MXFP8 to column-scaled, so we

0 commit comments

Comments
 (0)