You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Feature] Add gemm_a8w8_bpreshuffle_out for caller-owned bf16 outputs - #6437
SGLang's Kimi-K3 ROCm path writes its PTPC FP8 shared-expert down-projection straight into a preallocated buffer (shared_output = buf[offset:].view(M, hidden)), which is later all-reduced together with the MoE latent. gemm_a8w8_bpreshuffle always allocates its output, so today that result has to be copied. This PR adds an op that writes into a caller-owned buffer.
Technical Details
New op gemm_a8w8_bpreshuffle_out(out, XQ, WQ, x_scale, w_scale) -> None. It writes into out and picks the same kernel as gemm_a8w8_bpreshuffle, because both share one dispatch helper (_gemm_a8w8_bpreshuffle_into).
torch.compile:
The op declares only out as mutated (mutates_args=["out"]) and returns nothing. Its schema is (Tensor(a0!) out, Tensor XQ, Tensor WQ, Tensor x_scale, Tensor w_scale) -> ().
Compiled graphs therefore add no writebacks for the read-only inputs, and no op output aliases an input.
It is a separate op rather than an optional out= on gemm_a8w8_bpreshuffle: narrowing that op's mutates_args with an optional out broke inductor for its existing no-out callers (getitem is not an OpOverload).
Inputs:
Rejected with ValueError before any launch, because no kernel supports them: non-2-D or non-fp8 XQ/WQ, non-fp32 scales or scales without M/N elements, operands that are not all on one CUDA/HIP device, WQ K < XQ K, and an out that is not a contiguous bf16 [M, N] tensor.
bf16 only: the FlyDSL 8-wave kernels, selected by many tuned rows, write bf16 only, so an fp16 out would fail at launch on those shapes.
Contiguous out only: CK and CKTile write a dense row-major [M, N], so a strided out (for example a column slice of a wider buffer) would be written past its bounds.
out must start 16-byte aligned. A dense view into a larger buffer is fine at an aligned offset; at an odd bf16 offset the CK split-K kernel faulted. The rule applies to every backend so that a valid buffer never depends on which tuned row is selected. torch.empty storage and SGLang's buf[M * latent:] view are always aligned.
Arch: only gfx942 and gfx950 are accepted. Elsewhere the op raises NotImplementedError before launch, because the shared dispatch would reach backends this op was never validated on (the gfx1250 WMMA path and the CK MFMA fallback on RDNA); use gemm_a8w8_bpreshuffle there. This check runs after the argument checks, so invalid arguments always get ValueError.
Normalized, not rejected: a strided XQ, WQ or scale is copied to a dense tensor first, because CK and CKTile read only row strides and raw scale pointers. Dense inputs pass through without a copy.
Any device: when the operands are not on the current device, the op runs under torch.cuda.device(XQ.device), because the launchers use the current device's stream. On the normal path it adds no guard.
An empty out (M or N = 0) returns after validation. M=0 matches tuned FlyDSL rows whose launch would otherwise fail.
out must not overlap any input. A K-padded WQ keeps gemm_a8w8_bpreshuffle's padded-K handling unchanged.
gemm_a8w8_bpreshuffle is unchanged: same signature, same custom-op schema, same results.
Test Plan
New file op_tests/test_gemm_a8w8_bpreshuffle_out.py:
The schema marks only out as written and has no return value.
out is bit-identical to gemm_a8w8_bpreshuffle on gfx950 tuned rows that select each backend: CK, CK split-K, CKTile, FlyDSL, FlyDSL split-K and FlyDSL 8-wave. Each case asserts that its row still selects that backend and fails otherwise, so coverage cannot silently drop when tables change.
Bit-identical on an untuned shape and on the padded-K path (WQ K > XQ K). Both assert they have no tuned row.
A dense view of a larger flat buffer gets the right result, and the rest of the buffer is untouched.
torch.compile(fullgraph=True) with aot_eager and with inductor (caches disabled): out is filled and matches eager.
A wrong shape, fp16 or fp32 out, a column-slice or transposed out, WQ K < XQ K, WQ, x_scale or w_scale on another device, an all-CPU call, a rank-3 XQ or WQ, an int8 XQ, and a bf16 or short scale each raise ValueError and leave the buffer untouched.
A column-major or row-strided XQ, a row-strided WQ, strided scales and a non-contiguous [1, M] scale all give the same result as dense inputs.
With two GPUs visible, operands on cuda:1 called from cuda:0 give the right result and leave cuda:0 current.
An empty out (M=0 on a FlyDSL row and an untuned row, and N=0) is a no-op and leaves the device usable.
Running the file directly (python op_tests/test_gemm_a8w8_bpreshuffle_out.py) benchmarks both ops over a spread of N/K shapes, including the Kimi-K3 TP=8 shared-expert down projection (N=7168, K=768), writing out as a dense view into a caller-owned [latent | shared] buffer as SGLang does, and prints a markdown summary.
Existing op_tests/test_gemm_a8w8_bpreshuffle_pad_k.py still passes, and gemm_a8w8_bpreshuffle keeps main's schema and still compiles under inductor.
out at an odd bf16 offset raises ValueError and leaves the buffer untouched.
Every arch outside gfx942/gfx950 (gfx1250, gfx1201, unknown) raises NotImplementedError before launch, and an all-CPU call raises ValueError even when the arch probe fails.
The per-backend tuned-row cases run only on gfx950 with 256 CUs.
gemm_a8w8_bpreshuffle still compiles under inductor (fullgraph=True) and matches eager.
Test Result
MI355X (gfx950), ROCm 7.2, torch 2.11.0+rocm7.2:
op_tests/test_gemm_a8w8_bpreshuffle_out.py: 53 passed, 1 skipped with one GPU visible (the skipped test needs two GPUs and passes with two). With no GPU visible, all skip.
GPU kernel time per call (us), from the in-file benchmark, which now also times gemm_a8w8_bpreshuffle followed by the copy into the caller's buffer (run_perftest, FlyDSL rows at every shape below, error 0 everywhere). N=7168 K=768 is the Kimi-K3 TP=8 shared-expert down projection:
M
N, K
bpreshuffle
bpreshuffle + copy
bpreshuffle_out
1
7168, 768
2.75
4.10
2.75
4
7168, 768
2.74
4.76
2.74
16
7168, 768
2.78
4.71
2.76
64
7168, 768
3.33
5.16
3.34
4
7168, 2048
3.43
4.65
3.42
64
7168, 2048
4.68
9.94
4.66
Host time per call (M=4, best of 5 x 2000 calls, standalone script): K=768: 27.0 us with the copy vs 23.4 us for _out (22.6 us for bpreshuffle alone); K=2048: 24.9 vs 22.3 us (21.5 us alone). An earlier run of the same comparison read 33.0 -> 28.6 us host and 5.63 -> 3.92 us GPU at K=2048; the re-run above replaces it.
The benchmark main() over M = 1 to 4096 and N/K = 4096/4096, 7168/2048, 2112/7168, 8192/1024 and 7168/768 selects CK, CKTile, FlyDSL and untuned rows. Error is 0 for both ops on every row, and kernel times match within noise.
ATOM accuracy suite for PR and main models from ATOM models_accuracy.json
ci:vllm
vLLM benchmark: DeepSeek-V4-Pro, MiniMax-M3-MXFP4
ci:all
All standard extended tests (excludes ci:atom_full)
Only add ci:atom_full for FlyDSL or Triton upgrades.
Add labels via the sidebar or gh pr edit 6437 --add-label <label>
One backend per PR:
A PR changes one kernel backend: [Triton/Gluon] (Triton and Gluon count as one), [HIP], [ASM], [CK], [OPUS] or [FlyDSL]. If the title ends up with two backend tags, split the PR -- as stacked pull requests when one part cannot merge without the other.
PR title tags & labels:
Component tags ([Triton/Gluon], [HIP], [CK], [ASM], ...) are added to the PR title and as PR labels automatically from the changed files and re-synced on every push — change-type tags like [fix]/[Perf], op tags like [MLA], and human labels (ci:*) are left untouched. Add the no-auto-title label to stop the title rewrites; labels stay in sync either way.
Only WQ is type-checked. On a tuned 8-wave row, an int8XQ passes the launcher's one-byte-storage check and is then consumed by the FP8 kernel, producing incorrect values rather than rejecting the input. Validate both quantized operands here.
Validate scale tensors are fp32
aiter/ops/gemm_op_a8w8.py:899
The documented fp32 scale contract is not enforced before dispatch. The standard FlyDSL path passes these tensors through unchanged but emits BufferCopy32b(..., Float32) scale loads (preshuffle_gemm.py:716), while the 8-wave path converts them, so non-fp32 scales have backend-dependent behavior and can be misread by the standard kernel. Validate both scale dtypes before calling the helper.
Add the standard runnable op-test benchmark flow
op_tests/test_gemm_a8w8_bpreshuffle_out.py:19
This new op_tests/test_*.py suite omits the repository's standard runnable op-test entry point: there is no @benchmark/run_perftest candidate loop, checkAllclose, markdown summary, or main() gated by get_gfx(). As a result, the claimed allocation-overhead improvement has no in-repo benchmark reproducer and the suite cannot be exercised through the normal op-test workflow. Add the standard benchmark/main flow while retaining these pytest contract cases.
Use get_gfx consistently for architecture checks
op_tests/test_gemm_a8w8_bpreshuffle_out.py:25
This test gate uses the live-device helper, but _backend() calls the tuned-table lookup whose architecture key comes from get_gfx(). When GPU_ARCHS and the live card differ, the gate and the backend assertion inspect different architecture contracts, so the intended gfx950 backend coverage can be silently skipped. Tests should use get_gfx() consistently with the lookup they verify.
…8 adapter
- run() passed out= on every call, even out=None; aiter builds without that kwarg failed at warmup.
- Without a buffer, call gemm_a8w8_bpreshuffle with no out=.
- With a buffer, call aiter's gemm_a8w8_bpreshuffle_out (ROCm/aiter#6437), or copy on builds that lack it.
- Reject an out whose dtype differs from the GEMM output dtype.
yuychang
added a commit
to yuychang/aiter
that referenced
this pull request
Oct 12, 2026
- Same op as ROCm#6437: writes into out, returns None, mutates only out, bf16 contiguous [M, N] only.
- Share dispatch with gemm_a8w8_bpreshuffle via _gemm_a8w8_bpreshuffle_into.
- Keep the out= kwarg on gemm_a8w8_bpreshuffle until SGLang's ptpc wrapper no longer passes it.
yuychang
added a commit
to yuychang/aiter
that referenced
this pull request
Oct 12, 2026
…coverage
- Same as ROCm#6437: reject out, WQ, x_scale or w_scale on a device other than XQ's before launch.
- Fail, not skip, when a per-backend test row no longer selects its backend; assert untuned rows stay untuned.
yuychang
added a commit
to yuychang/aiter
that referenced
this pull request
Oct 12, 2026
…ller outputs
- Accept producer-quantized (fp8, per-token scale) inputs in apply_fp8_linear and Quark W8A8 FP8 on ROCm.
- Add ROCm probes for linears that take group-128 or PTPC FP8 tuples without re-quantization.
- Restore QuarkW4A4MXFP4.apply_into; the Kimi-K3 MoE path calls it on linear.scheme.
- Add the PTPC FP8 AITER GEMM wrapper with packing, per-channel detection and numerical tests.
- Write PTPC GEMM out= via aiter's gemm_a8w8_bpreshuffle_out (ROCm/aiter#6437), or copy on builds lacking it.
Integrate GEMM coverage with the standard op-test contract
op_tests/test_gemm_a8w8_bpreshuffle_out.py:8
This new op_tests/test_*.py suite does not follow the repository's op-test contract: it is pytest-only and has no @benchmark/run_perftest candidate loop, checkAllclose result reporting, markdown summary, or main() architecture gate. Consequently the caller-owned path is not exercised by the standard correctness/performance sweep. Integrate this coverage into the existing GEMM op test (or reshape this file to the standard aiter-op-test structure).
Use get_gfx for test-time architecture selection
op_tests/test_gemm_a8w8_bpreshuffle_out.py:25
Tests and build-time architecture gates must use get_gfx(); get_gfx_runtime() is reserved for runtime dispatch. Using the runtime probe here bypasses GPU_ARCHS, so the backend rows selected by the test can disagree with the configured test target. Switch this test helper to the test/build-time architecture helper.
Addressing the "previously missed" items from the Copilot review summaries, all in 043ebf3:
Validate both quantized operand dtypes: XQ must now be fp8, like WQ. New test test_int8_activation_rejected_before_launch.
Validate scale tensors are fp32: see the reply on the inline thread. Scales must be contiguous fp32 with M and N elements.
Use get_gfx() for architecture checks: the per-backend gate now uses get_gfx(), the same function the tuned-table lookup uses.
Standard op-test benchmark flow: the file now has a @benchmark / run_perftest / checkAllclosemain() that compares gemm_a8w8_bpreshuffle and gemm_a8w8_bpreshuffle_out on the Kimi-K3 shared-expert down-projection shapes and prints a markdown summary. It is gated on get_gfx(). The pytest contract cases are unchanged. The benchmark function is named bench_out so pytest does not collect it.
Set XQ.device as current device before launching kernels
aiter/ops/gemm_op_a8w8.py:923
This call does not establish XQ.device as the current device. The CK launcher and the ordinary FlyDSL launcher use the ambient current HIP stream/device, so tensors that are all on (for example) cuda:1 can still launch on cuda:0 when that is current. Run the shared dispatch under an XQ.device guard so the documented same-device contract works on non-current GPUs as well.
On the "previously missed" device-guard item, fixed in d848178: with every operand on cuda:1 and cuda:0 current, the call caused a GPU memory access fault. The existing gemm_a8w8_bpreshuffle faults the same way. The op now raises ValueError unless the operands are on the current device. I chose a check over a torch.cuda.device guard because the guard cost about 1.7 us of host time per call, while the check costs about 0.4 us. SGLang already sets the current device per rank. New test test_non_current_device_rejected_before_launch checks the rejection, that out is untouched, and that the same call succeeds under torch.cuda.device(1). Fixing gemm_a8w8_bpreshuffle itself is left out of this PR.
Add regression test for original no-out op signature
op_tests/test_gemm_a8w8_bpreshuffle_out.py:135
The compile regression that motivated keeping this as a separate op is not covered here: this test compiles only gemm_a8w8_bpreshuffle_out, and there is no repository test asserting that the existing no-out op still compiles or retains its schema. Add a fullgraph=True inductor regression test that invokes gemm_a8w8_bpreshuffle with its original signature, since the PR description notes that this exact caller path broke with the earlier design.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The architecture guard needs correction, and multi-backend buffer writes and compiled mutation behavior warrant human confirmation of runtime validation.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Multi-backend GPU execution and compiler mutation behavior need final human review because the reported runtime results were not independently validated.
Two follow-ups in this push. test_allocating_op_still_compiles_under_inductor now compiles gemm_a8w8_bpreshuffle with inductor (fullgraph=True) and checks it against eager, so the no-out path that motivated a separate op is covered. The arch check in gemm_a8w8_bpreshuffle_out now runs after the argument checks, so a CPU-tensor call on a host with no GPU gets the documented ValueError. The earlier gfx1250 items are covered by rejecting every arch other than gfx942/gfx950.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Human assessment is needed because the standard CI runner executes only the benchmark, skipping schema, validation, aliasing, empty-output, and torch.compile contract tests.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The mutable caller-owned-buffer op spans CK, CKTile, and FlyDSL, requiring GPU validation of correctness, torch.compile, and device/stream behavior on gfx942/gfx950 that this source-only review could not independently perform.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
GPU execution is unavailable, leaving the new mutating op’s caller-owned writes, torch.compile behavior, and non-current-device stream handling unverified on supported ROCm hardware.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Caller-owned GPU writes and torch.compile mutation semantics across CK, CKTile, and FlyDSL require runtime validation on gfx942 and gfx950 that this source-only review cannot independently perform.
No open issues 🎉
Review effort: Balanced
yuychang
changed the title
Add gemm_a8w8_bpreshuffle_out for caller-owned bf16 outputs
[Feature] Add gemm_a8w8_bpreshuffle_out for caller-owned bf16 outputs
Oct 28, 2026
- Add gemm_a8w8_bpreshuffle_out(out, XQ, WQ, x_scale, w_scale): writes into out, returns None, mutates only out.
- Share dispatch with gemm_a8w8_bpreshuffle via _gemm_a8w8_bpreshuffle_into, so both ops pick the same kernel.
- Keep gemm_a8w8_bpreshuffle's signature and schema; an optional out= on it broke inductor for its no-out callers.
- Accept only a contiguous, 16-byte aligned bf16 [M, N] out; FlyDSL 8-wave kernels write bf16 only.
- Reject a column-slice out that CK/CKTile would overrun, and an odd bf16 offset that faults CK split-K.
- Require 2-D fp8 XQ/WQ: dispatch reads K from shape[-1] but the CK launcher reads size(1).
- Require fp32 scales with M/N elements, every operand on one CUDA/HIP device, and WQ K >= XQ K.
- Raise ValueError for every bad argument before any kernel launch, so out is left untouched.
- Accept only gfx942 and gfx950; check the arch after the arguments so a GPU-less CPU call still gets ValueError.
- Copy strided XQ/WQ/scales to dense; dense inputs pass through without a copy.
- Reject an out that overlaps a dense input by byte range; the schema tells torch.compile only out is written.
- Return early for an empty out: tuned rows exist for M=0, but no launcher accepts an empty grid.
- Run under torch.cuda.device when the operands are not on the current device; the launchers use its stream.
- Check out is bit-identical to gemm_a8w8_bpreshuffle on CK, CK split-K, CKTile, FlyDSL, FlyDSL split-K and 8-wave.
- Fail, not skip, when a per-backend gfx950 256-CU row no longer selects its backend, so coverage cannot drop.
- Cover the untuned and padded-K default-kernel paths, asserting those shapes stay untuned.
- Check the schema marks only out as written and returns nothing.
- Run it under torch.compile (aot_eager and inductor) and keep gemm_a8w8_bpreshuffle compiling under inductor.
- Cover a dense view into a larger buffer, leaving the bytes before it untouched.
- Check bad out, mixed devices, all-CPU operands, unsupported arch, WQ K < XQ K, non-2-D, int8 XQ and bad scales.
- Assert every rejected call raises before launch and leaves the output storage unchanged.
- Reject an out overlapping each input; strided inputs that alias out still match, as the kernel reads a copy.
- Cover operands on a non-current device (two GPUs) and empty outputs, including an M=0 row that selects FlyDSL.
- Gate on get_gfx_runtime, which the op dispatches with, and skip the module outside gfx942/gfx950.
…lus a copy
- Add a @benchmark bench_out and main() comparing bpreshuffle, bpreshuffle+copy and bpreshuffle_out per shape.
- bpreshuffle+copy is what a caller without the out op pays, so the table shows the saved copy kernel time.
- Write out as a dense view past the MoE latent in a flat buffer, matching SGLang's Kimi-K3 [latent | shared] layout.
- Check each candidate against an fp32 torch reference with checkAllclose and log a markdown summary table.
- Count scale bytes and use out.element_size() in the TB/s byte total.
- Take shapes with the standard -s/--mnk M,N,K flag; default is the M token sweep crossed with five (N, K) weights.
- Include the Kimi-K3 TP=8 shared-expert down projection (N=7168, K=768) first in the default sweep.
…#6437)
* feat: add gemm_a8w8_bpreshuffle_out for caller-owned bf16 outputs
- Add gemm_a8w8_bpreshuffle_out(out, XQ, WQ, x_scale, w_scale): writes into out, returns None, mutates only out.
- Share dispatch with gemm_a8w8_bpreshuffle via _gemm_a8w8_bpreshuffle_into, so both ops pick the same kernel.
- Keep gemm_a8w8_bpreshuffle's signature and schema; an optional out= on it broke inductor for its no-out callers.
- Accept only a contiguous, 16-byte aligned bf16 [M, N] out; FlyDSL 8-wave kernels write bf16 only.
- Reject a column-slice out that CK/CKTile would overrun, and an odd bf16 offset that faults CK split-K.
- Require 2-D fp8 XQ/WQ: dispatch reads K from shape[-1] but the CK launcher reads size(1).
- Require fp32 scales with M/N elements, every operand on one CUDA/HIP device, and WQ K >= XQ K.
- Raise ValueError for every bad argument before any kernel launch, so out is left untouched.
- Accept only gfx942 and gfx950; check the arch after the arguments so a GPU-less CPU call still gets ValueError.
- Copy strided XQ/WQ/scales to dense; dense inputs pass through without a copy.
- Reject an out that overlaps a dense input by byte range; the schema tells torch.compile only out is written.
- Return early for an empty out: tuned rows exist for M=0, but no launcher accepts an empty grid.
- Run under torch.cuda.device when the operands are not on the current device; the launchers use its stream.
* test: cover the gemm_a8w8_bpreshuffle_out contract
- Check out is bit-identical to gemm_a8w8_bpreshuffle on CK, CK split-K, CKTile, FlyDSL, FlyDSL split-K and 8-wave.
- Fail, not skip, when a per-backend gfx950 256-CU row no longer selects its backend, so coverage cannot drop.
- Cover the untuned and padded-K default-kernel paths, asserting those shapes stay untuned.
- Check the schema marks only out as written and returns nothing.
- Run it under torch.compile (aot_eager and inductor) and keep gemm_a8w8_bpreshuffle compiling under inductor.
- Cover a dense view into a larger buffer, leaving the bytes before it untouched.
- Check bad out, mixed devices, all-CPU operands, unsupported arch, WQ K < XQ K, non-2-D, int8 XQ and bad scales.
- Assert every rejected call raises before launch and leaves the output storage unchanged.
- Reject an out overlapping each input; strided inputs that alias out still match, as the kernel reads a copy.
- Cover operands on a non-current device (two GPUs) and empty outputs, including an M=0 row that selects FlyDSL.
- Gate on get_gfx_runtime, which the op dispatches with, and skip the module outside gfx942/gfx950.
* test: benchmark gemm_a8w8_bpreshuffle_out against the allocating op plus a copy
- Add a @benchmark bench_out and main() comparing bpreshuffle, bpreshuffle+copy and bpreshuffle_out per shape.
- bpreshuffle+copy is what a caller without the out op pays, so the table shows the saved copy kernel time.
- Write out as a dense view past the MoE latent in a flat buffer, matching SGLang's Kimi-K3 [latent | shared] layout.
- Check each candidate against an fp32 torch reference with checkAllclose and log a markdown summary table.
- Count scale bytes and use out.element_size() in the TB/s byte total.
- Take shapes with the standard -s/--mnk M,N,K flag; default is the M token sweep crossed with five (N, K) weights.
- Include the Kimi-K3 TP=8 shared-expert down projection (N=7168, K=768) first in the default sweep.
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
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.
Motivation
SGLang's Kimi-K3 ROCm path writes its PTPC FP8 shared-expert down-projection straight into a preallocated buffer (
shared_output = buf[offset:].view(M, hidden)), which is later all-reduced together with the MoE latent.gemm_a8w8_bpreshufflealways allocates its output, so today that result has to be copied. This PR adds an op that writes into a caller-owned buffer.Technical Details
gemm_a8w8_bpreshuffle_out(out, XQ, WQ, x_scale, w_scale) -> None. It writes intooutand picks the same kernel asgemm_a8w8_bpreshuffle, because both share one dispatch helper (_gemm_a8w8_bpreshuffle_into).torch.compile:outas mutated (mutates_args=["out"]) and returns nothing. Its schema is(Tensor(a0!) out, Tensor XQ, Tensor WQ, Tensor x_scale, Tensor w_scale) -> ().out=ongemm_a8w8_bpreshuffle: narrowing that op'smutates_argswith an optionaloutbroke inductor for its existing no-outcallers (getitem is not an OpOverload).ValueErrorbefore any launch, because no kernel supports them: non-2-D or non-fp8XQ/WQ, non-fp32 scales or scales without M/N elements, operands that are not all on one CUDA/HIP device,WQ K < XQ K, and anoutthat is not a contiguous bf16[M, N]tensor.outwould fail at launch on those shapes.outonly: CK and CKTile write a dense row-major[M, N], so a stridedout(for example a column slice of a wider buffer) would be written past its bounds.outmust start 16-byte aligned. A dense view into a larger buffer is fine at an aligned offset; at an odd bf16 offset the CK split-K kernel faulted. The rule applies to every backend so that a valid buffer never depends on which tuned row is selected.torch.emptystorage and SGLang'sbuf[M * latent:]view are always aligned.NotImplementedErrorbefore launch, because the shared dispatch would reach backends this op was never validated on (the gfx1250 WMMA path and the CK MFMA fallback on RDNA); usegemm_a8w8_bpreshufflethere. This check runs after the argument checks, so invalid arguments always getValueError.XQ,WQor scale is copied to a dense tensor first, because CK and CKTile read only row strides and raw scale pointers. Dense inputs pass through without a copy.torch.cuda.device(XQ.device), because the launchers use the current device's stream. On the normal path it adds no guard.out(M or N = 0) returns after validation. M=0 matches tuned FlyDSL rows whose launch would otherwise fail.outmust not overlap any input. A K-paddedWQkeepsgemm_a8w8_bpreshuffle's padded-K handling unchanged.gemm_a8w8_bpreshuffleis unchanged: same signature, same custom-op schema, same results.Test Plan
New file
op_tests/test_gemm_a8w8_bpreshuffle_out.py:outas written and has no return value.outis bit-identical togemm_a8w8_bpreshuffleon gfx950 tuned rows that select each backend: CK, CK split-K, CKTile, FlyDSL, FlyDSL split-K and FlyDSL 8-wave. Each case asserts that its row still selects that backend and fails otherwise, so coverage cannot silently drop when tables change.WQ K > XQ K). Both assert they have no tuned row.torch.compile(fullgraph=True)withaot_eagerand with inductor (caches disabled):outis filled and matches eager.out, a column-slice or transposedout,WQ K < XQ K,WQ,x_scaleorw_scaleon another device, an all-CPU call, a rank-3XQorWQ, an int8XQ, and a bf16 or short scale each raiseValueErrorand leave the buffer untouched.XQ, a row-stridedWQ, strided scales and a non-contiguous[1, M]scale all give the same result as dense inputs.cuda:1called fromcuda:0give the right result and leavecuda:0current.out(M=0 on a FlyDSL row and an untuned row, and N=0) is a no-op and leaves the device usable.python op_tests/test_gemm_a8w8_bpreshuffle_out.py) benchmarks both ops over a spread of N/K shapes, including the Kimi-K3 TP=8 shared-expert down projection (N=7168, K=768), writingoutas a dense view into a caller-owned[latent | shared]buffer as SGLang does, and prints a markdown summary.op_tests/test_gemm_a8w8_bpreshuffle_pad_k.pystill passes, andgemm_a8w8_bpreshufflekeepsmain's schema and still compiles under inductor.outat an odd bf16 offset raisesValueErrorand leaves the buffer untouched.NotImplementedErrorbefore launch, and an all-CPU call raisesValueErroreven when the arch probe fails.gemm_a8w8_bpreshufflestill compiles under inductor (fullgraph=True) and matches eager.Test Result
MI355X (gfx950), ROCm 7.2, torch 2.11.0+rocm7.2:
op_tests/test_gemm_a8w8_bpreshuffle_out.py: 53 passed, 1 skipped with one GPU visible (the skipped test needs two GPUs and passes with two). With no GPU visible, all skip.op_tests/test_gemm_a8w8_bpreshuffle_pad_k.py: 17 passed, 8 skipped (gfx942 tuned-table cases).gemm_a8w8_bpreshufflefollowed by the copy into the caller's buffer (run_perftest, FlyDSL rows at every shape below, error 0 everywhere). N=7168 K=768 is the Kimi-K3 TP=8 shared-expert down projection:bpreshufflebpreshuffle+ copybpreshuffle_out_out(22.6 us forbpreshufflealone); K=2048: 24.9 vs 22.3 us (21.5 us alone). An earlier run of the same comparison read 33.0 -> 28.6 us host and 5.63 -> 3.92 us GPU at K=2048; the re-run above replaces it.ptpc_fp8_aiter_hip.py,run(..., out=)), which was closed without merging; this op has no in-tree caller until that change is resubmitted.main()over M = 1 to 4096 and N/K = 4096/4096, 7168/2048, 2112/7168, 8192/1024 and 7168/768 selects CK, CKTile, FlyDSL and untuned rows. Error is 0 for both ops on every row, and kernel times match within noise.Submission Checklist
🤖 Generated with Claude Code