Skip to content

Commit cb3ef64

Browse files
committed
[KMCompiler][Test][Benchmark] Fix fp8e4nv gate, memory guard and vendor op discovery
Three bugs in the test and benchmark for fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert, each of which makes them report something untrue. Scoped to this operator's own files. 1. is_support_fp8e4nv() gated on get_device_capability() >= (8, 9). That threshold means "Ada or newer" on NVIDIA only; other vendors report their own major/minor on a different scale, so it is a false negative that skips this whole file on hardware that supports the dtype. MetaX C550 reports (8, 0) and converts fp8e4nv bit-identically to torch. The check now consults an explicit vendor list before falling back to the NVIDIA capability rule. Four other files carry the same local copy of this check and have the same bug; they are out of scope here and left untouched. 2. The memory guard excluded shapes via a list hardcoded for an 80GB H800 -- which still OOMs on 64GB cards -- and skipped them with a bare return, so they were counted as passed. Replaced with a budget measured against free device memory, skipping explicitly. It collects before measuring and counts the allocator's cached blocks as available: releasing them with empty_cache() instead makes a marginal allocation fail that otherwise succeeds. 3. The benchmark probed torch.ops._C for its reference without importing the library that registers it, and torch.ops._C gives no hint that nothing did. Where the reference IS installed the benchmark reported it missing and skipped, so no comparison ran. The provider is not always vLLM's own: on MetaX it is mcoplib._C, while the vllm wheel there fails to load at all. Importing the top-level package is not enough; the compiled submodule must be imported before the schemas register.
1 parent a841b2e commit cb3ef64

2 files changed

Lines changed: 181 additions & 33 deletions

File tree

benchmark/test_fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# limitations under the License.
1414

1515
import dataclasses
16+
import importlib
17+
import logging
1618
import random
1719

1820
import pytest
@@ -23,15 +25,67 @@
2325

2426
from . import base
2527

28+
logger = logging.getLogger(__name__)
29+
30+
31+
# NVIDIA gates FP8 E4M3 on sm_89+, and this check has historically been spelled
32+
# `get_device_capability() >= (8, 9)`. That number only means "Ada or newer" on
33+
# NVIDIA; other vendors report their own major/minor on a different scale, so
34+
# applying the threshold to them is a false negative that skips this whole file
35+
# on hardware that supports the dtype. Add a vendor here only after verifying
36+
# on it that a Triton `tl.float8e4nv` conversion matches `torch.float8_e4m3fn`
37+
# bit-for-bit -- MetaX C550 reports (8, 0) and does (verified 2026-08-05).
38+
_FP8E4NV_CAPABLE_VENDORS = frozenset({"metax"})
39+
2640

2741
def is_support_fp8e4nv():
42+
if not hasattr(torch, "float8_e4m3fn"):
43+
return False
44+
if flag_gems.vendor_name in _FP8E4NV_CAPABLE_VENDORS:
45+
return True
2846
major, minor = get_device_capability()
2947
return major * 10 + minor >= 89
3048

3149

