Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
287c1d1
Add FlyDSL persistent work-stealing MXFP8 grouped GEMM (gfx950)
aris134 Sep 29, 2026
e4fc6bd
Add FlyDSL grouped MXFP8 dgrad (NN) backward path
aris134 Sep 30, 2026
10f48f8
Add FlyDSL grouped MXFP8 wgrad (NT) via uniform padding
aris134 Sep 30, 2026
6ca17d5
Generalize FlyDSL grouped MXFP8 GEMM to BF16 and tensor-wise FP8
aris134 Oct 14, 2026
9ad67e5
Merge remote-tracking branch 'origin/dev' into amartin/flydsl-mxfp8-g…
aris134 Oct 14, 2026
ddee2cf
Cache grouped-GEMM descriptor queue, drop per-call D2H sync
aris134 Oct 14, 2026
d5e00fd
add K-bucketing for bf16 wgrad grouped gemm
aris134 Oct 14, 2026
f394a18
Apply K-bucket wgrad to MXFP8 and FP8 grouped GEMM
aris134 Oct 14, 2026
14d85a4
Address grouped GEMM weights via per-expert pointers, dropping the ho…
aris134 Oct 15, 2026
4d5d448
Enable FP16 grouped GEMM, add a FlyDSL dispatch table, and wire a tes…
aris134 Oct 15, 2026
0dee39c
Cut FlyDSL grouped-GEMM host overhead across all dtypes
aris134 Oct 16, 2026
3112c12
Hoist _SpanCAI
aris134 Oct 17, 2026
d290583
Write FlyDSL FP8 grouped wgrad output in place, skip the copy-out loop
aris134 Oct 18, 2026
35711ab
Merge origin/dev into amartin/flydsl-mxfp8-group-gemm
aris134 Oct 19, 2026
a710a9a
Merge remote-tracking branch 'origin/dev' into amartin/flydsl-mxfp8-g…
aris134 Oct 20, 2026
071d251
Read FlyDSL FP8 grouped operands and scales in place via per-expert p…
aris134 Oct 20, 2026
0c783e4
Share per-expert in-place operand reads across all FlyDSL grouped dtypes
aris134 Oct 21, 2026
d14a87f
Make FlyDSL grouped GEMM dispatch non-blocking and cheaper on the host
aris134 Oct 21, 2026
bcab563
remove redundant checks, clean up some code comments
aris134 Oct 21, 2026
ff139a4
address bot comments
aris134 Oct 21, 2026
6635016
address bot comments
aris134 Oct 21, 2026
7f3d0ee
Merge remote-tracking branch 'origin/dev' into amartin/flydsl-mxfp8-g…
aris134 Oct 21, 2026
e169938
address bot comments
aris134 Oct 21, 2026
9efa0d8
remove formatting-only changes from quantization.py
aris134 Oct 21, 2026
2114ec4
Replace deprecated fx.Index with fx.Int64 in FlyDSL GEMM kernels
aris134 Oct 21, 2026
954519c
Merge remote-tracking branch 'origin/dev' into amartin/flydsl-mxfp8-g…
aris134 Oct 21, 2026
5312860
Merge remote-tracking branch 'origin/dev' into amartin/flydsl-mxfp8-g…
aris134 Oct 27, 2026
87b343c
address reviewer comments
aris134 Oct 28, 2026
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
187 changes: 182 additions & 5 deletions tests/pytorch/test_grouped_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ def _test_grouped_linear_accuracy(
split_size = 1
if fp8:
split_size = get_align_size_for_quantization(recipe)
elif os.environ.get("NVTE_USE_GROUPED_GEMM_FLYDSL", "0") == "1":
# The FlyDSL grouped GEMM tiles every expert with BLOCK_M=256, so even the
# unquantized path needs 256-aligned per-expert token counts.
split_size = 256
m = config.max_seqlen_q // split_size
dist = torch.sort(torch.randint(0, m, (num_gemms - 2,))).values.tolist()
dist.append(dist[-1]) # Manually add a zero
Expand Down Expand Up @@ -598,9 +602,9 @@ def test_grouped_linear_accuracy_cutlass(
)


# ROCm grouped GEMM backends (HipKittens / CK). Kept separate from the CUTLASS test above so the
# upstream one stays untouched. The backend is picked by env var; use_cutlass only relaxes the
# tolerances, since a non-reference GEMM will not be bit-exact against the sequential Linear.
# ROCm grouped GEMM backends (HipKittens / CK / FlyDSL). Kept separate from the CUTLASS test above
# so the upstream one stays untouched. The backend is picked by env var; use_cutlass only relaxes
# the tolerances, since a non-reference GEMM will not be bit-exact against the sequential Linear.
@pytest.mark.skipif(not IS_HIP_EXTENSION, reason="ROCm-only grouped GEMM backends")
@pytest.mark.parametrize("dtype", param_types, ids=str)
@pytest.mark.parametrize("num_gemms", [3, 6])
Expand All @@ -610,7 +614,7 @@ def test_grouped_linear_accuracy_cutlass(
@pytest.mark.parametrize("fp8_model_params", all_boolean)
@pytest.mark.parametrize("fuse_wgrad_accumulation", all_boolean)
@pytest.mark.parametrize("delay_wgrad_compute", all_boolean)
@pytest.mark.parametrize("grouped_gemm_backend", ["hipkittens", "ck"], ids=str)
@pytest.mark.parametrize("grouped_gemm_backend", ["hipkittens", "ck", "flydsl"], ids=str)
Comment thread
aris134 marked this conversation as resolved.
def test_grouped_linear_accuracy_rocm_backends(
dtype,
num_gemms,
Expand Down Expand Up @@ -638,13 +642,32 @@ def test_grouped_linear_accuracy_rocm_backends(
# Triton backends in test_grouped_linear_accuracy instead.
pytest.skip("HipKittens/CK grouped GEMM backends do not support FP8 block scaling.")

if grouped_gemm_backend == "flydsl":
# FlyDSL grouped GEMM is gfx950-only and covers MXFP8, tensor-wise FP8
# (current/delayed scaling), and unquantized bf16/fp16. Skip configs its gate
# rejects, which would silently fall back to the default backend and give
# false "flydsl verified" coverage.
if get_device_compute_capability() != (9, 5):
pytest.skip("FlyDSL grouped GEMM is only supported on gfx950.")
if recipe is None and dtype not in (torch.bfloat16, torch.float16):
pytest.skip("FlyDSL unquantized grouped GEMM supports only bf16/fp16.")
if recipe is not None and not (
recipe.mxfp8() or recipe.float8_current_scaling() or recipe.delayed()
):
pytest.skip("FlyDSL grouped GEMM supports MXFP8 and tensor-wise FP8 recipes only.")

monkeypatch.setenv("NVTE_USE_CUTLASS_GROUPED_GEMM", "1")
monkeypatch.delenv("NVTE_USE_HIPKITTENS_GROUPED_GEMM", raising=False)
monkeypatch.delenv("NVTE_USE_CK_GROUPED_GEMM", raising=False)
monkeypatch.delenv("NVTE_USE_GROUPED_GEMM_FLYDSL", raising=False)
if grouped_gemm_backend == "hipkittens":
monkeypatch.setenv("NVTE_USE_HIPKITTENS_GROUPED_GEMM", "1")
else:
elif grouped_gemm_backend == "ck":
monkeypatch.setenv("NVTE_USE_CK_GROUPED_GEMM", "1")
else:
# FlyDSL has its own opt-in; keep the cutlass/HK/CK grouped GEMM path disabled.
monkeypatch.delenv("NVTE_USE_CUTLASS_GROUPED_GEMM", raising=False)
monkeypatch.setenv("NVTE_USE_GROUPED_GEMM_FLYDSL", "1")

# For the configs CK grouped GEMM is guaranteed to handle,
# assert it does not silently fall back to multi-stream hipBLASLt.
Expand All @@ -656,6 +679,32 @@ def test_grouped_linear_accuracy_rocm_backends(
if expect_ck_no_fallback:
monkeypatch.setenv("NVTE_CUTLASS_GROUPED_GEMM_WARN_FALLBACK", "1")

# FlyDSL falls back to the default grouped GEMM silently on any config it rejects
# past the gate skipped above, and wgrad is not attempted at all with fused or
# delayed wgrad, so a missing fallback warning does not show that FlyDSL ran.
# Record each FlyDSL dispatch and assert on the phases that actually ran; the
# fallback warning stays on so a failure shows the reason.
flydsl_dispatches = []
if grouped_gemm_backend == "flydsl":
from transformer_engine.pytorch.flydsl_kernels.gemm import (
FlyDSLUnsupportedError,
gemm_wrappers,
)

monkeypatch.setenv("NVTE_FLYDSL_GEMM_WARN_FALLBACK", "1")
grouped_gemm_flydsl = gemm_wrappers._grouped_gemm_flydsl
phases = {"TN": "fwd", "NN": "dgrad", "NT": "wgrad"}

def recording_grouped_gemm_flydsl(*args, layout, **kwargs):
try:
grouped_gemm_flydsl(*args, layout=layout, **kwargs)
except FlyDSLUnsupportedError:
flydsl_dispatches.append((phases[layout], False))
raise
flydsl_dispatches.append((phases[layout], True))

monkeypatch.setattr(gemm_wrappers, "_grouped_gemm_flydsl", recording_grouped_gemm_flydsl)

test_grouped_linear_accuracy(
dtype,
num_gemms,
Expand All @@ -671,6 +720,20 @@ def test_grouped_linear_accuracy_rocm_backends(
use_cutlass=True,
)

if grouped_gemm_backend == "flydsl":
expected = {"fwd", "dgrad"}
if not fuse_wgrad_accumulation and not delay_wgrad_compute:
expected.add("wgrad")
ran = {phase for phase, ok in flydsl_dispatches if ok}
fell_back = sorted({phase for phase, ok in flydsl_dispatches if not ok})
assert not fell_back and expected <= ran, (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (low): the dispatch recording is a solid fix for the false pass, but expected <= ran is a subset check, so it only locks the lower bound. A regression that widens the production gate at grouped_linear.py:2804-2809 — e.g. FlyDSL wgrad running under fuse_wgrad_accumulation, where it would overwrite main_grad instead of accumulating — leaves ran == {"fwd","dgrad","wgrad"} against expected == {"fwd","dgrad"} and still passes this assertion.

expected == ran can't false-fail: phase is only ever "fwd"/"dgrad"/"wgrad", and with fuse_wgrad_accumulation or delay_wgrad_compute the gate short-circuits before _try_flydsl_grouped is called at all, so "wgrad" can never appear in ran for those cells. It makes the assertion verify the gate the comment above describes, for free.

f"FlyDSL grouped GEMM did not run every expected phase: expected {sorted(expected)}, "
f"ran {sorted(ran)}, fell back in {fell_back} (dtype={dtype}, "
f"recipe={recipe_id(recipe)}, fp8_model_params={fp8_model_params}, "
f"fuse_wgrad={fuse_wgrad_accumulation}, delay_wgrad={delay_wgrad_compute}). "
"See the [FLYDSL WARNING] for the fallback reason."
)

if expect_ck_no_fallback:
# NVTE_WARN writes to std::cerr; capfd captures file-descriptor-level
# output, including C/C++ stderr.
Expand All @@ -682,6 +745,120 @@ def test_grouped_linear_accuracy_rocm_backends(
)


@pytest.mark.skipif(not IS_HIP_EXTENSION, reason="ROCm-only grouped GEMM backend")
@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8)
@pytest.mark.parametrize(
"recipe", [recipe.Float8CurrentScaling(), recipe.DelayedScaling()], ids=recipe_id
)
@pytest.mark.parametrize("fp8_model_params", all_boolean)
def test_grouped_linear_flydsl_fp8_scaling(recipe, fp8_model_params, monkeypatch):
"""FlyDSL tensor-wise FP8 grouped GEMM vs. sequential Linear over several steps.

Delayed scaling quantizes its first step with unit scales (there is no amax history
yet), so the single-step accuracy test cannot catch a per-expert scale bug for it.
Run a few steps with uneven experts and require the compared step to hand FlyDSL
non-unit scales that differ across experts, for every phase.
"""
if get_device_compute_capability() != (9, 5):
pytest.skip("FlyDSL grouped GEMM is only supported on gfx950.")
pytest.importorskip("flydsl", reason="FlyDSL package is not installed.")
from transformer_engine.pytorch.flydsl_kernels.gemm import (
FlyDSLUnsupportedError,
gemm_wrappers,
)

num_gemms, num_steps, dtype = 3, 3, torch.bfloat16
config = model_configs["126m"]
monkeypatch.setenv("NVTE_USE_GROUPED_GEMM_FLYDSL", "1")
monkeypatch.setenv("NVTE_FLYDSL_GEMM_WARN_FALLBACK", "1")

# Record each FlyDSL grouped GEMM: its phase, whether it ran, and the operands'
# per-tensor inverse scales.
phases = {"TN": "fwd", "NN": "dgrad", "NT": "wgrad"}
dispatches = []
grouped_gemm_flydsl = gemm_wrappers._grouped_gemm_flydsl

def recording_grouped_gemm_flydsl(A, B, out, *, layout, **kwargs):
scales = {float(t._scale_inv) for t in list(A) + list(B)}
try:
grouped_gemm_flydsl(A, B, out, layout=layout, **kwargs)
except FlyDSLUnsupportedError:
dispatches.append((phases[layout], False, scales))
raise
dispatches.append((phases[layout], True, scales))

monkeypatch.setattr(gemm_wrappers, "_grouped_gemm_flydsl", recording_grouped_gemm_flydsl)

with quantized_model_init(enabled=fp8_model_params, recipe=recipe):
grouped_linear = GroupedLinear(
num_gemms,
config.hidden_size,
4 * config.hidden_size,
bias=False,
params_dtype=dtype,
device="cuda",
)
sequential_linear = torch.nn.ModuleList(
[
Linear(
config.hidden_size,
4 * config.hidden_size,
bias=False,
params_dtype=dtype,
device="cuda",
)
for _ in range(num_gemms)
]
)
with torch.no_grad():
for i in range(num_gemms):
sequential_linear[i].weight = Parameter(getattr(grouped_linear, f"weight{i}").clone())

# Uneven, FlyDSL-aligned experts so each carries its own scales.
align = get_align_size_for_quantization(recipe)
tokens = config.max_seqlen_q
m_splits = [tokens // 4, tokens // 2, tokens // 4]
assert sum(m_splits) == tokens and all(m % align == 0 for m in m_splits)
reset_rng_states()
inputs = [
torch.randn(tokens, config.hidden_size, dtype=dtype, device="cuda")
for _ in range(num_steps)
]

def run(block):
FP8GlobalStateManager.reset()
for inp in inputs:
dispatches.clear()
for p in block.parameters():
p.grad = None
inp = inp.clone().requires_grad_()
with autocast(enabled=True, recipe=recipe):
if isinstance(block, GroupedLinear):
out = block(inp, m_splits)
else:
out = torch.cat([block[i](x) for i, x in enumerate(torch.split(inp, m_splits))])
out.sum().backward()
torch.cuda.synchronize()
return [out, inp.grad] + [p.grad for p in block.parameters()]

outputs_ref = run(sequential_linear)
outputs = run(grouped_linear)

ran = {phase for phase, ok, _ in dispatches if ok}
fell_back = sorted({phase for phase, ok, _ in dispatches if not ok})
assert not fell_back and ran == {"fwd", "dgrad", "wgrad"}, (
f"FlyDSL grouped GEMM ran {sorted(ran)}, fell back in {fell_back}. "
"See the [FLYDSL WARNING] for the fallback reason."
)
for phase, _, scales in dispatches:
assert scales != {1.0} and len(scales) > 1, (
f"{phase}: the compared step used scales {sorted(scales)}; it must exercise "
"non-unit, per-expert FP8 scales."
)
for o, o_ref in zip(outputs, outputs_ref):
torch.testing.assert_close(o, o_ref, rtol=8e-3, atol=1e-3)


@pytest.mark.skipif(not IS_HIP_EXTENSION, reason="ROCm-only grouped GEMM backend")
@pytest.mark.skipif(not mxfp8_available, reason=reason_for_no_mxfp8)
def test_grouped_linear_cuda_graph_hipkittens_mxfp8(monkeypatch, capfd):
Expand Down
5 changes: 3 additions & 2 deletions transformer_engine/pytorch/flydsl_kernels/gemm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# See LICENSE for license information.

"""FlyDSL GEMM kernels (dense, non-grouped) for BF16/FP16/FP32/FP8/MXFP8."""
"""FlyDSL GEMM kernels: dense BF16/FP16/FP32/FP8/MXFP8, grouped BF16/FP16/FP8/MXFP8."""

# Supported flydsl release series. Single source of truth for the version
# requirement, since flydsl is user-installed (not a declared dependency). A
Expand Down Expand Up @@ -41,9 +41,10 @@ def _check_flydsl_version() -> None:
_check_flydsl_version()

from .exceptions import FlyDSLUnsupportedError
from .gemm_wrappers import te_generic_gemm_flydsl
from .gemm_wrappers import general_grouped_gemm_flydsl, te_generic_gemm_flydsl

__all__ = [
"FlyDSLUnsupportedError",
"general_grouped_gemm_flydsl",
"te_generic_gemm_flydsl",
]
Loading
Loading