Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 16 additions & 25 deletions docs/design_docs/flashinfer_moe_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -824,8 +824,9 @@ Repro: `benchmarks/flashinfer_benchmark.py --routine {trtllm_fp4_block_scale_moe
> `MoETensors` cluster drop ("Two packs, two lifetimes" + the pack rationale in
> `api.py`). The Reviewer 14 `_select_winner` thread remains tracked under Post-MVP
> Carryover (a deferred design decision, not part of this pass). The *open* MoE
> work is now the two fuzzer-filed bugs β€” gh #3547 / #3548 β€” described under
> "Test Harness" above.
> work includes the remaining fuzzer-filed global-scale gap, gh #3548,
> described under "Test Harness" above. The earlier gh #3547 EP-offset bug is
> fixed and retained as regression coverage.


### Test Harness β€” Forward-Compatible Fuzzer (PR #6, merged 2026-06-09)
Expand Down Expand Up @@ -869,32 +870,22 @@ local + `local_expert_offset` β€” the real deployment shape, in scope for the
single-GPU harness; the EP *collective* is not), all under a weight-memory
budget so one config never hogs the GPU.

**Known-failure ledger** (`_KNOWN_FAILURES`): a filed-and-tracked bug is `xfail`ed
by `(backend_key, predicate)` β€” the case is **still run**, so the suite stays
green yet flags loudly (`xpass` β†’ "remove this entry") the day the bug is fixed.
A crash is never tolerated, only a wrong answer.

**CI-safety gate (waived, opt-in).** The ledger tolerates a *wrong answer* but
cannot absorb a *process abort*, and a single-process run of this suite on SM100
hit `CUDA error: device-side assert triggered` β†’ `Fatal Python error: Aborted`
(triage 2026-06-09) β€” which would block B200 CI. Per-config isolation passes
68/86 incl. EP `offset>0`, so the abort is **not** cleanly attributable to one
config (the #3547 EP case returns tolerated zeros under `synchronize`, no
assert); it surfaces only in the accumulated single-process run CI uses, and
`--forked` can't isolate it (CUDA inits at collection). So the suite is gated
behind `FLASHINFER_UMOE_FUZZ` (`pytestmark` skip): **unset (CI default) β†’
collected-and-skipped, launches no kernel, cannot abort the job**; set β†’ runs
(developer / nightly). The follow-up PR fixes #3547, root-causes the abort, and
removes the gate.
**Known-failure ledger** (shared `tests/test_helpers/fuzz_ledger.py`):
wrong-answer findings still run and report `xfail` after healthy backends;
crash-class findings are quarantined before launch. Unexpected passes fail
strictly so stale waivers are removed.

**CI gate (default-on).** The accumulated fuzzer runs by default and uses a
`shard_group` marker to keep all configurations in one pytest process.
`FLASHINFER_UMOE_FUZZ=0` remains an emergency waiver.

**Bugs this fuzzer found + filed** (the EP/scale regimes the prior suite never
exercised end-to-end):
- **gh #3547** β€” `trtllm_fp4_routed` returns all-zeros for EP shards
(`local_expert_offset > 0`): the offset is applied twice (pre-subtracted in
`pack_inputs` *and* forwarded to the kernel). `cute_dsl_nvfp4` is correct
(passes global ids + offset, kernel localizes once). Encoded as the current
`_KNOWN_FAILURES` entry. Fix = stop pre-subtracting (pass global ids), then
delete the ledger entry so the case flips to passing.
- **gh #3547 (fixed)** β€” `trtllm_fp4_routed` returned all-zeros for EP shards
(`local_expert_offset > 0`) because the offset was applied twice
(pre-subtracted in `pack_inputs` and forwarded to the kernel). The fix passes
global IDs and lets the kernel localize once. Its ledger entry has been
removed; the EP-offset cases remain as regression coverage.
- **gh #3548** β€” activation **global-scale** gap: `prepare_*_weights` hardcodes
`gs=1.0`/`fc2_input_scale=1.0`/`alpha=ones` and `MoEActivationPack` has no
global-scale field, so calibrated-checkpoint scales are silently dropped
Expand Down
2 changes: 2 additions & 0 deletions flashinfer/fused_moe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
CutlassBf16Runner,
CutlassW4A16Runner,
CuteDslNvfp4Runner,
TrtllmBf16RoutedRunner,
TrtllmFp4RoutedRunner,
TrtllmFp8BlockRunner,
TrtllmFp8PerTensorRunner,
Expand Down Expand Up @@ -150,6 +151,7 @@
"MoEConfig",
"MoELayer",
"MoEWeightPack",
"TrtllmBf16RoutedRunner",
"TrtllmFp4RoutedRunner",
"TrtllmFp8BlockRunner",
"TrtllmFp8PerTensorRunner",
Expand Down
29 changes: 23 additions & 6 deletions tests/gemm/test_bmm_bf16.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# NOTE for future contributors (incl. AI agents): keep this file a SMALL curated
# smoke set. New coverage (shapes, dtypes, backends, randomized breadth) belongs in
# tests/gemm/test_unified_gemm_fuzz.py -- extend an adapter/axis there. Add cases
# here only as deliberate regression anchors or for paths the fuzzer cannot express.

import pytest
import torch
import torch.nn.functional as F
Expand All @@ -12,12 +17,24 @@
from flashinfer.utils import get_compute_capability


@pytest.mark.parametrize("b", [1, 16])
@pytest.mark.parametrize("m", [48, 128])
@pytest.mark.parametrize("n", [80, 64])
@pytest.mark.parametrize("k", [64, 256])
@pytest.mark.parametrize("res_dtype", [torch.bfloat16, torch.float16, torch.float32])
@pytest.mark.parametrize("backend", ["cutlass", "cudnn", "cutile", "tgv", "auto"])
# Curated smoke set. Randomized breadth over {b,m,n,k} x backends (tight elementwise
# oracle, determinism, autotune-winner validation) lives in
# tests/gemm/test_unified_gemm_fuzz.py's bmm_bf16 adapter; keep one deliberate case
# per backend with each out dtype covered at least once.
_SMOKE_CASES = [
# b, m, n, k, res_dtype, backend
(16, 48, 80, 256, torch.bfloat16, "cutlass"),
(1, 128, 64, 64, torch.float32, "cutlass"),
(16, 128, 64, 256, torch.float16, "cudnn"),
(1, 48, 80, 64, torch.bfloat16, "cudnn"),
(16, 48, 64, 64, torch.bfloat16, "cutile"),
(1, 128, 80, 256, torch.bfloat16, "tgv"),
(16, 128, 80, 64, torch.bfloat16, "auto"),
(1, 48, 64, 256, torch.float32, "auto"),
]


@pytest.mark.parametrize("b,m,n,k,res_dtype,backend", _SMOKE_CASES)
def test_bmm_bf16(b, m, n, k, res_dtype, backend):
compute_capability = get_compute_capability(torch.device(device="cuda"))
compute_capability_number = compute_capability[0] * 10 + compute_capability[1]
Expand Down
92 changes: 83 additions & 9 deletions tests/gemm/test_bmm_fp8.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# NOTE for future contributors (incl. AI agents): keep this file a SMALL curated
# smoke set. New coverage (shapes, dtypes, backends, randomized breadth) belongs in
# tests/gemm/test_unified_gemm_fuzz.py -- extend an adapter/axis there. Add cases
# here only as deliberate regression anchors or for paths the fuzzer cannot express.

import pytest
import torch
import torch.nn.functional as F
Expand All @@ -7,15 +12,84 @@
from tests.utils_fp8 import to_float8


@pytest.mark.parametrize("b", [1, 16])
@pytest.mark.parametrize("m", [1, 48, 128])
@pytest.mark.parametrize("n", [64, 80, 10304])
@pytest.mark.parametrize("k", [64, 256, 2688])
@pytest.mark.parametrize("input_dtype", [torch.float8_e4m3fn, torch.float8_e5m2])
@pytest.mark.parametrize("mat2_dtype", [torch.float8_e4m3fn, torch.float8_e5m2])
@pytest.mark.parametrize("res_dtype", [torch.bfloat16, torch.float16])
@pytest.mark.parametrize("backend", ["cudnn", "cublas", "cutlass", "auto"])
@pytest.mark.parametrize("auto_tuning", [True, False])
# Curated smoke set. Randomized breadth over {b,m,n,k} x {e4m3,e5m2} x backends
# (with a tight elementwise oracle, determinism and autotune-winner checks) lives in
# tests/gemm/test_unified_gemm_fuzz.py's bmm_fp8 adapter; this file keeps one
# deliberate case per backend / dtype-mix / autotune mode for fast bisection.
_SMOKE_CASES = [
# b, m, n, k, input_dtype, mat2_dtype, res_dtype, backend, auto_tuning
(
16,
48,
80,
256,
torch.float8_e4m3fn,
torch.float8_e4m3fn,
torch.bfloat16,
"cudnn",
True,
),
(
1,
128,
10304,
2688,
torch.float8_e4m3fn,
torch.float8_e4m3fn,
torch.float16,
"cublas",
False,
),
(
16,
1,
64,
2688,
torch.float8_e5m2,
torch.float8_e4m3fn,
torch.bfloat16,
"cudnn",
False,
),
(
1,
48,
80,
64,
torch.float8_e4m3fn,
torch.float8_e5m2,
torch.float16,
"cublas",
True,
),
(
16,
128,
80,
256,
torch.float8_e4m3fn,
torch.float8_e4m3fn,
torch.bfloat16,
"cutlass",
True,
),
(
1,
1,
10304,
256,
torch.float8_e4m3fn,
torch.float8_e4m3fn,
torch.bfloat16,
"auto",
False,
),
]


@pytest.mark.parametrize(
"b,m,n,k,input_dtype,mat2_dtype,res_dtype,backend,auto_tuning", _SMOKE_CASES
)
def test_bmm_fp8(b, m, n, k, input_dtype, mat2_dtype, res_dtype, backend, auto_tuning):
compute_capability = get_compute_capability(torch.device("cuda"))
if backend == "cutlass" and compute_capability[0] not in [10, 11, 12]:
Expand Down
43 changes: 31 additions & 12 deletions tests/gemm/test_bmm_mxfp8.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# NOTE for future contributors (incl. AI agents): keep this file a SMALL curated
# smoke set. New coverage (shapes, dtypes, backends, randomized breadth) belongs in
# tests/gemm/test_unified_gemm_fuzz.py -- extend an adapter/axis there. Add cases
# here only as deliberate regression anchors or for paths the fuzzer cannot express.

import warnings

import pytest
Expand All @@ -10,18 +15,27 @@
from flashinfer.utils import get_compute_capability


@pytest.mark.parametrize("b", [1, 16])
@pytest.mark.parametrize("m", [128, 256, 512])
@pytest.mark.parametrize("n", [128, 256, 512])
@pytest.mark.parametrize("k", [128, 256, 512, 1024])
@pytest.mark.parametrize("input_dtype", [torch.bfloat16])
@pytest.mark.parametrize("is_sf_swizzled_layout", [True, False])
@pytest.mark.parametrize("res_dtype", [torch.bfloat16])
@pytest.mark.parametrize("backend", ["cudnn", "cutlass"])
@pytest.mark.parametrize("auto_tuning", [True, False])
def test_bmm_mxfp8(
b, m, n, k, input_dtype, is_sf_swizzled_layout, res_dtype, backend, auto_tuning
):
# Curated smoke set. Randomized breadth over {b,m,n,k} x backend (swizzled scales,
# tight elementwise oracle, determinism, autotune-winner validation, and the tracked
# #3604 b>1/M%128 ledger) lives in tests/gemm/test_unified_gemm_fuzz.py's bmm_mxfp8
# adapter; keep autotune on/off per backend. Linear scale metadata cannot currently
# be represented by the public 3D BMM API, so every executable row uses swizzled scales.
_SMOKE_CASES = [
# b, m, n, k, is_sf_swizzled_layout, backend, auto_tuning
(16, 128, 256, 1024, True, "cudnn", True),
(1, 512, 128, 256, True, "cudnn", False),
(16, 256, 512, 128, True, "cudnn", True),
(16, 128, 128, 512, True, "cutlass", False),
(1, 256, 512, 1024, True, "cutlass", True),
]


@pytest.mark.parametrize(
"b,m,n,k,is_sf_swizzled_layout,backend,auto_tuning", _SMOKE_CASES
)
def test_bmm_mxfp8(b, m, n, k, is_sf_swizzled_layout, backend, auto_tuning):
input_dtype = torch.bfloat16
res_dtype = torch.bfloat16
compute_capability = get_compute_capability(torch.device("cuda"))
if backend == "cudnn" and compute_capability[0] != 10:
pytest.skip("bmm_mxfp8 cudnn backend requires SM10x.")
Expand All @@ -39,6 +53,11 @@ def test_bmm_mxfp8(
# Block size is 32 in MXFP8
assert input_mxfp8.numel() == (input_scale.numel() * 32)

# The cuDNN override-shape path requires B as a COLUMN-major [b, k, n] VIEW
# (K contiguous): quantize the contiguous [b, n, k] weight and pass the
# transpose of the quantized tensor. A .contiguous() on the transpose flips
# it back to row-major and is rejected once cudnn-frontend >= 1.24 enables
# the override path (CI's older frontend hid this).
weight = torch.randn([b, n, k], device="cuda", dtype=input_dtype)
weight_mxfp8, weight_scale = mxfp8_quantize(weight, is_sf_swizzled_layout)
mat2_mxfp8 = weight_mxfp8.transpose(-2, -1)
Expand Down
39 changes: 30 additions & 9 deletions tests/gemm/test_mm_bf16.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# NOTE for future contributors (incl. AI agents): keep this file a SMALL curated
# smoke set. New coverage (shapes, dtypes, backends, randomized breadth) belongs in
# tests/gemm/test_unified_gemm_fuzz.py -- extend an adapter/axis there. Add cases
# here only as deliberate regression anchors or for paths the fuzzer cannot express.

import pytest
import torch
import torch.nn.functional as F
Expand All @@ -8,17 +13,33 @@
from flashinfer.utils import get_compute_capability, is_sm100a_supported


@pytest.mark.parametrize("m", [1, 8, 16, 25, 32, 64])
@pytest.mark.parametrize("n", [1024, 2048, 4096])
@pytest.mark.parametrize("k", [1024, 2048, 3072])
@pytest.mark.parametrize("res_dtype", [torch.bfloat16, torch.float16, torch.float32])
@pytest.mark.parametrize("enable_bias", [True, False])
@pytest.mark.parametrize("pdl", [True, False])
# Curated smoke set. Randomized breadth over {m,n,k} x out-dtype x backend (tight
# elementwise oracle, determinism, autotune-winner validation) lives in
# tests/gemm/test_unified_gemm_fuzz.py's mm_bf16 adapter. The bias and pdl epilogue
# axes are NOT fuzzed -- each supported backend keeps a bias=True and pdl=True case
# here; plus one plain case per backend / out dtype / autotune mode.
_SMOKE_CASES = [
# m, n, k, res_dtype, enable_bias, pdl, backend, auto_tuning
(1, 1024, 2048, torch.bfloat16, True, False, "cudnn", False),
(16, 4096, 1024, torch.float16, False, True, "cudnn", True),
(64, 2048, 3072, torch.float32, True, True, "cudnn", False),
(8, 1024, 1024, torch.bfloat16, False, False, "cutlass", True),
(32, 4096, 3072, torch.float32, False, False, "cutlass", False),
(1, 2048, 1024, torch.bfloat16, True, True, "tgv", False),
(64, 1024, 2048, torch.bfloat16, False, True, "tgv", True),
(16, 2048, 2048, torch.bfloat16, False, False, "cublaslt", True),
(8, 4096, 3072, torch.float16, False, False, "cublaslt", False),
(1, 1024, 3072, torch.bfloat16, True, False, "tinygemm", False),
(32, 1024, 1024, torch.bfloat16, False, False, "cutile", False),
(25, 2048, 1024, torch.bfloat16, False, False, "cute-dsl", False),
(64, 4096, 2048, torch.bfloat16, False, False, "auto", True),
(1, 2048, 3072, torch.float16, False, False, "auto", False),
]


@pytest.mark.parametrize(
"backend",
["cudnn", "cutlass", "tgv", "cublaslt", "tinygemm", "cutile", "cute-dsl", "auto"],
"m,n,k,res_dtype,enable_bias,pdl,backend,auto_tuning", _SMOKE_CASES
)
@pytest.mark.parametrize("auto_tuning", [False, True])
def test_mm_bf16(
m: int,
n: int,
Expand Down
Loading
Loading