A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 5, 12:15 UTC
[NVFP4] Support NVFP4 dense models from `modelopt` and `compressed-tensors` on AMD Instinct MI300, MI355X and Hopper through emulation (#35733)
Signed-off-by: Felix Marty <Felix.Marty@amd.com> Signed-off-by: fxmarty-amd <felmarty@amd.com> Co-authored-by: Kyle Sayers <kylesayrs@gmail.com>
fxmarty-amd committed
Apr 7, 2026 at 00:18 UTC
00d7b497b33679a3ce641db9119eb09dd4ed5e30
10 files changed
+191
-58
tests/models/quantization/test_nvfp4.py
+15
-4
@@ -89,22 +89,33 @@ def test_models(example_prompts, model_name) -> None:
89
90
EAGER = [True, False]
91
92
+SM_100_NVFP4_BACKENDS = [
93
+ "flashinfer-cudnn",
94
+ "flashinfer-trtllm",
95
+ "flashinfer-cutlass",
96
+]
97
+
98
93
-@pytest.mark.skipif(
94
- not current_platform.has_device_capability(100),
95
- reason="modelopt_fp4 is not supported on this GPU type.",
96
-)
99
@pytest.mark.parametrize("model", ["nvidia/Llama-3.1-8B-Instruct-NVFP4"])
100
@pytest.mark.parametrize("eager", EAGER)
101
@pytest.mark.parametrize(
102
"backend",
103
[
104
+ "emulation",
105
"flashinfer-cudnn",
106
"flashinfer-trtllm", # the small seq_len ensures trtllm_8x4_layout backend is used
107
"flashinfer-cutlass",
108
],
109
)
110
def test_nvfp4(vllm_runner, model, eager, backend, monkeypatch):
111
+ if (
112
+ not current_platform.has_device_capability(100)
113
+ and backend in SM_100_NVFP4_BACKENDS
114
+ ):
115
+ pytest.skip(
116
+ f"The backend {backend} is not supported with current_platform.has_device_capability(100) == False"
117
+ )
118
+
119
monkeypatch.setenv("VLLM_NVFP4_GEMM_BACKEND", backend)
120
with vllm_runner(model, enforce_eager=eager) as llm:
121
output = llm.generate_greedy(["1 2 3 4 5"], max_tokens=2)
tests/quantization/test_compressed_tensors.py
+1
-4
@@ -366,9 +366,6 @@ def test_compressed_tensors_kv_cache_fp8_per_attn_head(vllm_runner):
366
assert output
367
368
369
-@pytest.mark.skipif(
370
- not current_platform.is_cuda(), reason="This test is skipped on non-CUDA platform."
371
-)
369
@pytest.mark.parametrize(
370
"args",
371
[
@@ -398,7 +395,7 @@ def test_compressed_tensors_nvfp4(vllm_runner, args):
395
assert qkv_proj.scheme.group_size == 16
396
397
llm.apply_model(check_model)
401
- output = llm.generate_greedy("Hello my name is", max_tokens=4)
398
+ output = llm.generate_greedy(["Hello my name is"], max_tokens=4)
399
print(output)
400
assert output
401
vllm/envs.py
+5
@@ -1464,6 +1464,10 @@ environment_variables: dict[str, Callable[[], Any]] = {
1464
# - "flashinfer-trtllm": use flashinfer trtllm GEMM backend
1465
# - "flashinfer-cutlass": use flashinfer cutlass GEMM backend
1466
# - "marlin": use marlin GEMM backend (for GPUs without native FP4 support)
1467
+ # - "emulation":
1468
+ # use BF16/FP16 GEMM, dequantizing weights and running QDQ on activations.
1469
+ # This is only meant for research purposes to run on devices where NVFP4
1470
+ # GEMM kernels are not available.
1471
# - <none>: automatically pick an available backend
1472
"VLLM_NVFP4_GEMM_BACKEND": env_with_choices(
1473
"VLLM_NVFP4_GEMM_BACKEND",
@@ -1474,6 +1478,7 @@ environment_variables: dict[str, Callable[[], Any]] = {
1478
"flashinfer-cutlass",
1479
"cutlass",
1480
"marlin",
1481
+ "emulation",
1482
],
1483
),
1484
# Controls garbage collection during CUDA graph capture.
vllm/model_executor/layers/quantization/compressed_tensors/schemes/compressed_tensors_w4a4_nvfp4.py
+23
@@ -5,10 +5,12 @@ from collections.abc import Callable
5
import torch
6
from torch.nn.parameter import Parameter
7
8
+from vllm.logger import init_logger
9
from vllm.model_executor.layers.quantization.compressed_tensors.schemes import (
10
CompressedTensorsScheme,
11
)
12
from vllm.model_executor.layers.quantization.utils.nvfp4_utils import (
13
+ NvFp4LinearBackend,
14
apply_nvfp4_linear,
15
convert_to_nvfp4_linear_kernel_format,
16
select_nvfp4_linear_backend,
@@ -19,6 +21,9 @@ from vllm.model_executor.parameter import (
21
PerTensorScaleParameter,
22
)
23
24
+logger = init_logger(__name__)
25
+
26
+
27
__all__ = ["CompressedTensorsW4A4Fp4"]
28
29
@@ -27,6 +32,10 @@ class CompressedTensorsW4A4Fp4(CompressedTensorsScheme):
32
self.backend = select_nvfp4_linear_backend()
33
self.group_size = 16
34
35
+ self.swizzle = None
36
+ if self.backend == NvFp4LinearBackend.EMULATION:
37
+ self.swizzle = False
38
+
39
@classmethod
40
def get_min_capability(cls) -> int:
41
return 75
@@ -89,6 +98,19 @@ class CompressedTensorsW4A4Fp4(CompressedTensorsScheme):
98
# Rename CT checkpoint names to standardized names
99
layer.weight = layer.weight_packed
100
del layer.weight_packed
101
+
102
+ if (
103
+ torch.unique(layer.input_global_scale).numel() != 1
104
+ or torch.unique(layer.weight_global_scale).numel() != 1
105
+ ):
106
+ logger.warning_once(
107
+ "In NVFP4 linear, the global scale for input or weight are different"
108
+ " for parallel layers (e.g. q_proj, k_proj, v_proj). This "
109
+ " will likely result in reduced accuracy. Please verify the model"
110
+ " accuracy. Consider using a checkpoint with a shared global NVFP4"
111
+ " scale for fused layers."
112
+ )
113
+
114
# Process global scales (CT stores as divisors, i.e. 1/scale)
115
input_global_scale_inv = layer.input_global_scale.max().to(torch.float32)
116
layer.input_global_scale = Parameter(
@@ -121,4 +143,5 @@ class CompressedTensorsW4A4Fp4(CompressedTensorsScheme):
143
layer=layer,
144
x=x,
145
bias=bias,
146
+ swizzle=self.swizzle,
147
)
vllm/model_executor/layers/quantization/modelopt.py
+19
@@ -71,6 +71,7 @@ from vllm.model_executor.layers.quantization.utils.mxfp8_utils import (
71
mxfp8_e4m3_quantize,
72
)
73
from vllm.model_executor.layers.quantization.utils.nvfp4_utils import (
74
+ NvFp4LinearBackend,
75
apply_nvfp4_linear,
76
convert_to_nvfp4_linear_kernel_format,
77
select_nvfp4_linear_backend,
@@ -1074,6 +1075,10 @@ class ModelOptNvFp4LinearMethod(LinearMethodBase):
1075
self.marlin_input_dtype = None
1076
self.backend = select_nvfp4_linear_backend()
1077
1078
+ self.swizzle = None
1079
+ if self.backend == NvFp4LinearBackend.EMULATION:
1080
+ self.swizzle = False
1081
+
1082
def create_weights(
1083
self,
1084
layer: torch.nn.Module,
@@ -1149,10 +1154,23 @@ class ModelOptNvFp4LinearMethod(LinearMethodBase):
1154
layer.register_parameter("weight_scale", weight_scale)
1155
1156
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
1157
+ if (
1158
+ torch.unique(layer.input_scale).numel() != 1
1159
+ or torch.unique(layer.weight_scale_2).numel() != 1
1160
+ ):
1161
+ logger.warning_once(
1162
+ "In NVFP4 linear, the global scale for input or weight are different"
1163
+ " for parallel layers (e.g. q_proj, k_proj, v_proj). This "
1164
+ " will likely results in reduce accuracy. Please verify the model"
1165
+ " accuracy. Consider using a checkpoint with a shared global NVFP4"
1166
+ " scale for parallel layers."
1167
+ )
1168
+
1169
# Rename ModelOpt checkpoint names to standardized names
1170
input_global_scale = layer.input_scale.max().to(torch.float32)
1171
layer.input_global_scale = Parameter(input_global_scale, requires_grad=False)
1172
del layer.input_scale
1173
+
1174
weight_global_scale = layer.weight_scale_2.max().to(torch.float32)
1175
layer.weight_global_scale = Parameter(weight_global_scale, requires_grad=False)
1176
del layer.weight_scale_2
@@ -1179,6 +1197,7 @@ class ModelOptNvFp4LinearMethod(LinearMethodBase):
1197
layer=layer,
1198
x=x,
1199
bias=bias,
1200
+ swizzle=self.swizzle,
1201
)
1202
1203
vllm/model_executor/layers/quantization/utils/marlin_utils_fp4.py
+1
-1
@@ -24,7 +24,7 @@ logger = init_logger(__name__)
24
25
26
def is_fp4_marlin_supported():
27
- return current_platform.has_device_capability(75)
27
+ return current_platform.is_cuda() and current_platform.has_device_capability(75)
28
29
30
def _nvfp4_compute_scale_factor(
vllm/model_executor/layers/quantization/utils/nvfp4_emulation_utils.py
+22
-9
@@ -1,5 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from types import SimpleNamespace
4
+
5
import torch
6
7
from vllm.scalar_type import scalar_types
@@ -11,9 +13,10 @@ __all__ = [
13
]
14
15
FLOAT4_E2M1_MAX = scalar_types.float4_e2m1f.max()
16
+FLOAT4_E2M1_MAX_RECIPROCAL = 1 / FLOAT4_E2M1_MAX
17
15
-kE2M1ToFloat = torch.tensor(
16
- [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0], dtype=torch.float32
18
+kE2M1ToFloat_handle = SimpleNamespace(
19
+ val=torch.tensor([0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0], dtype=torch.float32)
20
)
21
22
@@ -29,8 +32,9 @@ def break_fp4_bytes(a, dtype):
32
# Vectorized sign and magnitude extraction
33
signs = (combined & 0x08).to(torch.bool) # Sign bits
34
abs_vals = (combined & 0x07).to(torch.long)
35
+
36
+ kE2M1 = kE2M1ToFloat_handle.val
37
# Device-aware lookup and sign application
33
- kE2M1 = kE2M1ToFloat.to(device=a.device)
38
values = kE2M1[abs_vals] * torch.where(signs, -1.0, 1.0)
39
# Reshape to final form
40
return values.reshape(m, n * 2).to(dtype=dtype)
@@ -47,7 +51,12 @@ def convert_swizzled_to_linear(a_sf_swizzled: torch.Tensor, m, k, block_size):
51
52
53
def dequantize_to_dtype(
50
- tensor_fp4, tensor_sf, global_scale, dtype, device, block_size=16
54
+ tensor_fp4: torch.Tensor,
55
+ tensor_sf: torch.Tensor,
56
+ global_scale: torch.Tensor | float,
57
+ dtype: torch.dtype,
58
+ block_size: int = 16,
59
+ swizzle: bool | None = True,
60
):
61
"""Dequantize the fp4 tensor back to high precision."""
62
# Two fp4 values are packed into one uint8.
@@ -57,8 +66,10 @@ def dequantize_to_dtype(
66
tensor_f32 = break_fp4_bytes(tensor_fp4, torch.float32)
67
tensor_f32 = tensor_f32.reshape(m, k // block_size, block_size)
68
tensor_sf = tensor_sf.view(torch.float8_e4m3fn)
60
- tensor_sf = convert_swizzled_to_linear(tensor_sf, m, k, block_size)
61
- tensor_sf_dtype = tensor_sf.to(torch.float32) / global_scale
69
+
70
+ if swizzle:
71
+ tensor_sf = convert_swizzled_to_linear(tensor_sf, m, k, block_size)
72
+ tensor_sf_dtype = tensor_sf.to(torch.float32) * global_scale
73
74
# scale the tensor
75
out = (tensor_f32 * tensor_sf_dtype.unsqueeze(-1)).reshape(m, k)
@@ -67,7 +78,8 @@ def dequantize_to_dtype(
78
79
def get_reciprocal(x):
80
if isinstance(x, torch.Tensor):
70
- return torch.where(x == 0, torch.tensor(0.0, dtype=x.dtype), 1.0 / x)
81
+ # torch.where yields operation not permitted when stream is capturing.
82
+ return 1.0 / (x + (x == 0) * 1e8)
83
elif isinstance(x, (float, int)):
84
return 0.0 if x == 0 else 1.0 / x
85
else:
@@ -94,7 +106,7 @@ def ref_nvfp4_quant(x, global_scale, block_size):
106
m, n = x.shape
107
x = torch.reshape(x, (m, n // block_size, block_size))
108
vec_max = torch.max(torch.abs(x), dim=-1, keepdim=True)[0].to(torch.float32)
97
- scale = global_scale * (vec_max * get_reciprocal(FLOAT4_E2M1_MAX))
109
+ scale = global_scale * (vec_max * FLOAT4_E2M1_MAX_RECIPROCAL)
110
scale = torch.clamp(scale, max=448, min=-448)
111
scale = scale.to(torch.float8_e4m3fn).to(torch.float32)
112
output_scale = get_reciprocal(scale * get_reciprocal(global_scale))
@@ -111,6 +123,7 @@ def run_nvfp4_emulations(
123
weight: torch.Tensor,
124
weight_scale_swizzled: torch.Tensor,
125
weight_global_scale: torch.Tensor,
126
+ swizzle: bool | None = True,
127
):
128
group_size = 16
129
x_m, x_k = x.shape
@@ -132,8 +145,8 @@ def run_nvfp4_emulations(
145
weight_scale_swizzled.data,
146
weight_global_scale,
147
output_dtype,
135
- x.device,
148
group_size,
149
+ swizzle=swizzle,
150
)
151
152
# matmul
vllm/model_executor/layers/quantization/utils/nvfp4_utils.py
+99
-40
@@ -17,31 +17,99 @@ from vllm.model_executor.layers.quantization.utils.marlin_utils_fp4 import (
17
prepare_fp4_layer_for_marlin,
18
)
19
from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import (
20
+ kE2M1ToFloat_handle,
21
run_nvfp4_emulations,
22
)
23
from vllm.platforms import current_platform
24
from vllm.utils.flashinfer import flashinfer_scaled_fp4_mm, has_flashinfer
25
+from vllm.utils.import_utils import has_fbgemm_gpu
26
from vllm.utils.math_utils import round_up
27
28
logger = init_logger(__name__)
29
30
31
+# NOTE: This is ordered by preferred backend.
32
+# Example: if both are available, FLASHINFER_CUTLASS is preferred to VLLM_CUTLASS.
33
class NvFp4LinearBackend(Enum):
30
- VLLM_CUTLASS = "cutlass"
34
FLASHINFER_CUTLASS = "flashinfer-cutlass"
35
+ VLLM_CUTLASS = "cutlass"
36
+ MARLIN = "marlin"
37
FLASHINFER_TRTLLM = "flashinfer-trtllm"
38
FLASHINFER_CUDNN = "flashinfer-cudnn"
39
FBGEMM = "fbgemm"
35
- MARLIN = "marlin"
40
EMULATION = "emulation"
41
42
43
+NVFP4_LINEAR_BACKENDS = list(NvFp4LinearBackend)
44
+
45
+
46
+def is_backend_supported(backend: NvFp4LinearBackend) -> tuple[bool, str | None]:
47
+ reason = None
48
+ supported = True
49
+
50
+ if backend == NvFp4LinearBackend.FLASHINFER_CUTLASS:
51
+ # cutlass_fp4_supported() checks that the vLLM NVFP4 kernels (both
52
+ # quantization and GEMM) were compiled for the current SM version.
53
+ # FlashInfer backends still rely on the vLLM quantization kernels,
54
+ # so we gate them on the same check.
55
+ supported = (
56
+ cutlass_fp4_supported()
57
+ and current_platform.has_device_capability(100)
58
+ and has_flashinfer()
59
+ )
60
+
61
+ if not supported:
62
+ reason = "FlashInfer is required, >=sm_100 is required"
63
+ elif backend == NvFp4LinearBackend.VLLM_CUTLASS:
64
+ supported = cutlass_fp4_supported()
65
+ if not supported:
66
+ reason = "Cutlass is required"
67
+ elif backend == NvFp4LinearBackend.MARLIN:
68
+ supported = is_fp4_marlin_supported()
69
+ if not supported:
70
+ reason = "Marlin is required"
71
+ elif backend in [
72
+ NvFp4LinearBackend.FLASHINFER_TRTLLM,
73
+ NvFp4LinearBackend.FLASHINFER_CUDNN,
74
+ ]:
75
+ supported = has_flashinfer()
76
+ if not supported:
77
+ reason = "FlashInfer is required"
78
+ elif backend == NvFp4LinearBackend.FBGEMM:
79
+ supported = has_fbgemm_gpu()
80
+ if not supported:
81
+ reason = "fbgemm_gpu is required"
82
+ elif backend == NvFp4LinearBackend.EMULATION:
83
+ # e.g. AMD Instinct does not support native NVFP4.
84
+ unsupported_reasons = {}
85
+ for other_backend in NVFP4_LINEAR_BACKENDS:
86
+ if other_backend == NvFp4LinearBackend.EMULATION:
87
+ continue
88
+ other_supported, other_reason = is_backend_supported(other_backend)
89
+ if not other_supported:
90
+ unsupported_reasons[other_backend] = other_reason
91
+
92
+ if unsupported_reasons:
93
+ unsupported_reasons_str = "\n - ".join(
94
+ [f"{b.value}: {r}" for b, r in unsupported_reasons.items()]
95
+ )
96
+ logger.warning_once(
97
+ f"NVFP4 linear falling back to the slow and unoptimized "
98
+ f"backend=NvFp4LinearBackend.EMULATION as no optimized backend is "
99
+ f"available (unavailable reasons:\n - {unsupported_reasons_str}\n). "
100
+ "In case you expect one of these backend to be used, "
101
+ "please verify your environment."
102
+ )
103
+
104
+ return supported, reason
105
+
106
+
107
def select_nvfp4_linear_backend() -> NvFp4LinearBackend:
108
"""
109
Select the best available NVFP4 GEMM backend based on environment
110
configuration and platform capabilities.
111
"""
44
- backend: NvFp4LinearBackend | None = None
112
+ selected_backend: NvFp4LinearBackend | None = None
113
114
if envs.VLLM_USE_FBGEMM:
115
try:
@@ -51,51 +119,36 @@ def select_nvfp4_linear_backend() -> NvFp4LinearBackend:
119
"Backend fbgemm requires fbgemm.f4f4bf16 operator, "
120
"Please install with: pip install fbgemm-gpu-genai"
121
) from exc
54
- backend = NvFp4LinearBackend.FBGEMM
122
+ selected_backend = NvFp4LinearBackend.FBGEMM
123
elif envs.VLLM_USE_NVFP4_CT_EMULATIONS:
56
- backend = NvFp4LinearBackend.EMULATION
124
+ selected_backend = NvFp4LinearBackend.EMULATION
125
elif envs.VLLM_NVFP4_GEMM_BACKEND is None:
58
- # Auto-select best available backend.
59
- # cutlass_fp4_supported() checks that the vLLM NVFP4 kernels (both
60
- # quantization and GEMM) were compiled for the current SM version.
61
- # FlashInfer backends still rely on the vLLM quantization kernels,
62
- # so we gate them on the same check.
63
- if (
64
- cutlass_fp4_supported()
65
- and current_platform.has_device_capability(100)
66
- and has_flashinfer()
67
- ):
68
- backend = NvFp4LinearBackend.FLASHINFER_CUTLASS
69
- elif cutlass_fp4_supported():
70
- backend = NvFp4LinearBackend.VLLM_CUTLASS
71
- elif is_fp4_marlin_supported():
72
- backend = NvFp4LinearBackend.MARLIN
126
+ for backend in NVFP4_LINEAR_BACKENDS:
127
+ supported, reason = is_backend_supported(backend)
128
+ if supported:
129
+ selected_backend = backend
130
+ break
131
else:
74
- backend = NvFp4LinearBackend(envs.VLLM_NVFP4_GEMM_BACKEND)
132
+ selected_backend = NvFp4LinearBackend(envs.VLLM_NVFP4_GEMM_BACKEND)
133
76
- # Validate that the backend is supported
77
- if backend in (
78
- NvFp4LinearBackend.FLASHINFER_CUTLASS,
79
- NvFp4LinearBackend.FLASHINFER_TRTLLM,
80
- NvFp4LinearBackend.FLASHINFER_CUDNN,
81
- ):
82
- assert has_flashinfer(), f"FlashInfer is required for {backend}"
83
- assert cutlass_fp4_supported(), (
84
- f"{backend} requires vLLM NVFP4 quantization kernels compiled "
85
- f"for the current GPU (SM {current_platform.get_device_capability()})"
86
- )
87
- elif backend == NvFp4LinearBackend.VLLM_CUTLASS:
88
- assert cutlass_fp4_supported(), f"Cutlass is required for {backend}"
89
- elif backend == NvFp4LinearBackend.MARLIN:
90
- assert is_fp4_marlin_supported(), f"Marlin is required for {backend}"
91
- elif backend is None:
134
+ if selected_backend is None:
135
raise ValueError(
136
f"No NVFP4 GEMM backend selected, "
94
- f"available backends: {list(NvFp4LinearBackend)}"
137
+ f"available backends: {NVFP4_LINEAR_BACKENDS}"
138
)
139
97
- logger.info_once(f"Using {backend} for NVFP4 GEMM")
98
- return backend
140
+ supported, reason = is_backend_supported(selected_backend)
141
+
142
+ if not supported:
143
+ raise ValueError(
144
+ f"The selected backend={selected_backend} is not supported in current "
145
+ f"environment. Reason: {reason}. Current environment: "
146
+ f"{envs.VLLM_USE_FBGEMM=}, {envs.VLLM_USE_NVFP4_CT_EMULATIONS=}, "
147
+ f"{envs.VLLM_NVFP4_GEMM_BACKEND}."
148
+ )
149
+
150
+ logger.info_once(f"Using {selected_backend} for NVFP4 GEMM")
151
+ return selected_backend
152
153
154
def prepare_weights_for_nvfp4_flashinfer_trtllm(
@@ -183,6 +236,10 @@ def convert_to_nvfp4_linear_kernel_format(
236
layer.weight = torch.nn.Parameter(weight, requires_grad=False)
237
layer.weight_scale = torch.nn.Parameter(weight_scale, requires_grad=False)
238
layer.weights_padding_cols = weights_padding_cols
239
+ elif backend == NvFp4LinearBackend.EMULATION:
240
+ # We can not call `.to(device)` during cuda graph capture - do it here instead.
241
+ # (operation not permitted when stream is capturing)
242
+ kE2M1ToFloat_handle.val = kE2M1ToFloat_handle.val.to(layer.weight.device)
243
244
245
def apply_nvfp4_linear(
@@ -190,6 +247,7 @@ def apply_nvfp4_linear(
247
layer: torch.nn.Module,
248
x: torch.Tensor,
249
bias: torch.Tensor | None = None,
250
+ swizzle: bool | None = None,
251
) -> torch.Tensor:
252
"""
253
Apply NVFP4 linear transformation using the specified backend.
@@ -220,6 +278,7 @@ def apply_nvfp4_linear(
278
weight=weight,
279
weight_scale_swizzled=weight_scale,
280
weight_global_scale=weight_global_scale,
281
+ swizzle=swizzle,
282
)
283
if bias is not None:
284
out = out + bias
vllm/platforms/rocm.py
+1
@@ -409,6 +409,7 @@ class RocmPlatform(Platform):
409
"mxfp4",
410
"torchao",
411
"bitsandbytes",
412
+ "modelopt_fp4",
413
]
414
415
@classmethod
vllm/utils/import_utils.py
+5
@@ -461,3 +461,8 @@ def has_aiter() -> bool:
461
def has_mori() -> bool:
462
"""Whether the optional `mori` package is available."""
463
return _has_module("mori")
464
+
465
+
466
+def has_fbgemm_gpu() -> bool:
467
+ """Whether the optional `fbgemm_gpu` package is available."""
468
+ return _has_module("fbgemm_gpu")