Skip to content

Commit 64bb9a2

Browse files
authored
[PyTorch] Support scaled + clamped SwiGLU in te.ops and enable fused MXFP8 grouped MLP (NVIDIA#2855)
* cuDNN act_func='geglu' support for fused grouped MLP Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com> * rm incorrect/not needed doc Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com> * Address comments Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com> * Fix activation name Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com> * Min cudnn 1.23 for qgeglu fusion Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com> --------- Signed-off-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com>
1 parent 181322e commit 64bb9a2

7 files changed

Lines changed: 299 additions & 58 deletions

File tree

docs/api/pytorch.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ Operation fuser
221221

222222
.. autoapiclass:: transformer_engine.pytorch.ops.SReLU
223223

224+
.. autoapiclass:: transformer_engine.pytorch.ops.ScaledClampedQGeGLU
225+
224226
.. autoapiclass:: transformer_engine.pytorch.ops.ScaledSwiGLU
225227

226228
.. autoapiclass:: transformer_engine.pytorch.ops.SiLU

tests/pytorch/test_fusible_ops.py

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
import transformer_engine.common.recipe
1919
import transformer_engine.pytorch as te
2020
import transformer_engine.pytorch.ops as te_ops
21+
from transformer_engine.pytorch.ops._common import (
22+
_nvidia_cudnn_frontend_supports_scaled_clamped_qgeglu,
23+
)
2124

2225
from transformer_engine.pytorch.ops.fused import (
2326
BackwardActivationBias,
@@ -2234,6 +2237,91 @@ def test_interleaved_scaled_swiglu(self):
22342237
scales_requires_grad=True,
22352238
)
22362239

2240+
@pytest.mark.parametrize("in_shape", ((71, 192), (5, 7, 128)))
2241+
@pytest.mark.parametrize("input_requires_grad", (False, True))
2242+
@pytest.mark.parametrize("scales_requires_grad", (False, True))
2243+
def test_scaled_clamped_qgeglu(
2244+
self,
2245+
*,
2246+
in_shape: Iterable[int],
2247+
glu_interleave_size: Optional[int] = None,
2248+
dtype: torch.dtype = torch.float32,
2249+
device: torch.device = "cuda",
2250+
input_requires_grad: bool,
2251+
scales_requires_grad: bool,
2252+
limit: float = 7.0,
2253+
alpha: float = 1.702,
2254+
) -> None:
2255+
"""ScaledClampedQGeGLU (clamped QGeGLU with post-scale)"""
2256+
2257+
# Tensor dims
2258+
out_shape = list(in_shape)
2259+
out_shape[-1] //= 2
2260+
2261+
# Random data
2262+
x_ref, x_test = make_reference_and_test_tensors(
2263+
in_shape,
2264+
test_dtype=dtype,
2265+
test_device=device,
2266+
requires_grad=input_requires_grad,
2267+
)
2268+
scales_ref, scales_test = make_reference_and_test_tensors(
2269+
in_shape[:-1],
2270+
test_dtype=dtype,
2271+
test_device=device,
2272+
requires_grad=scales_requires_grad,
2273+
)
2274+
dy_ref, dy_test = make_reference_and_test_tensors(
2275+
out_shape,
2276+
test_dtype=dtype,
2277+
test_device=device,
2278+
requires_grad=False,
2279+
)
2280+
2281+
# Plain PyTorch reference (matches :class:`ClampedSwiGLU` numerics)
2282+
x = x_ref
2283+
if glu_interleave_size is not None:
2284+
x = x.reshape(
2285+
-1,
2286+
in_shape[-1] // (2 * glu_interleave_size),
2287+
2,
2288+
glu_interleave_size,
2289+
)
2290+
x = x.transpose(1, 2)
2291+
x = x.reshape(in_shape)
2292+
x_glu, x_linear = x.chunk(2, dim=-1)
2293+
x_glu = x_glu.clamp(min=None, max=limit)
2294+
x_linear = x_linear.clamp(min=-limit, max=limit)
2295+
out_glu = x_glu * torch.sigmoid(alpha * x_glu)
2296+
y = out_glu * (x_linear + 1)
2297+
y_ref = scales_ref.unsqueeze(-1) * y
2298+
if input_requires_grad or scales_requires_grad:
2299+
y_ref.backward(dy_ref)
2300+
2301+
op = te_ops.ScaledClampedQGeGLU(
2302+
glu_interleave_size=glu_interleave_size,
2303+
limit=limit,
2304+
alpha=alpha,
2305+
)
2306+
y_test = op(x_test, scales_test)
2307+
if input_requires_grad or scales_requires_grad:
2308+
y_test.backward(dy_test)
2309+
2310+
tols = dtype_tols(dtype)
2311+
y_test = y_test.to(dtype=torch.float64, device="cpu")
2312+
assert_close(y_test, y_ref, **tols)
2313+
assert_close_grads(x_test, x_ref, **tols)
2314+
assert_close_grads(scales_test, scales_ref, **tols)
2315+
2316+
def test_interleaved_scaled_clamped_qgeglu(self):
2317+
"""ScaledClampedQGeGLU with block interleaved input format"""
2318+
self.test_scaled_clamped_qgeglu(
2319+
in_shape=(32, 192),
2320+
glu_interleave_size=32,
2321+
input_requires_grad=True,
2322+
scales_requires_grad=True,
2323+
)
2324+
22372325

22382326
class TestFusedOps:
22392327
"""Tests for fused operations"""
@@ -3249,6 +3337,7 @@ def to_cpu(tensor: Optional[torch.Tensor]) -> Optional[torch.Tensor]:
32493337
@pytest.mark.parametrize("accumulate_into_main_grad", (False, True))
32503338
@pytest.mark.parametrize("glu_interleave_size", (None, 32))
32513339
@pytest.mark.parametrize("delay_wgrad_compute", (False, True))
3340+
@pytest.mark.parametrize("activation", ("scaled_swiglu", "scaled_clamped_qgeglu"))
32523341
def test_grouped_mlp(
32533342
self,
32543343
*,
@@ -3264,8 +3353,9 @@ def test_grouped_mlp(
32643353
split_alignment: int = 256,
32653354
glu_interleave_size: Optional[int],
32663355
delay_wgrad_compute: bool,
3356+
activation: str,
32673357
) -> None:
3268-
"""GroupedLinear + ScaledSwiGLU + GroupedLinear"""
3358+
"""GroupedLinear + ScaledSwiGLU / ScaledClampedQGeGLU + GroupedLinear"""
32693359

32703360
# Split sizes
32713361
split_sizes = [split_alignment * (i) for i in range(group_size)]
@@ -3288,6 +3378,9 @@ def test_grouped_mlp(
32883378
if quantization == "mxfp8" and bias:
32893379
# Will be supported in future CUDNN release.
32903380
pytest.skip("Bias/dbias not yet supported in MXFP8 fused grouped MLP")
3381+
if quantization == "nvfp4" and activation == "scaled_clamped_qgeglu" and bias:
3382+
# TODO: ksivaman: Need to debug numerics for this case.
3383+
pytest.skip("Bias/dbias not yet supported in NVFP4 fused grouped MLP with GeGLU")
32913384

32923385
# Random data
32933386
x_ref, x_test = make_reference_and_test_tensors(
@@ -3376,7 +3469,14 @@ def test_grouped_mlp(
33763469
x = x.transpose(1, 2)
33773470
x = x.reshape(-1, 2 * hidden_size)
33783471
x1, x2 = x.chunk(2, dim=-1)
3379-
x = torch.nn.functional.silu(x1) * x2
3472+
if activation == "scaled_swiglu":
3473+
x = torch.nn.functional.silu(x1) * x2
3474+
else:
3475+
lim = torch.tensor(7.0, device=x1.device, dtype=x1.dtype)
3476+
geglu_alpha = 1.702
3477+
x1c = torch.minimum(x1, lim)
3478+
x2c = torch.clamp(x2, -lim, lim)
3479+
x = (x2c + 1) * (x1c * torch.sigmoid(geglu_alpha * x1c))
33803480
x = x * probs[group_idx].unsqueeze(-1)
33813481
x = torch.nn.functional.linear(x, fc2_ws_ref[group_idx], bias=fc2_bs_ref[group_idx])
33823482
ys.append(x)
@@ -3385,6 +3485,11 @@ def test_grouped_mlp(
33853485

33863486
# Construct operations
33873487
recipe = make_recipe(quantization)
3488+
scaled_act = (
3489+
te_ops.ScaledSwiGLU(glu_interleave_size=glu_interleave_size)
3490+
if activation == "scaled_swiglu"
3491+
else te_ops.ScaledClampedQGeGLU(glu_interleave_size=glu_interleave_size)
3492+
)
33883493
with te.quantized_model_init(enabled=with_quantization, recipe=recipe):
33893494
fc1 = te_ops.GroupedLinear(
33903495
group_size,
@@ -3412,7 +3517,7 @@ def test_grouped_mlp(
34123517
)
34133518
module = te_ops.Sequential(
34143519
fc1,
3415-
te_ops.ScaledSwiGLU(glu_interleave_size=glu_interleave_size),
3520+
scaled_act,
34163521
fc2,
34173522
)
34183523

@@ -3484,6 +3589,10 @@ def test_grouped_mlp(
34843589
quantization == "mxfp8"
34853590
and dtype in (torch.bfloat16, torch.float16)
34863591
and glu_interleave_size == 32
3592+
and (
3593+
activation != "scaled_clamped_qgeglu"
3594+
or _nvidia_cudnn_frontend_supports_scaled_clamped_qgeglu()
3595+
)
34873596
):
34883597
if te_ops.fused.ForwardGroupedMLP_CuTeGEMMSwiGLU_MXFP8.is_supported():
34893598
forward_ops = module._module_groups[0]._forward_ops
@@ -3572,13 +3681,15 @@ def test_grouped_mlp(
35723681
@pytest.mark.parametrize("dtype", _dtypes)
35733682
@pytest.mark.parametrize("single_grouped_weight", (False, True))
35743683
@pytest.mark.parametrize("accumulate_into_main_grad", (False, True))
3684+
@pytest.mark.parametrize("activation", ("scaled_swiglu", "scaled_clamped_qgeglu"))
35753685
@pytest.mark.skipif(not mxfp8_available, reason=reason_for_no_mxfp8)
35763686
def test_grouped_mlp_cuda_graph_safe_mxfp8(
35773687
self,
35783688
*,
35793689
dtype: torch.dtype,
35803690
single_grouped_weight: bool,
35813691
accumulate_into_main_grad: bool,
3692+
activation: str,
35823693
device: torch.device = "cuda",
35833694
group_size: int = 4,
35843695
hidden_size: int = 256,
@@ -3591,6 +3702,12 @@ def test_grouped_mlp_cuda_graph_safe_mxfp8(
35913702
pytest.skip("MXFP8 fused grouped MLP is not supported on this system")
35923703
if dtype not in (torch.bfloat16, torch.float16):
35933704
pytest.skip("MXFP8 fused grouped MLP is only supported with BF16/FP16")
3705+
if activation == "scaled_clamped_qgeglu" and not (
3706+
_nvidia_cudnn_frontend_supports_scaled_clamped_qgeglu()
3707+
):
3708+
pytest.skip(
3709+
"ScaledClampedQGeGLU fused grouped MLP requires nvidia-cudnn-frontend >= 1.23.0"
3710+
)
35943711

35953712
split_sizes = [split_alignment * (i + 1) for i in range(group_size)]
35963713
random.shuffle(split_sizes)
@@ -3619,9 +3736,14 @@ def test_grouped_mlp_cuda_graph_safe_mxfp8(
36193736
single_grouped_weight=single_grouped_weight,
36203737
accumulate_into_main_grad=accumulate_into_main_grad,
36213738
)
3739+
scaled_act = (
3740+
te_ops.ScaledSwiGLU(glu_interleave_size=glu_interleave_size)
3741+
if activation == "scaled_swiglu"
3742+
else te_ops.ScaledClampedQGeGLU(glu_interleave_size=glu_interleave_size)
3743+
)
36223744
module = te_ops.Sequential(
36233745
fc1,
3624-
te_ops.ScaledSwiGLU(glu_interleave_size=glu_interleave_size),
3746+
scaled_act,
36253747
fc2,
36263748
)
36273749

transformer_engine/pytorch/ops/_common.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
"""Helper functions used in fusible operations."""
66

77
from __future__ import annotations
8+
import functools
9+
from importlib.metadata import PackageNotFoundError, version as get_pkg_version
810
from typing import Optional
911

1012
import torch
13+
from packaging.version import Version as PkgVersion
1114

1215
from transformer_engine_torch import FP8TensorMeta
1316
from ..torch_version import torch_version
@@ -17,6 +20,15 @@
1720
from ..utils import canonicalize_dtype
1821

1922

23+
@functools.lru_cache(maxsize=1)
24+
def _nvidia_cudnn_frontend_supports_scaled_clamped_qgeglu() -> bool:
25+
"""Check cuDNN FE min version with fixed numerics for qgeglu."""
26+
try:
27+
return PkgVersion(get_pkg_version("nvidia-cudnn-frontend")) >= PkgVersion("1.23.0")
28+
except PackageNotFoundError:
29+
return False
30+
31+
2032
def is_quantized_tensor(tensor: torch.Tensor | QuantizedTensorStorage) -> bool:
2133
"""Check if tensor is a quantized tensor"""
2234
return isinstance(tensor, QuantizedTensorStorage)
@@ -73,8 +85,8 @@ def get_fp8_meta_from_fp8_tensor(tensor: Float8Tensor) -> tuple[FP8TensorMeta, i
7385
return fp8_meta, 0
7486

7587

76-
def validate_grouped_mlp_dims(fc1, swiglu, fc2) -> None:
77-
"""Validate FC1/SwiGLU/FC2 dimensions and interleave size for fused grouped MLP."""
88+
def validate_grouped_mlp_dims(fc1, glu_op, fc2) -> None:
89+
"""Validate FC1 / scaled GLU / FC2 dimensions for fused grouped MLP."""
7890

7991
if fc1.in_features % 256 != 0 or fc1.out_features % 256 != 0:
8092
raise ValueError(
@@ -93,10 +105,10 @@ def validate_grouped_mlp_dims(fc1, swiglu, fc2) -> None:
93105
f"and FC2 (num_groups={fc2.num_groups}, in_features={fc2.in_features}, "
94106
f"out_features={fc2.out_features}) do not match."
95107
)
96-
if swiglu.glu_interleave_size != 32:
108+
if glu_op.glu_interleave_size != 32:
97109
raise ValueError(
98110
"Fused kernel requires 32-wide GLU interleaving, "
99-
f"but got glu_interleave_size={swiglu.glu_interleave_size}."
111+
f"but got glu_interleave_size={glu_op.glu_interleave_size}."
100112
)
101113

102114

@@ -106,7 +118,7 @@ def fuse_grouped_mlp_ops(
106118
recipe,
107119
fused_op_cls,
108120
):
109-
"""Sliding-window fusion for GroupedLinear + ScaledSwiGLU + GroupedLinear.
121+
"""Sliding-window fusion for GroupedLinear + scaled GLU + GroupedLinear.
110122
111123
Parameters
112124
----------
@@ -116,7 +128,9 @@ def fuse_grouped_mlp_ops(
116128
Quantization recipe.
117129
fused_op_cls : type
118130
Fused operation class with ``is_supported()`` classmethod and
119-
constructor accepting ``fc1``, ``swiglu``, ``fc2`` keyword args.
131+
constructor accepting ``fc1``, ``glu_op``, ``fc2`` keyword args. The
132+
``glu_op`` must be :class:`~transformer_engine.pytorch.ops.basic.swiglu.ScaledSwiGLU`
133+
or :class:`~transformer_engine.pytorch.ops.basic.swiglu.ScaledClampedQGeGLU`.
120134
May also expose ``is_fc1_bias_supported()`` and/or
121135
``is_fc2_bias_supported()`` classmethods for bias eligibility.
122136
@@ -125,7 +139,11 @@ def fuse_grouped_mlp_ops(
125139
list of FusibleOperation
126140
Updated operations with matched triples replaced by fused ops.
127141
"""
128-
from .basic import GroupedLinear, ScaledSwiGLU # pylint: disable=import-outside-toplevel
142+
from .basic import ( # pylint: disable=import-outside-toplevel
143+
GroupedLinear,
144+
ScaledClampedQGeGLU,
145+
ScaledSwiGLU,
146+
)
129147

130148
if not fused_op_cls.is_supported():
131149
return ops
@@ -146,10 +164,15 @@ def fuse_grouped_mlp_ops(
146164
matches_pattern = True
147165
if not (
148166
isinstance(window[0], GroupedLinear)
149-
and isinstance(window[1], ScaledSwiGLU)
167+
and isinstance(window[1], (ScaledSwiGLU, ScaledClampedQGeGLU))
150168
and isinstance(window[2], GroupedLinear)
151169
):
152170
matches_pattern = False
171+
elif isinstance(window[1], ScaledClampedQGeGLU) and (
172+
abs(window[1]._clamped.alpha - 1.702) > 0.001
173+
or not _nvidia_cudnn_frontend_supports_scaled_clamped_qgeglu()
174+
):
175+
matches_pattern = False
153176
elif window[0].num_groups != window[2].num_groups:
154177
matches_pattern = False
155178
elif (

transformer_engine/pytorch/ops/basic/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@
3232
from .reduce_scatter import ReduceScatter
3333
from .reshape import Reshape
3434
from .rmsnorm import RMSNorm
35-
from .swiglu import ClampedSwiGLU, ScaledSwiGLU, SwiGLU
35+
from .swiglu import ClampedSwiGLU, ScaledClampedQGeGLU, ScaledSwiGLU, SwiGLU

0 commit comments

Comments
 (0)