Add FlyDSL grouped groupwise FP8 GEMM for ROCm - #456
Closed
aryaman-gupta wants to merge 18 commits into
Closed
Conversation
Add the cross-cutting FlyDSL kernel building blocks that MSLK's FlyDSL-backed GEMM/MoE kernels depend on, under mslk/flydsl/kernels/ mirroring FlyDSL's own common/mma/gemm domain layout. These modules are not part of the installable flydsl package and must be vendored to be importable by MSLK. Co-Authored-By: Claude <noreply@anthropic.com>
Adds the ROCm preshuffle sibling of f8f8bf16_groupwise_grouped, backed by a FlyDSL contiguous grouped GEMM kernel that dispatches over a linearized per-tile map (compact unpadded output, no tile spanning a group boundary) and consumes weights pre-swizzled into the MFMA B layout. Registers the op when FlyDSL is available, adds preshuffle_b_mfma for the one-time weight swizzle, and covers correctness in the grouped GEMM test. Co-Authored-By: Claude <noreply@anthropic.com>
Extends the FlyDSL contiguous grouped groupwise FP8 GEMM kernel two ways, both unified in grouped_gemm_blockscale_contiguous.py: - Wide-MFMA software-scaling path: on gfx950 the FP32-scale path now uses the wide mfma_scale_f32_16x16x128 with a neutral E8M0 scale + per-scale-block FP32 apply (gfx942 keeps the narrow 16x16x32 path), matching the working blockscale_preshuffle_gemm kernel. Large speedup on K-heavy shapes. - Plain (non-preshuffled) B via a b_preshuffled flag (default True). When False, B is plain row-major [G,N,K] and is staged HBM->LDS->registers (mirroring the A pipeline: coalesced dwordx4 loads, xor16-swizzled bank-conflict-free LDS) instead of the preshuffle HBM->registers path. This is the apples-to-apples input for the Triton f8f8bf16_groupwise_grouped op. The two variants share the whole kernel body (tile-map group dispatch, scale-apply, wide-MFMA, CShuffle epilogue); only the B load stage + its LDS allocation differ. grouped_gemm_blockscale_plain.py is a thin delegator. Correctness verified on gfx950 for both variants (plain grouped test + existing preshuffle acceptance cases). Co-Authored-By: Claude <noreply@anthropic.com>
Registers the FlyDSL plain-B kernel as the ROCm impl of the plain op mslk::f8f8bf16_groupwise_grouped (C++ schema in gemm_ops.cpp), consuming plain row-major [G,N,K] weights via the unified kernel with b_preshuffled=False. The Triton impl now registers only as a fallback when FlyDSL is unavailable (it is slated for removal). Drops the redundant grouped_gemm_blockscale_plain delegator in favor of calling compile_grouped_gemm_blockscale_contiguous(..., b_preshuffled=False) directly. Verified: both grouped ops pass the acceptance suite end-to-end via torch.ops.mslk (plain cases 0-3 + preshuffle cases 4-7), 8/8 on gfx950. Co-Authored-By: Claude <noreply@anthropic.com>
The FlyDSL wrapper rebuilt a per-tile dispatch map every call via ~8 tiny tensor-op kernels (arange/cumsum/searchsorted/where). Under CUDA-graph capture these replay as separate kernels with fixed per-call overhead that dominated small-M shapes. Replace the host tile-map with in-kernel group resolution: the kernel takes M_sizes directly and, for its flat M-tile id, runs a compile-time unrolled loop over the groups (num_groups is constexpr) to find the owning group, row start, and row limit. The grid keeps its compact upper-bound extent. Also wire per-shape tile selection through get_tile_config (static tables) and unify the plain and preshuffle ops behind _dispatch_grouped_gemm. Co-Authored-By: Claude <noreply@anthropic.com>
Replace the static tile table with FlyDSL's native @autotune, gated on MSLK_AUTOTUNE_ENABLE (CUTLASS precedent): when set, autotune benchmarks the candidate tiles on a cache-miss and persists the winner to disk; when unset, a fixed default tile is used with no benchmarking (the CI / graph-capture path). The autotuner wraps a thin target that forwards to the existing compile factory, so the kernel is unchanged. The tuning key is (nextPow2(TotalM), N, K, b_preshuffled): TotalM is bucketed to match graph-capture buckets and bound the pre-warm set, N/K separate problem shapes, and b_preshuffled keeps the two kernels' entries distinct in one shared disk cache. Invalid tiles are pruned per shape before benchmarking. FlyDSL's Autotuner discards the target return value, so the wrapper returns the in-place output buffer itself. Co-Authored-By: Claude <noreply@anthropic.com>
mslk/utils/flydsl.py was removed when FlyDSL became a required ROCm dependency (meta-pytorch#447); is_flydsl_available/require_flydsl now live in mslk.flydsl.common and run_compiled in mslk.flydsl.jit. Point the grouped-GEMM op, its Triton fallback gate, the gemm package init, and the test at the new modules.
The preshuffle path had no host-side LDS check, so an oversized tile reached the compiler backend, which reports an LDS overflow as a hard error that kills the process -- autotune could not catch and skip it. Add validate_lds_budget_preshuffle (A ping-pong aliased with the CShuffle epilogue) and run it alongside the plain check before tracing. The capacity was also hardcoded to 64 KiB. It is arch-dependent: 64 KiB on gfx942 but 160 KiB on gfx950. Source it from FlyDSL's SMEM_CAPACITY_MAP so the limit matches the compiler that enforces it, falling back to 64 KiB for unknown architectures. A tile_m=256 tile that fits on MI350 is now correctly rejected on MI300, and the wider gfx950 tiles are no longer needlessly excluded.
The plain f8f8bf16_groupwise_grouped op is FlyDSL-backed on ROCm, so require FlyDSL for both variants there rather than exercising the Triton fallback. Drop test/gemm/flydsl_plain_grouped_test.py: it covered the same four group shapes as the parameterized test in gemm_test.py.
quantize_fp8_group(m_sizes=...) stores scale_a as per-group blocks: group g starts at M_start*scale_k and holds element (local_m, k_g) at local_m + k_g*M_g. The kernel instead indexed it as a global [scale_k, TotalM] transpose (k_g*TotalM + row_global), which only coincides with the real layout when there is a single group or a single K-block. With G > 1 and K > 128 every group past the first applied another group's scales, silently producing wrong results. Carry the owning group's M_start and M_g out of the group-resolution loop and use them to address scale_a. Output now matches the Triton implementation bit-exactly on the shapes that previously diverged. Tighten the grouped test tolerance to match: outputs are O(1e-2) and FP8 block quantization accounts for ~4e-3, so the old atol of 8e-2 was several times the signal and passed regardless of the scales used.
The kernel resolves group ownership from m_sizes, but the module docstring and the launcher comment still described the tile_group/tile_row_start/ tile_row_limit arrays that preceded it, and the kernel still declared i32_num_m_tiles without using it (the launcher keeps it for the grid extent). Both docstrings also described scale_a as a global [scale_k, M_total] transpose; document the per-group block layout the kernel actually reads. Note in the wrapper docstring and the gemm package init that FlyDSL registers the plain op as well as the preshuffle sibling. Rename the local i32-constant helper to _i32 so it no longer shadows the _c compile-constants namedtuple from the enclosing scope.
The consuming kernels build their B layout with NLane=16, KLane=4 and KPack=16 hardcoded (make_preshuffle_b_layout), so the NLane parameter could only ever produce a layout they cannot read. Drop it and fix the extents. Add the divisibility precondition as well: an N or K that does not divide evenly previously surfaced as a bare reshape error naming internal dimensions. Accept **kwargs in _prune_tiles so it works with either the FlyDSL (configs, sig_args) or Triton (configs, named_args, **meta) calling convention.
The wrapper hands the FP8 operands to the kernel as raw bytes, so weights quantized in the other FP8 format were applied with the wrong exponent bias instead of being rejected. Check both operands against the format the device uses (fnuz on gfx942, OCP elsewhere), and restore the M_sizes rank and length checks the Triton implementation performed. Read m_sizes directly as int64 by loading the low dword of each element, which removes the per-call narrowing kernel the wrapper previously enqueued.
Describe what the code does rather than how it came to look that way, and correct the plain-B K-loop docstring: the loop is software-pipelined and uses two barriers per K-tile, not one, and single-buffered LDS is a budget tradeoff rather than unfinished work.
The schema comment still pointed readers at the Triton implementation.
Every other op in mslk/gemm declares its schema in csrc/gemm/gemm_ops.cpp, so move this one there too, next to the other ROCm-only preshuffle GEMMs. The implementation stays in Python, matching ops such as bf16i4bf16_rowwise. Register both ops through one guarded helper: the schema is absent in a python-only build, and a repeated import must not fail on rebinding.
Argument marshalling packs each memref extent as int32, so a flattened operand overflowed once it reached 2**31 elements: at G=8, N=K=16384 the B operand is exactly 2**31 bytes and no configuration could launch, though the Triton implementation handles that shape. Pass the operands with their natural shape; the kernel addresses them as flat byte buffers through explicit descriptors either way.
|
@q10 has imported this pull request. If you are a Meta employee, you can view this in D113947727. |
mslk/gemm/flydsl/ holds the op wrappers for the FlyDSL GEMM kernels but was not in the ROCm trigger paths, so a change confined to it would skip ROCm CI.
aryaman-gupta
marked this pull request as ready for review
July 31, 2026 10:38
Contributor
|
@aryaman-gupta could you rebase this PR on latest main? |
q10
pushed a commit
to q10/MSLK-1
that referenced
this pull request
Aug 5, 2026
Summary: Pull Request resolved: meta-pytorch#470 Adds a FlyDSL implementation of the grouped groupwise FP8 GEMM for ROCm and makes it the ROCm backend for `mslk::f8f8bf16_groupwise_grouped`. Two ops share one kernel, selected by a compile-time `b_preshuffled` flag: - **`mslk::f8f8bf16_groupwise_grouped`** — plain row-major `[G, N, K]` weights. - **`mslk::f8f8bf16_groupwise_grouped_preshuffle`** — new ROCm-only sibling taking weights already swizzled into the MFMA B layout by `mslk.quantize.shuffle.preshuffle_b_mfma`. Callers shuffle once at load time and the op does no shuffling. Both require `N` and `K` to be multiples of 128, matching the weight scale-block granularity. Activation scales are consumed in the per-group block layout produced by `quantize_fp8_group(m_sizes=...)`. ## Performance CUDA-graph replay latency in µs on gfx950 (MI350X), G=8. **Both implementations are tuned**: Triton through its own `triton.autotune`, FlyDSL through MSLK autotune over its full candidate tile set. The last two columns are speedup over Triton, `(t_triton / t_flydsl − 1) × 100`, so **higher is better** and a negative value is a slowdown. Representative rows (two per shape) from a 50-row sweep. Preshuffle is faster on 45 of 50 shapes, median **+38%**, range −20% to +191%. | totM | N | K | triton µs | plain µs | preshuffle µs | plain | preshuffle | |-----:|----:|-----:|----------:|---------:|--------------:|------:|-----------:| | 32 | 1280 | 8192 | 42.9 | 65.2 | 53.5 | −34% | −20% | | 128 | 1280 | 8192 | 82.0 | 66.0 | 54.5 | +24% | +50% | | 1024 | 2048 | 7168 | 99.4 | 62.2 | 53.0 | +60% | +88% | | 16384 | 2048 | 7168 | 699.5 | 470.9 | 405.4 | +49% | +73% | | 1024 | 7168 | 2304 | 74.1 | 42.8 | 43.5 | +73% | +70% | | 16384 | 7168 | 2304 | 623.6 | 547.0 | 482.6 | +14% | +29% | | 32 | 7168 | 8192 | 111.8 | 119.3 | 97.0 | −6% | +15% | | 128 | 7168 | 8192 | 156.8 | 116.2 | 102.0 | +35% | +54% | | 1 | 8192 | 1024 | 20.7 | 13.4 | 12.0 | +54% | +72% | | 128 | 8192 | 1024 | 29.6 | 19.8 | 19.9 | +49% | +49% | | 32 | 8192 | 3584 | 46.2 | 41.9 | 39.9 | +10% | +16% | | 128 | 8192 | 3584 | 62.8 | 43.4 | 41.0 | +45% | +53% | | 32 | 13312 | 6656 | 158.8 | 154.0 | 146.1 | +3% | +9% | | 128 | 13312 | 6656 | 231.8 | 157.2 | 148.6 | +47% | +56% | | 32 | 13312 | 16384 | 373.2 | 453.5 | 367.2 | −18% | +2% | | 128 | 13312 | 16384 | 549.0 | 463.6 | 380.7 | +18% | +44% | | 32 | 16384 | 6656 | 266.7 | 167.1 | 162.6 | +60% | +64% | | 128 | 16384 | 6656 | 507.5 | 186.2 | 174.2 | +173% | +191% | FlyDSL is slower on `1280 × 8192` up to totM 96, where the output is narrow enough that it cannot produce as many tiles as Triton; it leads on every other shape measured. Every shape is checked against a per-group bf16 reference before timing, and additionally against Triton: the two agree to within one bf16 ulp on every shape, and the residual against the reference is FP8 quantization error, identical for both (~3.5% of max|ref|). ## Tile selection With `MSLK_AUTOTUNE_ENABLE` set, FlyDSL autotune benchmarks the candidate tiles on a cache miss and persists the winner to disk, keyed on `(nextPow2(total_M), N, K, b_preshuffled)`. This follows the CUTLASS precedent in `f8f8bf16_groupwise_grouped.cu`, including the env gate: unset — the default, and what CI uses — a fixed tile is used and nothing is benchmarked. Benchmarking cannot run inside a graph capture, so a shape must be warmed before it is captured. Everything else about dispatch is capture-safe: the grid extent is derived from static shapes alone, nothing on the host reads tensor contents, and no helper kernels are launched, so eager and captured dispatch issue the same single kernel. ## Architecture support gfx950 uses the wide `mfma_scale_f32_16x16x128_f8f6f4` with a neutral E8M0 scale and applies the FP32 scales in software. gfx942 lacks that instruction and takes a narrow `16x16x32` path. LDS budgets are checked per architecture (64 KiB on gfx942, 160 KiB on gfx950), so a tile that fits on MI350 is rejected at compile time on MI300 rather than failing in the backend. Correctness has been verified on gfx950 (MI350) and gfx942 (MI300). ## Testing `test/gemm/gemm_test.py::test_f8f8bf16_groupwise_grouped` covers both variants across four group splits, including uneven and non-tile-aligned ones, and requires FlyDSL on ROCm. The tolerance there is tightened from `atol=8e-2` to `1e-2`. Outputs on those shapes are O(1e-2), so the previous bound sat several times above the signal and would pass regardless of which scales the kernel applied. ## Notes for reviewers `mslk/flydsl/kernels/{common,mma}/` and `gemm/fp8_gemm_utils.py` are vendored unmodified from FlyDSL and are best reviewed as an import. The kernel itself is `mslk/flydsl/kernels/gemm/grouped_gemm_blockscale_{contiguous,common}.py`, and the op wrapper is `mslk/gemm/flydsl/fp8_groupwise_grouped_gemm.py`. Pull Request resolved: meta-pytorch#456 Reviewed By: jwfromm Differential Revision: D113947727 Pulled By: q10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a FlyDSL implementation of the grouped groupwise FP8 GEMM for ROCm and makes it the ROCm backend for
mslk::f8f8bf16_groupwise_grouped.Two ops share one kernel, selected by a compile-time
b_preshuffledflag:mslk::f8f8bf16_groupwise_grouped— plain row-major[G, N, K]weights.mslk::f8f8bf16_groupwise_grouped_preshuffle— new ROCm-only sibling taking weights already swizzled into the MFMA B layout bymslk.quantize.shuffle.preshuffle_b_mfma. Callers shuffle once at load time and the op does no shuffling.Both require
NandKto be multiples of 128, matching the weight scale-block granularity. Activation scales are consumed in the per-group block layout produced byquantize_fp8_group(m_sizes=...).Performance
CUDA-graph replay latency in µs on gfx950 (MI350X), G=8. Both implementations are tuned: Triton through its own
@triton.autotune, FlyDSL through MSLK autotune over its full candidate tile set.The last two columns are the latency reduction relative to Triton,
(t_triton − t_flydsl) / t_triton × 100, so higher is better and a negative value is a slowdown.Representative rows (two per shape) from a 50-row sweep. Preshuffle is faster on 45 of 50 shapes, median +28%, range −25% to +66%.
FlyDSL is slower on
1280 × 8192up to totM 96, where the output is narrow enough that it cannot produce as many tiles as Triton; it leads on every other shape measured.Every shape is checked against a per-group bf16 reference before timing, and additionally against Triton: the two agree to within one bf16 ulp on every shape, and the residual against the reference is FP8 quantization error, identical for both (~3.5% of max|ref|).
Tile selection
With
MSLK_AUTOTUNE_ENABLEset, FlyDSL autotune benchmarks the candidate tiles on a cache miss and persists the winner to disk, keyed on(nextPow2(total_M), N, K, b_preshuffled). This follows the CUTLASS precedent inf8f8bf16_groupwise_grouped.cu, including the env gate: unset — the default, and what CI uses — a fixed tile is used and nothing is benchmarked.Benchmarking cannot run inside a graph capture, so a shape must be warmed before it is captured. Everything else about dispatch is capture-safe: the grid extent is derived from static shapes alone, nothing on the host reads tensor contents, and no helper kernels are launched, so eager and captured dispatch issue the same single kernel.
Architecture support
gfx950 uses the wide
mfma_scale_f32_16x16x128_f8f6f4with a neutral E8M0 scale and applies the FP32 scales in software. gfx942 lacks that instruction and takes a narrow16x16x32path. LDS budgets are checked per architecture (64 KiB on gfx942, 160 KiB on gfx950), so a tile that fits on MI350 is rejected at compile time on MI300 rather than failing in the backend.Correctness has been verified on gfx950 (MI350) and gfx942 (MI300).
Testing
test/gemm/gemm_test.py::test_f8f8bf16_groupwise_groupedcovers both variants across four group splits, including uneven and non-tile-aligned ones, and requires FlyDSL on ROCm.The tolerance there is tightened from
atol=8e-2to1e-2. Outputs on those shapes are O(1e-2), so the previous bound sat several times above the signal and would pass regardless of which scales the kernel applied.Notes for reviewers
ROCm CI already triggers on this change;
mslk/gemm/flydsl/**is added to its path filter so later changes confined to the op wrappers do not skip it.The FlyDSL modules vendored here are also vendored, at the same path, by #434 and #444. If either lands first, this PR will be refactored to drop the repeated files.
mslk/flydsl/kernels/{common,mma}/andgemm/fp8_gemm_utils.pyare vendored unmodified from FlyDSL and are best reviewed as an import. The kernel itself ismslk/flydsl/kernels/gemm/grouped_gemm_blockscale_{contiguous,common}.py, and the op wrapper ismslk/gemm/flydsl/fp8_groupwise_grouped_gemm.py.