32-
VLLM_REF_AVAILABLE = hasattr(
33-
torch.ops._C, "fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert"
34-
)
50+
# The reference is registered under `torch.ops._C` by whichever compiled op
51+
# library the platform ships, and probing that namespace does not import it --
52+
# so where the reference IS installed, a bare hasattr() reports it missing and
53+
# no comparison runs. The provider is not always vLLM's own: on MetaX it is
54+
# `mcoplib._C`, while the `vllm` wheel there fails to load. Note importing the
55+
# top-level package is not enough; the compiled submodule must be imported
56+
# before the schemas register.
57+
VENDOR_OP_LIBS = ("vllm._C", "mcoplib._C")
58+
59+
60+
def _load_vendor_ref(op_name):
61+
"""Return `torch.ops._C.<op_name>`, importing vendor libraries as needed.
62+
63+
Returns None if no library provides it. Import failures are logged rather
64+
than swallowed -- a silent `except: pass` is what makes a missing baseline
65+
indistinguishable from an unimportable one.
66+
"""
67+
fn = getattr(torch.ops._C, op_name, None)
68+
if callable(fn):
69+
return fn
70+
for lib in VENDOR_OP_LIBS:
71+
try:
72+
importlib.import_module(lib)
73+
except Exception as e:
74+
logger.info("vendor op library %s unavailable: %s", lib, e)
75+
continue
76+
fn = getattr(torch.ops._C, op_name, None)
77+
if callable(fn):
78+
logger.info("found %s in %s", op_name, lib)
79+
return fn
80+
logger.info("%s loaded but does not provide %s", lib, op_name)
81+
logger.info(
82+
"no vendor kernel for %s (tried %s)", op_name, ", ".join(VENDOR_OP_LIBS)
83+
)
84+
return None
85+
86+
87+
_VENDOR_REF = _load_vendor_ref("fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert")
88+
VLLM_REF_AVAILABLE = _VENDOR_REF is not None
3589
HEAD_DIM = 512
3690
ROPE_DIM = 64
3791
HEAD_BYTES = 584
@@ -59,7 +113,7 @@ class FusedDeepseekV4QnormRopeKVRopeQuantInsertBenchmark(base.Benchmark):
59113
def __init__(self):
60114
super().__init__(
61115
"fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert",
62-
torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert,
116+
_VENDOR_REF,
63117
[torch.bfloat16],
64118
)
65119
self.set_gems(flag_gems.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert)
@@ -170,11 +224,11 @@ def make_input(param: TestParam):
170224

171225
@pytest.mark.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert
172226
@pytest.mark.skipif(
173-
not VLLM_REF_AVAILABLE, reason="The referenced vLLM implementation is not installed"
174-
)
175-
@pytest.mark.skipif(
176-
not is_support_fp8e4nv(), reason="Do not support fp8e4nv when capability < 89"
227+
not VLLM_REF_AVAILABLE,
228+
reason="No vendor kernel found for this operator (tried %s)"
229+
% ", ".join(VENDOR_OP_LIBS),
177230
)
231+
@pytest.mark.skipif(not is_support_fp8e4nv(), reason="Device does not support fp8e4nv")
178232
def test_fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert():
179233
bench = FusedDeepseekV4QnormRopeKVRopeQuantInsertBenchmark()
180234
bench.run()

tests/test_fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert.py

Lines changed: 119 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,34 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import gc
16+
1517
import pytest
1618
import torch
1719

1820
import flag_gems
21+
from flag_gems.fused.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert import (
22+
fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert as _generic_impl,
23+
)
1924
from flag_gems.utils.device_info import get_device_capability
2025

2126
from .conftest import QUICK_MODE
2227

28+
# NVIDIA gates FP8 E4M3 on sm_89+, and this check has historically been spelled
29+
# `get_device_capability() >= (8, 9)`. That number only means "Ada or newer" on
30+
# NVIDIA; other vendors report their own major/minor on a different scale, so
31+
# applying the threshold to them is a false negative that skips this whole file
32+
# on hardware that supports the dtype. Add a vendor here only after verifying
33+
# on it that a Triton `tl.float8e4nv` conversion matches `torch.float8_e4m3fn`
34+
# bit-for-bit -- MetaX C550 reports (8, 0) and does (verified 2026-08-05).
35+
_FP8E4NV_CAPABLE_VENDORS = frozenset({"metax"})
36+
2337

2438
def is_support_fp8e4nv():
39+
if not hasattr(torch, "float8_e4m3fn"):
40+
return False
41+
if flag_gems.vendor_name in _FP8E4NV_CAPABLE_VENDORS:
42+
return True
2543
major, minor = get_device_capability()
2644
return major * 10 + minor >= 89
2745

@@ -39,6 +57,71 @@ def is_support_fp8e4nv():
3957
SCALE_BYTES_PER_TOKEN = NUM_QUANT_BLOCKS + 1 # 8
4058
HEAD_BYTES = TOKEN_DATA_BYTES + SCALE_BYTES_PER_TOKEN # 584
4159

60+
# Bytes of device memory the fp32 reference needs per element of
61+
# [num_tokens, n_heads, HEAD_DIM]. The op itself is cheap; the reference is what
62+
# blows up, because it upcasts q to fp32: q (bf16, 2B) + q_ref (bf16, 2B) + the
63+
# fp32 upcast (4B) + the fp32 result (4B) = 12 B/elem live at once, plus
64+
# allocator headroom. Two observations bracket the real figure:
65+
# > 13.2 -- 98304 x 128 OOMs on an 80GB H800 (the shape the old hardcoded
66+
# exclusion list was written for)
67+
# <=15.5 -- 65536 x 128, an identical element count, passes on a 63.59GB
68+
# MetaX C550
69+
REF_BYTES_PER_ELEM = 14
70+
71+
72+
def _free_device_memory():
73+
"""Free device bytes, or None if the backend cannot report it.
74+
75+
Two corrections matter, and both were found by getting them wrong on a
76+
MetaX C550:
77+
78+
1. Collect before measuring. The previous parametrized case's tensors are
79+
still reachable from its frame, so without this a 64GB card reports
80+
~38GB free and the guard skips shapes that in fact pass.
81+
2. Count PyTorch's cached-but-unused blocks as available instead of calling
82+
`empty_cache` to release them. Releasing hands them back to the driver,
83+
and a marginal allocation then has to be re-served from possibly
84+
fragmented driver memory rather than reusing blocks already in hand --
85+
which is enough to turn 65536 x 128, a shape that otherwise passes,
86+
into an OutOfMemoryError.
87+
"""
88+
try:
89+
gc.collect() # drop the previous case's tensors before measuring
90+
driver_free = torch.cuda.mem_get_info()[0]
91+
cached = torch.cuda.memory_reserved() - torch.cuda.memory_allocated()
92+
return driver_free + cached
93+
except Exception:
94+
pass
95+
try:
96+
return torch.cuda.get_device_properties(0).total_memory
97+
except Exception:
98+
return None
99+
100+
101+
def _skip_if_reference_wont_fit(num_tokens: int, n_heads: int):
102+
"""Skip shapes whose fp32 reference exceeds device memory.
103+
104+
Replaces a hardcoded exclusion list tuned for an 80GB H800, which still
105+
OOMs on 64GB cards -- MetaX C550 died at num_tokens=131072, n_heads=64,
106+
inside the reference and before the op was ever called. At
107+
REF_BYTES_PER_ELEM the rule reproduces the old H800 exclusion list exactly
108+
on an 80GB card, and on a 64GB C550 leaves 131072 x 64 to the
109+
OutOfMemoryError handler below rather than pre-skipping it.
110+
111+
This is a cheap pre-check that avoids allocating tens of GiB only to fail.
112+
It is deliberately not the whole story: whether a marginal shape fits also
113+
depends on allocator fragmentation (65536 x 128 passes on a C550 while
114+
131072 x 64, an identical element count, does not), so the reference call
115+
itself also catches OutOfMemoryError.
116+
"""
117+
needed = num_tokens * n_heads * HEAD_DIM * REF_BYTES_PER_ELEM
118+
free = _free_device_memory()
119+
if free is not None and needed > free:
120+
pytest.skip(
121+
f"reference needs ~{needed / 2**30:.0f}GiB for num_tokens={num_tokens}, "
122+
f"n_heads={n_heads}; only {free / 2**30:.0f}GiB free"
123+
)
124+
42125

43126
# ─── pytorch reference implementation from vllm ───
44127

@@ -266,9 +349,7 @@ def fused_impl(q, kv, k_cache, slot_mapping, positions, cos_sin_cache, eps, bs):
266349

267350

268351
@pytest.mark.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert
269-
@pytest.mark.skipif(
270-
not is_support_fp8e4nv(), reason="Do not support fp8e4nv when capability < 89"
271-
)
352+
@pytest.mark.skipif(not is_support_fp8e4nv(), reason="Device does not support fp8e4nv")
272353
@pytest.mark.parametrize("num_tokens", [1, 4, 17, 64])
273354
@pytest.mark.parametrize("n_heads", [8, 64])
274355
def test_q_path_matches_reference(num_tokens: int, n_heads: int):
@@ -326,9 +407,7 @@ def _ue8m0_per_block_scales(kv_roped_nope_f32: torch.Tensor, qblock: int):
326407

327408

328409
@pytest.mark.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert
329-
@pytest.mark.skipif(
330-
not is_support_fp8e4nv(), reason="Do not support fp8e4nv when capability < 89"
331-
)
410+
@pytest.mark.skipif(not is_support_fp8e4nv(), reason="Device does not support fp8e4nv")
332411
@pytest.mark.parametrize("num_tokens", [1, 4, 17, 64])
333412
@pytest.mark.parametrize("block_size", [16, 64])
334413
def test_kv_path_matches_reference(num_tokens: int, block_size: int):
@@ -374,9 +453,7 @@ def test_kv_path_matches_reference(num_tokens: int, block_size: int):
374453

375454

376455
@pytest.mark.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert
377-
@pytest.mark.skipif(
378-
not is_support_fp8e4nv(), reason="Do not support fp8e4nv when capability < 89"
379-
)
456+
@pytest.mark.skipif(not is_support_fp8e4nv(), reason="Device does not support fp8e4nv")
380457
@pytest.mark.parametrize("num_tokens", [4, 17])
381458
@pytest.mark.parametrize("pad", [1, 5])
382459
@pytest.mark.parametrize("block_size", [16, 64])
@@ -426,19 +503,15 @@ def test_kv_path_with_dp_padding(num_tokens: int, pad: int, block_size: int):
426503

427504

428505
@pytest.mark.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert
429-
@pytest.mark.skipif(
430-
not is_support_fp8e4nv(), reason="Do not support fp8e4nv when capability < 89"
431-
)
506+
@pytest.mark.skipif(not is_support_fp8e4nv(), reason="Device does not support fp8e4nv")
432507
@pytest.mark.parametrize(
433508
"num_tokens",
434509
[1, 4, 17, 64] if QUICK_MODE else [1, 4, 17, 64, 8192, 32768, 65536, 98304, 131072],
435510
)
436511
@pytest.mark.parametrize("n_heads", [64, 128])
437512
@pytest.mark.parametrize("block_size", [16, 64])
438513
def test_combined_q_and_kv(num_tokens: int, n_heads: int, block_size: int):
439-
# out of memory for huge shape on H800
440-
if (num_tokens == 98304 or num_tokens == 131072) and n_heads == 128:
441-
return
514+
_skip_if_reference_wont_fit(num_tokens, n_heads)
442515

443516
torch.manual_seed(2)
444517
device = "cuda"
@@ -462,16 +535,37 @@ def test_combined_q_and_kv(num_tokens: int, n_heads: int, block_size: int):
462535
cos_sin_cache_ref = cos_sin_cache.clone()
463536
slot_mapping_ref = slot_mapping.clone()
464537

465-
ref_impl(
466-
q_ref,
467-
kv_ref,
468-
k_cache_ref,
469-
slot_mapping_ref,
470-
positions_ref,
471-
cos_sin_cache_ref,
472-
eps,
473-
block_size,
474-
)
538+
try:
539+
ref_impl(
540+
q_ref,
541+
kv_ref,
542+
k_cache_ref,
543+
slot_mapping_ref,
544+
positions_ref,
545+
cos_sin_cache_ref,
546+
eps,
547+
block_size,
548+
)
549+
except torch.OutOfMemoryError:
550+
# The reference is scaffolding, not the code under test, so its running
551+
# out of room says nothing about the op -- skip rather than fail.
552+
#
553+
# Dropping the tensors here is what keeps this from cascading. A failed
554+
# case's traceback is retained by pytest, the traceback references this
555+
# frame, and the frame keeps q/q_ref/... alive for the rest of the
556+
# session: one OOM at num_tokens=131072 pinned 32GiB on a 64GB C550 and
557+
# took out an unrelated case 20 tests later. Rebinding to None empties
558+
# the frame slots before the exception is raised, so nothing is held.
559+
# (`del` would do the same, but leaves the names undefined for the rest
560+
# of the scope as far as static analysis is concerned.)
561+
q = kv = k_cache = q_ref = kv_ref = k_cache_ref = None
562+
positions = positions_ref = cos_sin_cache = cos_sin_cache_ref = None
563+
slot_mapping = slot_mapping_ref = None
564+
torch.cuda.empty_cache()
565+
pytest.skip(
566+
f"reference ran out of memory at num_tokens={num_tokens}, "
567+
f"n_heads={n_heads}"
568+
)
475569
fused_impl(q, kv, k_cache, slot_mapping, positions, cos_sin_cache, eps, block_size)
476570

477571
torch.testing.assert_close(q, q_ref, rtol=1e-2, atol=1e-2)

0 commit comments

Comments
 (0)