164 lines
5.4 KiB
Python
164 lines
5.4 KiB
Python
"""Unit test for the FA4 capability guard in FlexInference.__init__.
|
|
|
|
Runs on any GPU (and on CPU) because we monkey-patch
|
|
`torch.cuda.get_device_capability` and stub out the page-table / model
|
|
patching that the constructor does after the guard.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import types
|
|
import warnings
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
import torch
|
|
|
|
|
|
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
BENCH_DIR = os.path.join(REPO_ROOT, "scripts", "benchmarks")
|
|
if BENCH_DIR not in sys.path:
|
|
sys.path.insert(0, BENCH_DIR)
|
|
|
|
# qwen3_flex_inference imports heavy siblings (flex_paged_attention).
|
|
# Stub the PageTable and patch_qwen3_model the constructor calls after the
|
|
# guard so we don't need a real model / CUDA device.
|
|
import qwen3_flex_inference as qfi # noqa: E402
|
|
|
|
|
|
class _FakePageTable:
|
|
def __init__(self, *a, **kw):
|
|
pass
|
|
|
|
def create_causal_blockmask(self, *a, **kw):
|
|
return None
|
|
|
|
|
|
class _FakeTokenizer:
|
|
eos_token_id = 0
|
|
|
|
|
|
def _make_fake_model(device_str = "cpu"):
|
|
m = types.SimpleNamespace()
|
|
m.device = torch.device(device_str)
|
|
return m
|
|
|
|
|
|
def _build(fa4_prefill, cc_major, cc_minor = 0):
|
|
"""Construct a FlexInference with the guard exercised.
|
|
|
|
Returns the instance. Patches torch.cuda.get_device_capability,
|
|
torch.zeros (to avoid CUDA allocation), PageTable, and
|
|
patch_qwen3_model so __init__ can run to completion without a real
|
|
model.
|
|
"""
|
|
fake_model = _make_fake_model("cpu")
|
|
fake_tok = _FakeTokenizer()
|
|
|
|
_real_zeros = torch.zeros
|
|
|
|
def _fake_zeros(*a, **kw):
|
|
kw.pop("device", None)
|
|
return _real_zeros(*a, **kw)
|
|
|
|
with (
|
|
mock.patch.object(
|
|
torch.cuda, "get_device_capability", return_value = (cc_major, cc_minor)
|
|
),
|
|
mock.patch.object(qfi, "PageTable", _FakePageTable),
|
|
mock.patch.object(qfi, "patch_qwen3_model", lambda *a, **kw: None),
|
|
mock.patch.object(torch, "zeros", _fake_zeros),
|
|
):
|
|
return qfi.FlexInference(
|
|
model = fake_model,
|
|
tokenizer = fake_tok,
|
|
max_batch_size = 2,
|
|
max_seq_length = 128,
|
|
n_pages = 4,
|
|
page_size = 128,
|
|
max_new_tokens = 16,
|
|
fa4_prefill = fa4_prefill,
|
|
)
|
|
|
|
|
|
def _fa4_warnings(caught):
|
|
return [
|
|
w
|
|
for w in caught
|
|
if issubclass(w.category, RuntimeWarning) and "fa4_prefill" in str(w.message)
|
|
]
|
|
|
|
|
|
class TestFA4CapabilityGuard(unittest.TestCase):
|
|
# --- explicit opt-in: --fa4_prefill=True ---
|
|
def test_explicit_on_sub_hopper_disables_and_warns(self):
|
|
with warnings.catch_warnings(record = True) as caught:
|
|
warnings.simplefilter("always")
|
|
fi = _build(fa4_prefill = True, cc_major = 8)
|
|
self.assertTrue(
|
|
_fa4_warnings(caught),
|
|
f"expected RuntimeWarning about fa4_prefill, got {caught!r}",
|
|
)
|
|
self.assertIs(fi.fa4_prefill, False)
|
|
self.assertEqual(fi.prefill_q_block, 128)
|
|
self.assertNotIn("BACKEND", fi.prefill_kernel_options)
|
|
|
|
def _assert_fa4_enabled(self, cc_major, fa4_prefill):
|
|
with warnings.catch_warnings(record = True) as caught:
|
|
warnings.simplefilter("always")
|
|
fi = _build(fa4_prefill = fa4_prefill, cc_major = cc_major)
|
|
self.assertEqual(
|
|
_fa4_warnings(caught),
|
|
[],
|
|
f"unexpected fa4 RuntimeWarning on sm_{cc_major}0 "
|
|
f"with fa4_prefill={fa4_prefill}: {caught!r}",
|
|
)
|
|
self.assertIs(fi.fa4_prefill, True)
|
|
self.assertEqual(fi.prefill_q_block, 256)
|
|
self.assertEqual(fi.prefill_kernel_options.get("BACKEND"), "FLASH")
|
|
|
|
def test_explicit_on_hopper_enables(self):
|
|
self._assert_fa4_enabled(cc_major = 9, fa4_prefill = True)
|
|
|
|
def test_explicit_on_blackwell_sm100_enables(self):
|
|
self._assert_fa4_enabled(cc_major = 10, fa4_prefill = True)
|
|
|
|
def test_explicit_on_blackwell_sm120_enables(self):
|
|
self._assert_fa4_enabled(cc_major = 12, fa4_prefill = True)
|
|
|
|
# --- auto-detect: fa4_prefill is None ---
|
|
def test_auto_on_sub_hopper_disables_silently(self):
|
|
with warnings.catch_warnings(record = True) as caught:
|
|
warnings.simplefilter("always")
|
|
fi = _build(fa4_prefill = None, cc_major = 8)
|
|
self.assertEqual(
|
|
_fa4_warnings(caught),
|
|
[],
|
|
f"auto-detect must not warn on unsupported GPU: {caught!r}",
|
|
)
|
|
self.assertIs(fi.fa4_prefill, False)
|
|
self.assertEqual(fi.prefill_q_block, 128)
|
|
self.assertNotIn("BACKEND", fi.prefill_kernel_options)
|
|
|
|
def test_auto_on_hopper_enables(self):
|
|
self._assert_fa4_enabled(cc_major = 9, fa4_prefill = None)
|
|
|
|
def test_auto_on_blackwell_sm100_enables(self):
|
|
self._assert_fa4_enabled(cc_major = 10, fa4_prefill = None)
|
|
|
|
def test_auto_on_blackwell_sm120_enables(self):
|
|
self._assert_fa4_enabled(cc_major = 12, fa4_prefill = None)
|
|
|
|
# --- explicit opt-out: --no-fa4_prefill / fa4_prefill=False ---
|
|
def test_explicit_off_on_blackwell_stays_off(self):
|
|
with warnings.catch_warnings(record = True) as caught:
|
|
warnings.simplefilter("always")
|
|
fi = _build(fa4_prefill = False, cc_major = 10)
|
|
self.assertEqual(_fa4_warnings(caught), [])
|
|
self.assertIs(fi.fa4_prefill, False)
|
|
self.assertEqual(fi.prefill_q_block, 128)
|
|
self.assertNotIn("BACKEND", fi.prefill_kernel_options)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|