[Performance] Add Triton kernel for Gemma3n sparse GELU (#48498)
Signed-off-by: BWAAEEEK <jooho414@gmail.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com> Co-authored-by: Cyrus Leung <tlleungac@connect.ust.hk>
JooHo Lee committed
Sep 14, 2026 at 16:42 UTC
78e84261ab2ffb691a8079d698d1f3f96ecb7635
15 files changed
+555
-21
benchmarks/kernels/benchmark_gelu_and_mul_sparse.py
new
+90
@@ -0,0 +1,90 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import itertools
5
+
6
+import torch
7
+
8
+import vllm.kernels # noqa: F401
9
+from vllm import ir
10
+from vllm.benchmarks.lib.utils import default_vllm_config
11
+from vllm.triton_utils import triton
12
+from vllm.utils.argparse_utils import FlexibleArgumentParser
13
+from vllm.utils.torch_utils import STR_DTYPE_TO_TORCH_DTYPE, set_random_seed
14
+
15
+NUM_TOKENS = [1, 2, 8, 32, 128, 512, 2048]
16
+INTERMEDIATE_SIZES = [2048, 4096, 8192, 13824, 16384, 32768]
17
+CONFIGS = list(itertools.product(NUM_TOKENS, INTERMEDIATE_SIZES))
18
+STD_MULTIPLIER = 1.6448536269514722
19
+
20
+
21
+@default_vllm_config()
22
+def benchmark_gelu_and_mul_sparse(
23
+ num_tokens: int,
24
+ intermediate_size: int,
25
+ provider: str,
26
+ dtype: torch.dtype,
27
+):
28
+ set_random_seed(0)
29
+ torch.set_default_device("cuda")
30
+ x = torch.randn(num_tokens, 2 * intermediate_size, dtype=dtype)
31
+ native = ir.ops.gelu_and_mul_sparse.impls["native"].impl_fn
32
+ triton_impl = ir.ops.gelu_and_mul_sparse.impls["triton"].impl_fn
33
+ args = (x, STD_MULTIPLIER, "tanh")
34
+
35
+ expected = native(*args)
36
+ actual = triton_impl(*args)
37
+ torch.testing.assert_close(
38
+ actual,
39
+ expected,
40
+ **ir.ops.gelu_and_mul_sparse.get_tolerance(actual.dtype),
41
+ )
42
+
43
+ if provider == "triton":
44
+ fn = lambda: triton_impl(*args)
45
+ elif provider == "native_compiled":
46
+ compiled = torch.compile(native, fullgraph=True)
47
+ compiled(*args)
48
+ fn = lambda: compiled(*args)
49
+ else:
50
+ fn = lambda: native(*args)
51
+
52
+ ms, min_ms, max_ms = triton.testing.do_bench_cudagraph(
53
+ fn, quantiles=[0.5, 0.2, 0.8]
54
+ )
55
+ return ms, max_ms, min_ms
56
+
57
+
58
+if __name__ == "__main__":
59
+ parser = FlexibleArgumentParser(
60
+ description="Benchmark the Gemma3n sparse GELU activation."
61
+ )
62
+ parser.add_argument(
63
+ "--dtype",
64
+ type=str,
65
+ choices=["half", "bfloat16", "float"],
66
+ default="bfloat16",
67
+ )
68
+ args = parser.parse_args()
69
+ dtype = STR_DTYPE_TO_TORCH_DTYPE[args.dtype]
70
+
71
+ perf_report = triton.testing.perf_report(
72
+ triton.testing.Benchmark(
73
+ x_names=["num_tokens", "intermediate_size"],
74
+ x_vals=CONFIGS,
75
+ line_arg="provider",
76
+ line_vals=["triton", "native_compiled", "native_eager"],
77
+ line_names=["Triton", "Native (torch.compile)", "Native (eager)"],
78
+ styles=[("blue", "-"), ("green", "-"), ("red", "-")],
79
+ ylabel="ms",
80
+ plot_name="gelu-and-mul-sparse-performance",
81
+ args={},
82
+ )
83
+ )
84
+ perf_report(
85
+ lambda num_tokens, intermediate_size, provider: (
86
+ benchmark_gelu_and_mul_sparse(
87
+ num_tokens, intermediate_size, provider, dtype
88
+ )
89
+ )
90
+ ).run(print_data=True)
tests/compile/passes/ir/test_lowering.py
+34
@@ -11,6 +11,7 @@ from vllm.compilation.passes.ir.lowering_pass import (
11
)
12
from vllm.config import get_current_vllm_config
13
from vllm.ir import ops
14
+from vllm.model_executor.layers.activation import GeluAndMulSparse
15
from vllm.platforms import current_platform
16
17
from ...backend import TestBackend
@@ -34,6 +35,15 @@ class Model(nn.Module):
35
return x6 + 3.0
36
37
38
+class SparseActivationModel(nn.Module):
39
+ def __init__(self):
40
+ super().__init__()
41
+ self.activation = GeluAndMulSparse(0.95, "tanh")
42
+
43
+ def forward(self, x):
44
+ return self.activation(x)
45
+
46
+
47
@pytest.mark.parametrize("rms_provider", ops.rms_norm.supported_providers())
48
def test_lowering_rms_norm(rms_provider, default_vllm_config):
49
torch.set_default_device(current_platform.device_type)
@@ -67,3 +77,27 @@ def test_lowering_rms_norm(rms_provider, default_vllm_config):
77
78
torch.testing.assert_close(output_unlowered, output)
79
torch.testing.assert_close(output_unlowered, output2)
80
+
81
+
82
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA-only provider")
83
+def test_lowering_gelu_and_mul_sparse(default_vllm_config):
84
+ torch.set_default_device("cuda")
85
+ lowering_pass = VllmIRLoweringPass(get_current_vllm_config())
86
+ backend = TestBackend(lowering_pass)
87
+ model = SparseActivationModel()
88
+ x = torch.randn(8, 32768, dtype=torch.bfloat16)
89
+
90
+ with (
91
+ ops.gelu_and_mul_sparse.set_priority(["triton", "native"]),
92
+ ir.enable_torch_wrap(True),
93
+ ):
94
+ compiled_model = torch.compile(model, backend=backend, fullgraph=True)
95
+ output = compiled_model(x)
96
+
97
+ selected = lowering_pass.selected_impls["gelu_and_mul_sparse"]
98
+ assert selected == {"gelu_and_mul_sparse": "triton"}
99
+
100
+ expected = ops.gelu_and_mul_sparse.impls["native"].impl_fn(
101
+ x, 1.6448536269514722, "tanh"
102
+ )
103
+ torch.testing.assert_close(output, expected, rtol=1.6e-2, atol=1e-3)
tests/compile/test_aot_compile.py
+65
@@ -13,6 +13,8 @@ import pytest
13
import torch
14
15
import vllm.envs as envs
16
+import vllm.kernels # noqa: F401 to register kernels
17
+from vllm import ir
18
from vllm.compilation.backends import VllmBackend
19
from vllm.compilation.caching import (
20
StandaloneCompiledArtifacts,
@@ -28,6 +30,9 @@ from vllm.config import (
30
)
31
from vllm.envs import disable_envs_cache
32
from vllm.forward_context import set_forward_context
33
+from vllm.ir import ops
34
+from vllm.model_executor.layers.activation import GeluAndMulSparse
35
+from vllm.platforms import current_platform
36
from vllm.utils.torch_utils import is_torch_equal_or_newer
37
38
from ..utils import create_new_process_for_each_test
@@ -77,6 +82,18 @@ class CompiledModTuple(torch.nn.Module):
82
return reference_fn_tuple(x)
83
84
85
+@support_torch_compile(dynamic_arg_dims={"x": 0})
86
+class SparseActivationAotModel(torch.nn.Module):
87
+ def __init__(self, **kwargs):
88
+ super().__init__()
89
+ self.up_proj = torch.nn.Linear(32, 256, bias=False)
90
+ self.activation = GeluAndMulSparse(0.95, "tanh")
91
+ self.down_proj = torch.nn.Linear(128, 16, bias=False)
92
+
93
+ def forward(self, x: torch.Tensor):
94
+ return self.down_proj(self.activation(self.up_proj(x)))
95
+
96
+
97
def make_vllm_config() -> VllmConfig:
98
return VllmConfig(
99
compilation_config=CompilationConfig(
@@ -175,6 +192,54 @@ def test_save_and_load(monkeypatch: pytest.MonkeyPatch):
192
assert torch.allclose(ret, expected)
193
194
195
+@pytest.mark.skipif(
196
+ not current_platform.is_cuda() or not is_torch_equal_or_newer("2.10.0"),
197
+ reason="requires CUDA and torch 2.10",
198
+)
199
+def test_sparse_activation_standalone_aot_preserves_dependencies(
200
+ monkeypatch: pytest.MonkeyPatch, vllm_tmp_cache: Path
201
+):
202
+ def native_reference(model: SparseActivationAotModel, x: torch.Tensor):
203
+ projected = torch.nn.functional.linear(x, model.up_proj.weight)
204
+ activated = ops.gelu_and_mul_sparse.impls["native"].impl_fn(
205
+ projected, model.activation.std_multiplier, "tanh"
206
+ )
207
+ return torch.nn.functional.linear(activated, model.down_proj.weight)
208
+
209
+ with monkeypatch.context() as m:
210
+ m.setenv("VLLM_USE_AOT_COMPILE", "1")
211
+ m.setenv("VLLM_USE_MEGA_AOT_ARTIFACT", "1")
212
+ m.setenv("VLLM_USE_STANDALONE_COMPILE", "1")
213
+ m.setenv("VLLM_CACHE_ROOT", str(vllm_tmp_cache / "vllm_cache"))
214
+
215
+ vllm_config = make_vllm_config()
216
+ torch.manual_seed(0)
217
+ inputs = [
218
+ torch.randn(4, 32, device="cuda", dtype=torch.bfloat16),
219
+ torch.randn(7, 32, device="cuda", dtype=torch.bfloat16),
220
+ ]
221
+
222
+ with (
223
+ use_vllm_config(vllm_config),
224
+ vllm_config.kernel_config.ir_op_priority.set_priority(),
225
+ ir.enable_torch_wrap(True),
226
+ ):
227
+ compiled = SparseActivationAotModel(vllm_config=vllm_config).to(
228
+ device="cuda", dtype=torch.bfloat16
229
+ )
230
+ compiled.requires_grad_(False)
231
+ expected = [native_reference(compiled, x) for x in inputs]
232
+ actual = [compiled(x) for x in inputs]
233
+
234
+ for result, reference in zip(actual, expected):
235
+ assert torch.isfinite(result).all()
236
+ torch.testing.assert_close(
237
+ result,
238
+ reference,
239
+ **ops.gelu_and_mul_sparse.get_tolerance(result.dtype),
240
+ )
241
+
242
+
243
@pytest.mark.skipif(not is_torch_equal_or_newer("2.10.0"), reason="requires torch 2.10")
244
def test_save_and_load_slice(monkeypatch: pytest.MonkeyPatch):
245
from torch._subclasses import FakeTensorMode
tests/kernels/ir/test_activation.py
new
+183
@@ -0,0 +1,183 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import pytest
5
+import torch
6
+
7
+import vllm.kernels # noqa: F401
8
+from tests.ir.ir_test_utils import assert_close, clone_args
9
+from vllm import ir
10
+from vllm.platforms import current_platform
11
+from vllm.triton_utils import HAS_TRITON
12
+
13
+gelu_and_mul_sparse_native = ir.ops.gelu_and_mul_sparse.impls["native"].impl_fn
14
+
15
+
16
+def test_gelu_and_mul_sparse_registration():
17
+ expected = {
18
+ "native": True,
19
+ "triton": HAS_TRITON and current_platform.is_cuda(),
20
+ }
21
+ actual = {
22
+ provider: impl.supported
23
+ for provider, impl in ir.ops.gelu_and_mul_sparse.impls.items()
24
+ }
25
+ assert actual == expected
26
+
27
+
28
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA-only provider")
29
+@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
30
+@pytest.mark.parametrize("num_tokens", [1, 8, 64])
31
+@pytest.mark.parametrize("intermediate_size", [2048, 4096, 8192, 13824, 16384, 32768])
32
+def test_gelu_and_mul_sparse_triton(
33
+ dtype: torch.dtype, num_tokens: int, intermediate_size: int
34
+):
35
+ torch.manual_seed(0)
36
+ torch.set_default_device("cuda")
37
+ args = ir.ops.gelu_and_mul_sparse.generate_inputs(
38
+ num_tokens=num_tokens,
39
+ hidden_size=intermediate_size,
40
+ dtype=dtype,
41
+ )
42
+ impl = ir.ops.gelu_and_mul_sparse.impls["triton"]
43
+ assert impl.supports_args(*args)
44
+
45
+ expected = gelu_and_mul_sparse_native(*clone_args(args))
46
+ actual = impl.impl_fn(*clone_args(args))
47
+ assert_close(ir.ops.gelu_and_mul_sparse, actual, expected)
48
+
49
+ mask_mismatch = torch.count_nonzero((actual == 0) != (expected == 0))
50
+ assert mask_mismatch / actual.numel() < 1e-3
51
+
52
+
53
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA-only provider")
54
+def test_gelu_and_mul_sparse_triton_second_seed():
55
+ torch.manual_seed(1)
56
+ torch.set_default_device("cuda")
57
+ args = ir.ops.gelu_and_mul_sparse.generate_inputs(
58
+ num_tokens=32,
59
+ hidden_size=8192,
60
+ dtype=torch.bfloat16,
61
+ )
62
+
63
+ actual = ir.ops.gelu_and_mul_sparse.impls["triton"].impl_fn(*clone_args(args))
64
+ expected = gelu_and_mul_sparse_native(*clone_args(args))
65
+ assert_close(ir.ops.gelu_and_mul_sparse, actual, expected)
66
+
67
+
68
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA-only provider")
69
+@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
70
+@pytest.mark.parametrize("intermediate_size", [1, 31, 128])
71
+def test_gelu_and_mul_sparse_triton_small_sizes(
72
+ dtype: torch.dtype, intermediate_size: int
73
+):
74
+ torch.manual_seed(0)
75
+ torch.set_default_device("cuda")
76
+ args = ir.ops.gelu_and_mul_sparse.generate_inputs(
77
+ num_tokens=5,
78
+ hidden_size=intermediate_size,
79
+ dtype=dtype,
80
+ )
81
+
82
+ actual = ir.ops.gelu_and_mul_sparse.impls["triton"].impl_fn(*clone_args(args))
83
+ expected = gelu_and_mul_sparse_native(*clone_args(args))
84
+ assert_close(ir.ops.gelu_and_mul_sparse, actual, expected)
85
+ assert torch.equal(actual == 0, expected == 0)
86
+
87
+
88
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA-only provider")
89
+def test_gelu_and_mul_sparse_triton_nd_and_empty():
90
+ torch.set_default_device("cuda")
91
+ impl = ir.ops.gelu_and_mul_sparse.impls["triton"]
92
+
93
+ x = torch.randn(2, 3, 16384, dtype=torch.bfloat16)
94
+ args = (x, 1.6448536269514722, "tanh")
95
+ actual = impl.impl_fn(*args)
96
+ expected = gelu_and_mul_sparse_native(*args)
97
+ assert actual.shape == (2, 3, 8192)
98
+ assert_close(ir.ops.gelu_and_mul_sparse, actual, expected)
99
+
100
+ empty = torch.empty(0, 16384, dtype=torch.bfloat16)
101
+ empty_out = impl.impl_fn(empty, 1.6448536269514722, "tanh")
102
+ assert empty_out.shape == (0, 8192)
103
+ assert empty_out.dtype == empty.dtype
104
+
105
+
106
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA-only provider")
107
+@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
108
+def test_gelu_and_mul_sparse_triton_constant_rows(dtype: torch.dtype):
109
+ torch.set_default_device("cuda")
110
+ gate = torch.full((4, 8192), 3.0, dtype=dtype)
111
+ up = torch.randn_like(gate)
112
+ x = torch.cat((gate, up), dim=-1)
113
+ args = (x, 1.6448536269514722, "tanh")
114
+
115
+ actual = ir.ops.gelu_and_mul_sparse.impls["triton"].impl_fn(*args)
116
+ expected = gelu_and_mul_sparse_native(*args)
117
+ torch.testing.assert_close(actual, expected, rtol=0.0, atol=0.0)
118
+ assert torch.count_nonzero(actual) == 0
119
+
120
+
121
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA-only provider")
122
+@pytest.mark.parametrize("nonfinite", [float("nan"), float("inf")])
123
+def test_gelu_and_mul_sparse_triton_nonfinite_gate(nonfinite: float):
124
+ torch.set_default_device("cuda")
125
+ x = torch.randn(2, 16384, dtype=torch.bfloat16)
126
+ x[0, 0] = nonfinite
127
+ args = (x, 1.6448536269514722, "tanh")
128
+
129
+ actual = ir.ops.gelu_and_mul_sparse.impls["triton"].impl_fn(*args)
130
+ expected = gelu_and_mul_sparse_native(*args)
131
+ torch.testing.assert_close(actual, expected, equal_nan=True)
132
+
133
+
134
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA-only provider")
135
+def test_gelu_and_mul_sparse_triton_fallback_conditions():
136
+ torch.set_default_device("cuda")
137
+ impl = ir.ops.gelu_and_mul_sparse.impls["triton"]
138
+ x = torch.randn(4, 16384, dtype=torch.bfloat16)
139
+
140
+ assert impl.supports_args(x, 1.0, "tanh")
141
+ assert not impl.supports_args(x, 1.0, "none")
142
+ assert not impl.supports_args(x[:, ::2], 1.0, "tanh")
143
+ assert not impl.supports_args(x[:, :-1], 1.0, "tanh")
144
+ assert not impl.supports_args(
145
+ torch.randn(1, 65538, dtype=torch.bfloat16), 1.0, "tanh"
146
+ )
147
+
148
+
149
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA-only provider")
150
+def test_gelu_and_mul_sparse_triton_dispatch_and_opcheck():
151
+ torch.set_default_device("cuda")
152
+ args = ir.ops.gelu_and_mul_sparse.generate_inputs(
153
+ num_tokens=8,
154
+ hidden_size=4096,
155
+ dtype=torch.bfloat16,
156
+ )
157
+ impl = ir.ops.gelu_and_mul_sparse.impls["triton"]
158
+
159
+ with ir.ops.gelu_and_mul_sparse.set_priority(["triton", "native"]):
160
+ dispatched = ir.ops.gelu_and_mul_sparse(*args)
161
+ torch.library.opcheck(torch.ops.vllm_ir.gelu_and_mul_sparse, args)
162
+
163
+ direct = impl.impl_fn(*args)
164
+ torch.testing.assert_close(dispatched, direct, rtol=0.0, atol=0.0)
165
+
166
+
167
+@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA-only provider")
168
+def test_gelu_and_mul_sparse_triton_cudagraph():
169
+ torch.set_default_device("cuda")
170
+ x = torch.randn(16, 32768, dtype=torch.bfloat16)
171
+ impl = ir.ops.gelu_and_mul_sparse.impls["triton"]
172
+
173
+ for _ in range(3):
174
+ expected = impl.impl_fn(x, 1.6448536269514722, "tanh")
175
+ torch.accelerator.synchronize()
176
+
177
+ graph = torch.cuda.CUDAGraph()
178
+ with torch.cuda.graph(graph):
179
+ actual = impl.impl_fn(x, 1.6448536269514722, "tanh")
180
+ graph.replay()
181
+ torch.accelerator.synchronize()
182
+
183
+ torch.testing.assert_close(actual, expected, rtol=0.0, atol=0.0)
tests/test_config.py
+16
@@ -3322,6 +3322,22 @@ def test_ir_op_priority_default():
3322
assert priority_config.fused_add_rms_norm == ["native"]
3323
3324
3325
+@pytest.mark.parametrize("mode", [CompilationMode.NONE, CompilationMode.VLLM_COMPILE])
3326
+@pytest.mark.parametrize("backend", ["inductor", "eager"])
3327
+def test_ir_op_platform_defaults_support_sparse_gelu(mode, backend):
3328
+ """Worker initialization must not select an unregistered sparse GELU provider."""
3329
+ from vllm import ir
3330
+
3331
+ config = SimpleNamespace(
3332
+ compilation_config=CompilationConfig(mode=mode, backend=backend)
3333
+ )
3334
+ priority = current_platform.get_default_ir_op_priority(config)
3335
+ expected = ["triton", "native"] if current_platform.is_cuda() else ["native"]
3336
+
3337
+ with priority.set_priority():
3338
+ assert ir.ops.gelu_and_mul_sparse.get_priority() == expected
3339
+
3340
+
3341
def test_ir_op_priority_str():
3342
"""Test that passing a comma-delimited string works"""
3343
from vllm.config.kernel import IrOpPriorityConfig
vllm/config/kernel.py
+3
@@ -34,6 +34,9 @@ class IrOpPriorityConfig:
34
fused_add_rms_norm: list[str] = Field(default_factory=list)
35
"""Priority list for vllm.ir.ops.fused_add_rms_norm"""
36
37
+ gelu_and_mul_sparse: list[str] = Field(default_factory=list)
38
+ """Priority list for vllm.ir.ops.gelu_and_mul_sparse"""
39
+
40
def compute_hash(self) -> str:
41
"""
42
Produces a hash unique to the pass configuration.
vllm/ir/ops/__init__.py
+2
-1
@@ -1,5 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from .activation import gelu_and_mul_sparse
4
from .layernorm import fused_add_rms_norm, rms_norm
5
5
-__all__ = ["rms_norm", "fused_add_rms_norm"]
6
+__all__ = ["rms_norm", "fused_add_rms_norm", "gelu_and_mul_sparse"]
vllm/ir/ops/activation.py
new
+34
@@ -0,0 +1,34 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import torch
5
+import torch.nn.functional as F
6
+from torch import Tensor
7
+
8
+from ..op import register_op
9
+
10
+
11
+@register_op
12
+def gelu_and_mul_sparse(
13
+ x: Tensor, std_multiplier: float, approximate: str = "none"
14
+) -> Tensor:
15
+ """Apply Gaussian sparsification, GELU, and gated multiplication."""
16
+ d = x.shape[-1] // 2
17
+ gate = x[..., :d]
18
+ # Statistics intentionally remain local to each tensor-parallel shard.
19
+ mean = torch.mean(gate, dim=-1, keepdim=True)
20
+ std = torch.std(gate, dim=-1, keepdim=True, unbiased=False)
21
+ sparse_gate = F.relu(gate - (mean + std * std_multiplier))
22
+ return F.gelu(sparse_gate, approximate=approximate) * x[..., d:]
23
+
24
+
25
+@gelu_and_mul_sparse.register_input_generator
26
+def _gelu_and_mul_sparse_input_generator(
27
+ num_tokens: int,
28
+ hidden_size: int,
29
+ dtype: torch.dtype,
30
+ std_multiplier: float = 1.6448536269514722,
31
+ approximate: str = "tanh",
32
+) -> tuple:
33
+ x = torch.randn(num_tokens, hidden_size * 2, dtype=dtype)
34
+ return x, std_multiplier, approximate
vllm/kernels/__init__.py
+2
-2
@@ -2,6 +2,6 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""Kernel implementations for vLLM."""
4
5
-from . import aiter_ops, oink_ops, vllm_c
5
+from . import aiter_ops, oink_ops, triton, vllm_c
6
7
-__all__ = ["vllm_c", "aiter_ops", "oink_ops"]
7
+__all__ = ["vllm_c", "aiter_ops", "oink_ops", "triton"]
vllm/kernels/triton/__init__.py
+4
@@ -1,3 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""Triton kernel implementations."""
4
+
5
+from . import activation
6
+
7
+__all__ = ["activation"]
vllm/kernels/triton/activation.py
new
+110
@@ -0,0 +1,110 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import torch
5
+from torch import Tensor
6
+from torch.library import triton_op, wrap_triton
7
+
8
+from vllm import ir
9
+from vllm.platforms import current_platform
10
+from vllm.triton_utils import HAS_TRITON, tl, triton
11
+
12
+_MAX_INTERMEDIATE_SIZE = 32768
13
+_SUPPORTED_DTYPES = {torch.float16, torch.bfloat16, torch.float32}
14
+
15
+
16
+@triton.jit
17
+def _gelu_and_mul_sparse_kernel(
18
+ x_ptr,
19
+ output_ptr,
20
+ d: tl.constexpr,
21
+ std_multiplier,
22
+ BLOCK_SIZE: tl.constexpr,
23
+):
24
+ row = tl.program_id(0).to(tl.int64)
25
+ offsets = tl.arange(0, BLOCK_SIZE)
26
+ mask = offsets < d
27
+ row_start = row * 2 * d
28
+
29
+ gate = tl.load(x_ptr + row_start + offsets, mask=mask, other=0.0).to(tl.float32)
30
+ # TODO: Investigate delaying this load to reduce spills for large reductions
31
+ # without regressing smaller shapes.
32
+ up = tl.load(x_ptr + row_start + d + offsets, mask=mask, other=0.0).to(tl.float32)
33
+
34
+ input_dtype = x_ptr.dtype.element_ty
35
+ mean = tl.sum(gate, axis=0) / d
36
+ centered = tl.where(mask, gate - mean, 0.0)
37
+ variance = tl.sum(centered * centered, axis=0) / d
38
+ # Match the input-dtype rounding at each native PyTorch op boundary.
39
+ mean = mean.to(input_dtype).to(tl.float32)
40
+ std = tl.sqrt(variance).to(input_dtype).to(tl.float32)
41
+ scaled_std = (std * std_multiplier).to(input_dtype).to(tl.float32)
42
+ cutoff = (mean + scaled_std).to(input_dtype).to(tl.float32)
43
+ sparse_gate = (gate - cutoff).to(input_dtype).to(tl.float32)
44
+ sparse_gate = tl.where(sparse_gate < 0.0, 0.0, sparse_gate)
45
+
46
+ inner = 0.7978845608028654 * (
47
+ sparse_gate + 0.044715 * sparse_gate * sparse_gate * sparse_gate
48
+ )
49
+ tanh = 2.0 * tl.sigmoid(2.0 * inner) - 1.0
50
+ activated = 0.5 * sparse_gate * (1.0 + tanh)
51
+ activated = activated.to(input_dtype).to(tl.float32)
52
+ tl.store(output_ptr + row * d + offsets, activated * up, mask=mask)
53
+
54
+
55
+def _supports_gelu_and_mul_sparse(
56
+ x: Tensor, std_multiplier: float, approximate: str = "none"
57
+) -> bool:
58
+ del std_multiplier
59
+ if (
60
+ x.device.type != "cuda"
61
+ or x.dtype not in _SUPPORTED_DTYPES
62
+ or not x.is_contiguous()
63
+ or x.ndim == 0
64
+ or approximate != "tanh"
65
+ ):
66
+ return False
67
+ last_dim = x.shape[-1]
68
+ return (
69
+ last_dim > 0 and last_dim % 2 == 0 and last_dim // 2 <= _MAX_INTERMEDIATE_SIZE
70
+ )
71
+
72
+
73
+@triton_op(
74
+ "vllm::gelu_and_mul_sparse_triton",
75
+ mutates_args=(),
76
+)
77
+def _gelu_and_mul_sparse_triton_op(x: Tensor, std_multiplier: float) -> Tensor:
78
+ d = x.shape[-1] // 2
79
+ output = torch.empty((*x.shape[:-1], d), dtype=x.dtype, device=x.device)
80
+ if output.numel() == 0:
81
+ return output
82
+
83
+ n_rows = x.numel() // x.shape[-1]
84
+
85
+ def grid(meta):
86
+ return (n_rows,)
87
+
88
+ block_size = triton.next_power_of_2(d)
89
+ num_warps = min(max(block_size // 256, 1), 8)
90
+ wrap_triton(_gelu_and_mul_sparse_kernel)[grid](
91
+ x,
92
+ output,
93
+ d=d,
94
+ std_multiplier=std_multiplier,
95
+ BLOCK_SIZE=block_size,
96
+ num_warps=num_warps,
97
+ )
98
+ return output
99
+
100
+
101
+@ir.ops.gelu_and_mul_sparse.register_impl(
102
+ "triton",
103
+ supported=HAS_TRITON and current_platform.is_cuda(),
104
+ supports_args=_supports_gelu_and_mul_sparse,
105
+)
106
+def gelu_and_mul_sparse_triton(
107
+ x: Tensor, std_multiplier: float, approximate: str = "none"
108
+) -> Tensor:
109
+ assert approximate == "tanh"
110
+ return _gelu_and_mul_sparse_triton_op(x, std_multiplier)
vllm/model_executor/layers/activation.py
+3
-15
@@ -8,6 +8,7 @@ import torch
8
import torch.nn as nn
9
import torch.nn.functional as F
10
11
+from vllm import ir
12
from vllm.logger import init_logger
13
from vllm.model_executor.custom_op import CustomOp
14
from vllm.platforms import CpuArchEnum, current_platform
@@ -332,24 +333,11 @@ class GeluAndMulSparse(CustomOp):
333
raise ValueError("activation_sparsity is 0.0. Please use GeluAndMul.")
334
target_sparsity_tensor = torch.tensor(activation_sparsity, dtype=torch.float32)
335
normal_dist = torch.distributions.normal.Normal(0, 1)
335
- self.std_multiplier = normal_dist.icdf(target_sparsity_tensor)
336
-
337
- def _gaussian_topk(self, x: torch.Tensor) -> torch.Tensor:
338
- """Get % sparse percentile of the Gaussian distribution."""
339
- # NOTE(rob): for TP>1, we could all-gather to get the means/std.
340
- # But we do not do this because in expectation they are the same
341
- # and in practice the eval scores are good without gathering.
342
- mean = torch.mean(x, dim=-1, keepdim=True)
343
- std = torch.std(x, dim=-1, keepdim=True, unbiased=False)
344
- cutoff_x = mean + std * self.std_multiplier
345
- return nn.functional.relu(x - cutoff_x)
336
+ self.std_multiplier = normal_dist.icdf(target_sparsity_tensor).item()
337
338
def forward_native(self, x: torch.Tensor) -> torch.Tensor:
339
"""PyTorch-native implementation equivalent to forward()."""
349
- d = x.shape[-1] // 2
350
- out = self._gaussian_topk(x[..., :d])
351
- out = F.gelu(out, approximate=self.approximate)
352
- return out * x[..., d:]
340
+ return ir.ops.gelu_and_mul_sparse(x, self.std_multiplier, self.approximate)
341
342
def forward_cuda(self, x: torch.Tensor) -> torch.Tensor:
343
return self.forward_native(x)
vllm/platforms/cuda.py
+4
-1
@@ -755,7 +755,10 @@ class CudaPlatformBase(Platform):
755
rms_norm = ["oink"] + default
756
757
return IrOpPriorityConfig.with_default(
758
- default, rms_norm=rms_norm, fused_add_rms_norm=rms_norm
758
+ default,
759
+ rms_norm=rms_norm,
760
+ fused_add_rms_norm=rms_norm,
761
+ gelu_and_mul_sparse=["triton", "native"],
762
)
763
764
@classmethod
vllm/platforms/rocm.py
+4
-1
@@ -1161,7 +1161,10 @@ class RocmPlatform(Platform):
1161
rms_norm = default
1162
1163
return IrOpPriorityConfig.with_default(
1164
- default, rms_norm=rms_norm, fused_add_rms_norm=rms_norm
1164
+ default,
1165
+ rms_norm=rms_norm,
1166
+ fused_add_rms_norm=rms_norm,
1167
+ gelu_and_mul_sparse=["native"],
1168
)
1169
1170
@classmethod
vllm/platforms/xpu.py
+1
-1
@@ -491,7 +491,7 @@ class XPUPlatform(Platform):
491
using_inductor = cc.backend == "inductor" and cc.mode != CompilationMode.NONE
492
default = ["native"] if using_inductor else ["vllm_c", "native"]
493
494
- return IrOpPriorityConfig.with_default(default)
494
+ return IrOpPriorityConfig.with_default(default, gelu_and_mul_sparse=["native"])
495
496
@classmethod
497
def device_count(cls) -> int: