Skip to content

[MoE] Expose zero-copy MegaMoE workspace output view - #4341

Merged
mhoqueanik merged 3 commits into
flashinfer-ai:mainfrom
foraxe:codex/zero-copy-output
Aug 18, 2026
Merged

mhoqueanik merged 3 commits into
flashinfer-ai:mainfrom
foraxe:codex/zero-copy-output

Conversation

@foraxe

@foraxe foraxe commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

This draft exposes a backward-compatible zero-copy output path for the
FlashInfer MegaMoE layer.

The new return_workspace_view=False argument preserves the existing owned
output behavior by default. When enabled and supported by the selected
MegaMoE backend, forward returns the layer's workspace output view instead
of copying the result into a new tensor. Capability gating keeps unsupported
backends on the existing path. Backend support is expressed as the
supports_output_view capability property on the kernel contract.

The patch also keeps the workspace-view contract explicit across the NVFP4 and
MXFP8 CuTeDSL MegaMoE backends and updates the relevant CUDA-graph and
multi-rank tests.

Before and after

Before (default behavior):

  MoEEpTensors
       |
       v
  MegaMoE forward(...)
       |
       v
  workspace output --copy--> caller-owned output tensor

After (opt-in view):

  MoEEpTensors
       |
       v
  MegaMoE forward(..., return_workspace_view=True)
       |
       v
  workspace output view ------> caller uses workspace-backed tensor

The default remains the copied, caller-owned output. The new path makes the
workspace view available only when the backend advertises the capability.

Motivation

The SGLang MegaMoE integration currently needs the computed output to remain
in the FlashInfer workspace. Returning that view removes a large per-layer
output materialization while retaining the old API behavior for callers that
need an owned tensor.

Validation

  • tests/moe_ep/test_mega_cuda_graph.py: 8 passed.
  • The targeted output-view test passes: 1 passed, 7 deselected.
  • Four-GPU NVFP4 multi-rank output-view coverage passes with
    NVSHMEM_DISABLE_CUDA_VMM=1.
  • The corresponding SGLang adapter test passes 3 cases when paired with this
    API.

The upstream moe_ep_benchmark harness was also run from the
vllm_repro_8_gpu_v2 branch with the FlashInfer MegaMoE section on four GPUs
(GPUS=4, DEVS=0,1,2,3). This is a GB200 smoke/regression run, not an
8-GPU reference result:

backend tokens/rank hidden/intermediate experts/top-k p50 latency throughput
NVFP4 CuTeDSL 32 7168 / 2048 256 / 8 358.4 us 357174.7
MXFP8 CuTeDSL 32 7168 / 2048 256 / 8 619.9 us 206472.9

The harness reported accuracy-loss fields of 23.201 percent for NVFP4 and
6.371 percent for MXFP8 against its BF16 dense reference. These are retained
as harness output and are not used as a model accuracy claim here.

This is the FlashInfer side of the paired SGLang integration. The SGLang
draft depends on this API and is posted separately.

Summary by CodeRabbit

  • New Features

    • Added an option to return workspace-backed output views, reducing unnecessary output copying when supported.
    • Added capability detection so unsupported configurations provide a clear error while preserving existing default behavior.
    • Enabled output views for supported FP8 and FP4 workflows.
  • Tests

    • Added coverage for output-view shapes, workspace aliasing, repeatability, zero-token inputs, and multi-size execution.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 187d2f32-e340-4f83-8523-f7aa43516507

📥 Commits

Reviewing files that changed from the base of the PR and between 6f1d4f5 and 982cb6a.

📒 Files selected for processing (1)
  • flashinfer/moe_ep/modes/mega_layer.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • flashinfer/moe_ep/modes/mega_layer.py

Included review availability: Your plan includes up to 8 reviews per rolling hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The PR adds backend capability declarations for workspace-backed outputs, exposes return_workspace_view on MoEEpMegaLayer.forward, and adds CUDA graph and multi-rank validation.

Changes

Workspace-backed output views

Layer / File(s) Summary
Output-view capability contract
flashinfer/moe_ep/core/kernel/base.py, flashinfer/moe_ep/backends/mega/kernel/sm100/*/backend.py
MegaKernelBackend defaults supports_output_view to False. The MXFP8 and NVFP4 backends enable the capability.
Workspace-view forward path
flashinfer/moe_ep/modes/mega_layer.py
MoEEpMegaLayer exposes backend support and accepts return_workspace_view. Unsupported requests raise MoEEpConfigError; supported requests omit owned output allocation.
Output-view validation
tests/moe_ep/test_mega_cuda_graph.py, tests/moe_ep/test_moe_ep_nvfp4_cutedsl_mega_multirank.py
Tests cover empty and varying token counts, workspace aliasing, repeated forwards, CUDA graphs, and copied-output equality.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 982cb

The change preserves the existing copied-output behavior by default and adds an opt-in workspace-backed view with backend capability gating; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant MoEEpMegaLayer
  participant MegaKernelBackend
  participant Workspace
  Caller->>MoEEpMegaLayer: forward(return_workspace_view=True)
  MoEEpMegaLayer->>MegaKernelBackend: check supports_output_view
  MoEEpMegaLayer->>Workspace: omit owned output allocation
  Workspace-->>MoEEpMegaLayer: return workspace-backed output view
  MoEEpMegaLayer-->>Caller: return output view
Loading

Suggested reviewers: anerudhan, aleozlx, yzh119

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: exposing a zero-copy MegaMoE workspace output view.
Description check ✅ Passed The description clearly explains the API, backward-compatible behavior, backend support, motivation, and validation results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mhoqueanik mhoqueanik left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

overall looks like a good suggestion. Can you briefly run the microbench from here to make sure the PR doesn't break the upstream paths?

Comment thread flashinfer/moe_ep/backends/mega/kernel/mxfp8_cutedsl/backend.py Outdated
Comment thread flashinfer/moe_ep/backends/mega/kernel/nvfp4_cutedsl/backend.py Outdated
Comment thread flashinfer/moe_ep/core/kernel/base.py Outdated
Comment thread flashinfer/moe_ep/modes/mega_layer.py Outdated
@foraxe

foraxe commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

overall looks like a good suggestion. Can you briefly run the microbench from here to make sure the PR doesn't break the upstream paths?

@mhoqueanik Yes — I ran the vllm_repro_8_gpu_v2 harness on 4×GB200 (GPUS=4, DEVS=0,1,2,3) as a smoke/regression check.

Both upstream MegaMoE paths completed successfully:

  • NVFP4 CuTeDSL: 358.4 µs p50, 357174.7 throughput
  • MXFP8 CuTeDSL: 619.9 µs p50, 206472.9 throughput

I also reran the focused CUDA-graph, output-view, and 4-GPU multi-rank tests. The detailed results are included in the PR description.

This is not the original 8-GPU reference configuration, but I did not observe any failure or regression in the upstream paths.

@foraxe
foraxe marked this pull request as ready for review August 6, 2026 01:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/moe_ep/test_mega_cuda_graph.py (1)

247-272: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add MXFP8 coverage for the public output-view API.

The feature contract enables return_workspace_view=True for NVFP4 and MXFP8. This test initializes only "nvfp4". Add an MXFP8 case that checks support, shape, workspace aliasing, repeatability, and equality with the copied output.

Confidence: High.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/moe_ep/test_mega_cuda_graph.py` around lines 247 - 272, The test
test_mega_layer_forward_output_view_public_api currently covers only NVFP4;
extend it to run the same assertions for MXFP8 as well. Parameterize or
otherwise repeat the case for both layer types, verifying supports_output_view,
output shape, workspace aliasing for non-empty batches, repeatability, and
equality with the copied output while preserving setup and cleanup for each
case.
flashinfer/moe_ep/modes/mega_layer.py (1)

191-196: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Document the first-call precondition for knobs="auto".

If return_workspace_view=True is the first forward, both supported CuTeDSL backends reject compute(output=None) while autotuning is pending. State that warmup() or one owned-output forward must run first, or reject this combination with MoEEpConfigError before staging.

Confidence: high.

As per coding guidelines, keep documentation synchronized with code changes, including documented error handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flashinfer/moe_ep/modes/mega_layer.py` around lines 191 - 196, Update the
MegaMoE forward API documentation near the return_workspace_view description to
state that knobs="auto" requires warmup() or one prior owned-output forward
before return_workspace_view=True; alternatively, add an early MoEEpConfigError
validation for this combination before staging. Keep the documented behavior
synchronized with the implemented precondition and backend constraints.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@flashinfer/moe_ep/modes/mega_layer.py`:
- Around line 191-196: Update the MegaMoE forward API documentation near the
return_workspace_view description to state that knobs="auto" requires warmup()
or one prior owned-output forward before return_workspace_view=True;
alternatively, add an early MoEEpConfigError validation for this combination
before staging. Keep the documented behavior synchronized with the implemented
precondition and backend constraints.

In `@tests/moe_ep/test_mega_cuda_graph.py`:
- Around line 247-272: The test test_mega_layer_forward_output_view_public_api
currently covers only NVFP4; extend it to run the same assertions for MXFP8 as
well. Parameterize or otherwise repeat the case for both layer types, verifying
supports_output_view, output shape, workspace aliasing for non-empty batches,
repeatability, and equality with the copied output while preserving setup and
cleanup for each case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f806aec2-1d7e-440d-8c80-75399bd24dc5

📥 Commits

Reviewing files that changed from the base of the PR and between 4967994 and adbcaab.

📒 Files selected for processing (6)
  • flashinfer/moe_ep/backends/mega/kernel/mxfp8_cutedsl/backend.py
  • flashinfer/moe_ep/backends/mega/kernel/nvfp4_cutedsl/backend.py
  • flashinfer/moe_ep/core/kernel/base.py
  • flashinfer/moe_ep/modes/mega_layer.py
  • tests/moe_ep/test_mega_cuda_graph.py
  • tests/moe_ep/test_moe_ep_nvfp4_cutedsl_mega_multirank.py

@mhoqueanik

Copy link
Copy Markdown
Collaborator

Hi @foraxe, there is a minor conflict (it was introduced after the push style sm90 megamoe). Could you resolve it?

@foraxe
foraxe force-pushed the codex/zero-copy-output branch from adbcaab to 6f1d4f5 Compare August 16, 2026 07:38
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@foraxe

foraxe commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

Hi @mhoqueanik Thanks for pointing this out; I rebased onto the latest main, resolved the SM90 MegaMoE conflict, and verified the relevant tests pass.

@mhoqueanik

Copy link
Copy Markdown
Collaborator

Hi @foraxe — while preparing the moe_ep merge queue we ran your PR merged with current main through the full moe_ep suite and hit one unit failure:

tests/moe_ep/test_mega_layer_validation.py::test_mega_layer_allocates_output_before_staging_round

Making the owned-output allocation conditional moved the torch.empty to after stage_inputs(), which breaks the allocate-before-staging contract that test enforces (from #4069 — allocator work between stage and compute can sync the device mid-round). It didn't surface in this PR's checks because CI hasn't actually run yet (it's gated on the @flashinfer-bot run authorization).

The fix keeps your conditional and just restores the ordering — we validated exactly this patch (unit + Blackwell mega multirank green on 8×B200):

--- a/flashinfer/moe_ep/modes/mega_layer.py
+++ b/flashinfer/moe_ep/modes/mega_layer.py
@@ -220,12 +220,9 @@ class MoEEpMegaLayer(nn.Module):
 
         workspace = self._ensure_workspace()
 
-        self._kernel.stage_inputs(
-            t,
-            workspace,
-            quantize_input=quantize_input,
-        )
-
+        # Owned-output allocation must stay ahead of the staging round (see
+        # test_mega_layer_allocates_output_before_staging_round): allocator
+        # work between stage and compute can sync the device mid-round.
         y = None
         if not return_workspace_view:
             y = torch.empty(
@@ -234,6 +231,13 @@ class MoEEpMegaLayer(nn.Module):
                 dtype=torch.bfloat16,
                 device=t.hidden_states.device,
             )
+
+        self._kernel.stage_inputs(
+            t,
+            workspace,
+            quantize_input=quantize_input,
+        )
+
         return self._kernel.compute(
             workspace,
             self._transformed,

No rebase needed — the branch merges cleanly with main; just this one commit, then a fresh @flashinfer-bot run. Also FYI: the incoming SM100 BF16 backend (#4386, merging right after this) intentionally does not opt into supports_output_view yet, so no action needed on your side for it.

@foraxe

foraxe commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

@flashinfer-bot run

@foraxe

foraxe commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @mhoqueanik, I've applied the fix in 982cb6a by moving output allocation before stage_inputs to satisfy the allocate-before-staging contract;
I also verified locally that test_mega_layer_allocates_output_before_staging_round is now passing.
It seems @flashinfer-bot run will need authorization.

@mhoqueanik

Copy link
Copy Markdown
Collaborator

@flashinfer-bot run

@Anerudhan

Copy link
Copy Markdown
Collaborator

/bot run tests/moe_ep

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !1243 has been created, and the CI pipeline #63146462 is currently running. I'll report back once the pipeline job completes.

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[FAILED] Pipeline #63146462 — 27/30 executed test jobs passed

Compared with nightly #63077496.

Unit Tests

GPU CUDA 12.9 CUDA 13.0 cu129--0 cu129--1 cu129--2 cu129--3 cu130--0 cu130--1 cu130--2 cu130--3 Notes
5090 ✅ Pass ✅ Pass ✅ Pass ✅ Pass ✅ Pass ✅ Pass ✅ Pass ✅ Pass
B300 ✅ Pass ❌ New New: tests.moe_ep.test_fused_quant_stage (11 failures; CUDA 13.0)
New: tests.moe_ep.test_nvfp4_cutedsl_kernel_vs_reference (3 failures; CUDA 13.0)
New: tests.moe_ep.test_compute_bridge (1 failure; CUDA 13.0)
… and 5 more
GB200 ✅ Pass ❌ New New: tests.moe_ep.test_fused_quant_stage (11 failures; CUDA 13.0)
New: tests.moe_ep.test_nvfp4_cutedsl_kernel_vs_reference (3 failures; CUDA 13.0)
New: tests.moe_ep.test_compute_bridge (1 failure; CUDA 13.0)
… and 5 more
GB300 ✅ Pass ❔ Unknown Unknown: script failed before producing a JUnit report (1 job; CUDA 13.0)
H100 ✅ Pass ✅ Pass ✅ Pass ✅ Pass ✅ Pass ✅ Pass ✅ Pass ✅ Pass
RTX Pro 6000 Blackwell ✅ Pass ✅ Pass

✅ Pass · 🟡 Old failure · ❌ New failure · ⏱ Test timeout · ⚠️ Infrastructure · ❔ Unknown or unclassified · — Not run

Multi-GPU and Multi-Node Tests — 6/6 passed

GPU CUDA 12.9 CUDA 13.0 cu129--0 cu129--1 cu129--2 cu129--3 cu130--0 cu130--1 cu130--2 cu130--3 Notes
B300 (multi-GPU) ✅ Pass ✅ Pass
GB200 (multi-node) ✅ Pass ✅ Pass
GB300 (multi-node) ✅ Pass ✅ Pass
Failure details

PR-related regressions

  • tests.moe_ep.test_mega_cuda_graph — 14 failures on B300 / CUDA 13.0, GB200 / CUDA 13.0
    • cutlass.base_dsl.common.DSLRuntimeError: #x1B[91m#x1B[1merror[INTERNAL]:#x1B[0m The compiler hit an internal DSL problem while compiling your code. #x1B[94mnote:#x1B[0m This is…
  • tests.moe_ep.test_moe_ep_nvfp4_cutedsl_mega_multirank — 2 failures on B300 / CUDA 13.0, GB200 / CUDA 13.0
    • cutlass.base_dsl.common.DSLRuntimeError: #x1B[91m#x1B[1merror[INTERNAL]:#x1B[0m The compiler hit an internal DSL problem while compiling your code. #x1B[94mnote:#x1B[0m This is…

New relative to nightly (attribution uncertain)

  • tests.moe_ep.test_fused_quant_stage — 22 failures on B300 / CUDA 13.0, GB200 / CUDA 13.0
    • cutlass.base_dsl.common.DSLRuntimeError: #x1B[91m#x1B[1merror[INTERNAL]:#x1B[0m The compiler hit an internal DSL problem while compiling your code. #x1B[94mnote:#x1B[0m This is…
  • tests.moe_ep.test_nvfp4_cutedsl_kernel_vs_reference — 6 failures on B300 / CUDA 13.0, GB200 / CUDA 13.0
    • cutlass.base_dsl.common.DSLRuntimeError: #x1B[91m#x1B[1merror[INTERNAL]:#x1B[0m The compiler hit an internal DSL problem while compiling your code. #x1B[94mnote:#x1B[0m This is…
  • tests.moe_ep.test_compute_bridge — 2 failures on B300 / CUDA 13.0, GB200 / CUDA 13.0
    • cutlass.base_dsl.common.DSLRuntimeError: #x1B[91m#x1B[1merror[INTERNAL]:#x1B[0m The compiler hit an internal DSL problem while compiling your code. #x1B[94mnote:#x1B[0m This is…
  • tests.moe_ep.test_deep_gemm_mega_kernel_vs_reference — 2 failures on B300 / CUDA 13.0, GB200 / CUDA 13.0
    • cutlass.base_dsl.common.DSLRuntimeError: #x1B[91m#x1B[1merror[INTERNAL]:#x1B[0m The compiler hit an internal DSL problem while compiling your code. #x1B[94mnote:#x1B[0m This is…
  • tests.moe_ep.test_mxfp8_cutedsl_preprocess_vs_reference — 2 failures on B300 / CUDA 13.0, GB200 / CUDA 13.0
    • cutlass.base_dsl.common.DSLRuntimeError: #x1B[91m#x1B[1merror[INTERNAL]:#x1B[0m The compiler hit an internal DSL problem while compiling your code. #x1B[94mnote:#x1B[0m This is…
  • tests.moe_ep.test_workspace_pool — 2 failures on B300 / CUDA 13.0, GB200 / CUDA 13.0
    • cutlass.base_dsl.common.DSLRuntimeError: #x1B[91m#x1B[1merror[INTERNAL]:#x1B[0m The compiler hit an internal DSL problem while compiling your code. #x1B[94mnote:#x1B[0m This is…

Timeouts, infrastructure, or incomplete jobs

@foraxe

foraxe commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

We reran the unit tests in local env. (on GB200, Driver Version: 580.105.08, CUDA Version: 13.1)

Ran with:

  CUDA_VISIBLE_DEVICES=0 FLASHINFER_DISABLE_VERSION_CHECK=1 /workspace/sglang_flashinfer/work/.venv-main/bin/python -m pytest -q <each file> --maxfail=1

Local results:

  • test_mega_cuda_graph.py: 8 passed
  • test_fused_quant_stage.py: 11 passed
  • test_nvfp4_cutedsl_kernel_vs_reference.py: 4 passed
  • test_compute_bridge.py: 8 passed
  • test_deep_gemm_mega_kernel_vs_reference.py: 1 passed
  • test_mxfp8_cutedsl_preprocess_vs_reference.py: 3 passed
  • test_workspace_pool.py: 8 passed
  • test_moe_ep_nvfp4_cutedsl_mega_multirank.py: 7 passed, 14 skipped

So in this local env, those DSLRuntimeError failures do not reproduce. They look environment/branch-variant specific to the CI stack (B300/GB200 + CUDA 13.0), not currently reproducible on this local setup.

We also tested in sgl-project/sglang#31470 (comment)

The new failures in bot pipeline seems like a broad MoE cutlass/DSL compiler regression on CUDA 13.0 + B300/GB200 (same DSLRuntimeError across many unrelated tests), not a narrow PR regression from this output-view/output-order fix.

@mhoqueanik

Copy link
Copy Markdown
Collaborator

@foraxe Agreed! I am looking into this

@mhoqueanik
mhoqueanik merged commit 7015afd into flashinfer-ai:main Aug 18, 2026
37 of 84 checks passed
jefby pushed a commit to jefby/flashinfer that referenced this pull request Aug 19, 2026
)

## Summary

This draft exposes a backward-compatible zero-copy output path for the
FlashInfer MegaMoE layer.

The new `return_workspace_view=False` argument preserves the existing
owned
output behavior by default. When enabled and supported by the selected
MegaMoE backend, `forward` returns the layer's workspace output view
instead
of copying the result into a new tensor. Capability gating keeps
unsupported
backends on the existing path. Backend support is expressed as the
`supports_output_view` capability property on the kernel contract.

The patch also keeps the workspace-view contract explicit across the
NVFP4 and
MXFP8 CuTeDSL MegaMoE backends and updates the relevant CUDA-graph and
multi-rank tests.

## Before and after

```text
Before (default behavior):

  MoEEpTensors
       |
       v
  MegaMoE forward(...)
       |
       v
  workspace output --copy--> caller-owned output tensor

After (opt-in view):

  MoEEpTensors
       |
       v
  MegaMoE forward(..., return_workspace_view=True)
       |
       v
  workspace output view ------> caller uses workspace-backed tensor
```

The default remains the copied, caller-owned output. The new path makes
the
workspace view available only when the backend advertises the
capability.

## Motivation

The SGLang MegaMoE integration currently needs the computed output to
remain
in the FlashInfer workspace. Returning that view removes a large
per-layer
output materialization while retaining the old API behavior for callers
that
need an owned tensor.

## Validation

- `tests/moe_ep/test_mega_cuda_graph.py`: 8 passed.
- The targeted output-view test passes: 1 passed, 7 deselected.
- Four-GPU NVFP4 multi-rank output-view coverage passes with
  `NVSHMEM_DISABLE_CUDA_VMM=1`.
- The corresponding SGLang adapter test passes 3 cases when paired with
this
  API.

The upstream `moe_ep_benchmark` harness was also run from the
`vllm_repro_8_gpu_v2` branch with the FlashInfer MegaMoE section on four
GPUs
(`GPUS=4`, `DEVS=0,1,2,3`). This is a GB200 smoke/regression run, not an
8-GPU reference result:

| backend | tokens/rank | hidden/intermediate | experts/top-k | p50
latency | throughput |
| --- | ---: | ---: | ---: | ---: | ---: |
| NVFP4 CuTeDSL | 32 | 7168 / 2048 | 256 / 8 | 358.4 us | 357174.7 |
| MXFP8 CuTeDSL | 32 | 7168 / 2048 | 256 / 8 | 619.9 us | 206472.9 |

The harness reported accuracy-loss fields of 23.201 percent for NVFP4
and
6.371 percent for MXFP8 against its BF16 dense reference. These are
retained
as harness output and are not used as a model accuracy claim here.

This is the FlashInfer side of the paired SGLang integration. The SGLang
draft depends on this API and is posted separately.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added an option to return workspace-backed output views, reducing
unnecessary output copying when supported.
* Added capability detection so unsupported configurations provide a
clear error while preserving existing default behavior.
  * Enabled output views for supported FP8 and FP4 workflows.

* **Tests**
* Added coverage for output-view shapes, workspace aliasing,
repeatability, zero-token inputs, and multi-size execution.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
aleozlx added a commit that referenced this pull request Sep 5, 2026
…es (#4956)

<!-- .github/pull_request_template.md -->

## 📌 Description

`.github/workflows/ci-bot-commands.yml` decides whether a PR comment is
a bot command with
unanchored substring matches:

```
# BOT below stands for the literal bot handle, elided so this PR does not trigger itself.
if: github.event.issue.pull_request && contains(github.event.comment.body, 'BOT')
...
elif echo "$COMMENT_BODY" | grep -qi "BOT run"; then
```

Neither is anchored, so the phrase matches **anywhere** in a comment
body — inside inline code
spans, fenced blocks, markdown tables, and quoted reply history.
*Writing about* a command runs it.

Each accidental fire re-applies the `run-ci` label, which emits a
`labeled` event, which under
`concurrency: cancel-in-progress` **cancels the in-flight GPU run and
starts a new one**. A run is
~4.5 hours, so each accident is expensive. The known workaround is to
write the handle with a
zero-width entity (`@flashinfer&#8203;-bot`) — a hack no contributor
should need to know.

### The fix

A command counts only when it **starts a line that is not inside a
fenced code block.**

Implemented entirely inside the `Parse command` step, in two stages:

1. `awk` drops fenced code blocks (both ``` and `~~~`, including
indented fences).
2. `grep -iEm1
'^[[:space:]]*@flashinfer&#8203;-bot[[:space:]]+(run|rerun|stop)([[:space:]]|$)'`
takes the first
surviving line that *begins* with the handle. Leading whitespace is
allowed; anything else to the
left — `>` for a quoted reply, `|` for a table cell, a backtick for an
inline span, or prose — is not.

The four existing classifiers then run against that single extracted
line, gaining `^` anchors and a
trailing word boundary. Order (`rerun failed` before `rerun`) is
unchanged.

**Why not the job-level `if:`** — GitHub Actions expressions have no
regex (only
`contains`/`startsWith`/`endsWith`), so the job guard cannot be
anchored. It is left as-is and
re-commented as a cheap pre-filter. This is harmless: a prose comment
now spawns a job that resolves
`command=unknown` and takes no action, since every handler step is gated
on `steps.parse.outputs.command`.

No bot-author guard is included. It would not have prevented any of
these accidents — they came from
**humans writing documentation**, not from the bot. It is worth adding
separately as complementary
hardening, but anchoring is the actual fix.

## 🔍 Related Issues

No tracking issue. The behaviour was found while working on #4880, where
four documentation
comments each cancelled and restarted an in-flight ~4.5 hour GPU run —
but the problem is
repo-wide and predates it (see the replay below, spanning 2025-10-18 →
2026-09-04 across five
PRs). This change is independent of #4880: `git grep` confirms lines 26
and 96-102 of
`ci-bot-commands.yml` are the only consumers of `comment.body` on
`main`.

## 🚀 Pull Request Checklist

Thank you for contributing to FlashInfer! Before we review your pull
request, please make sure the following items are complete.

### ✅ Pre-commit Checks

- [x] I have installed `pre-commit` by running `pip install pre-commit`
(or used your preferred method).
- [x] I have installed the hooks with `pre-commit install`.
- [x] I have run the hooks manually with `pre-commit run --all-files`
and fixed any reported issues.

> If you are unsure about how to set up `pre-commit`, see [the
pre-commit documentation](https://pre-commit.com/).

## 🧪 Tests

- [x] Tests have been added or updated as needed. — see the replay,
corpus and verification below.
- [x] All tests are passing (`unittest`, etc.).

**This file cannot be tested by CI.** `issue_comment` workflows always
load from the default
branch, so zero CI runs on this PR execute the changed file; it takes
effect only once merged.
All verification below was therefore done out-of-band.

### Repo-wide replay

I replayed **every** issue comment in this repository's history through
both matchers — 19,930
comments, of which 725 are handle-bearing PR comments spanning
2025-10-18 → 2026-09-04.

| | old | new |
|---|---|---|
| fires | 689 | 666 |
| suppressed (old fired, new does not) | — | **31** |
| newly honored (old ignored, new fires) | — | **8** |
| reclassified to a different command | — | **0** |

**All 31 suppressions are accidental. Zero legitimate commands are
lost.** By how the phrase was
embedded: 21 inline code span, 4 bare in a prose sentence, 2 table cell,
2 fenced block, 2 blockquote.

8 of the 31 were by users authorized to trigger CI (`aleozlx` ×4,
`Anerudhan`, `mhoqueanik`,
`qsang-nv`, `yongwww`) across 5 PRs (#4880, #4795, #4341, #3471, #2529)
over 7 months — these are the
ones that actually consumed GPU CI, and every one is documentation
prose. **This is a repo-wide
problem, not a #4880 artifact.** The other 23 were by users with only
`read` permission, so the bot
replied "unauthorized" and no CI ever started; the only loss there is a
feedback reaction.

The 4 "bare in prose" cases are the most arguable, e.g. *"Could a
maintainer please approve the
external CI for this PR? @flashinfer​-bot run"* (#4435). I checked all
three authors
(`foraxe`, `DocJlm`, `Archie-wang`): each has only `read` and is not in
`ci-users`, so none of these
started CI under the old code either.

**The change also fixes a latent bug in the other direction.**
`@flashinfer​-bot` + two spaces +
`run` matched *nothing* under the old literal-substring rule. It was
silently ignored 8 times by 4
authorized maintainers (`yzh119` ×3, `yongwww` ×3, `jiahanc`,
`kahyunnam`); 7 of the 8 carry zero
reactions, confirming the handler never fired. Those now work.

### Corpus

33 hand-built cases + the 11 real #4880 comments. Verified three ways:
against an independent Python
model of the pipeline, by executing the shipped step under `bash -e`,
and live in a sandbox repo.

**MUST TRIGGER — all preserved**

| case | body | old | new |
|---|---|---|---|
| bare run | `@flashinfer&#8203;-bot run` | run | run |
| with path | `@flashinfer&#8203;-bot run tests/gemm/test_x.py` | run |
run |
| multiple paths | `@flashinfer&#8203;-bot run tests/a.py tests/b.py` |
run | run |
| leading spaces | `␣␣␣@flashinfer&#8203;-bot run` | run | run |
| leading tab | `⇥@flashinfer&#8203;-bot rerun failed` | rerun-failed |
rerun-failed |
| mixed case | `@FlashInfer&#8203;-Bot RUN` | run | run |
| mixed case rerun | `@FLASHINFER&#8203;-BOT ReRun` | rerun | rerun |
| first line of multi-line | `@flashinfer&#8203;-bot run\n\nKicking off
CI.` | run | run |
| later line of multi-line | `Rebased.\n\n@flashinfer&#8203;-bot run` |
run | run |
| middle line | `Fixed lint.\n@flashinfer&#8203;-bot run
tests/utils/\nThanks!` | run | run |
| rerun | `@flashinfer&#8203;-bot rerun` | rerun | rerun |
| rerun failed | `@flashinfer&#8203;-bot rerun failed` | rerun-failed |
rerun-failed |
| stop | `@flashinfer&#8203;-bot stop` | stop | stop |
| trailing prose | `@flashinfer&#8203;-bot run please` | run | run |
| after a fenced block | a log in a backtick fence, then
`@flashinfer&#8203;-bot run` below it | run | run |
| CRLF line endings | `Rebased.\r\n@flashinfer&#8203;-bot run\r\n` | run
| run |
| trailing whitespace | `@flashinfer&#8203;-bot run␣␣␣` | run | run |
| after a bullet list | list then `@flashinfer&#8203;-bot rerun failed`
| rerun-failed | rerun-failed |
| **double space** | `@flashinfer&#8203;-bot␣␣run` | **unknown** |
**run** |

**MUST NOT TRIGGER — all now suppressed**

| case | body | old | new |
|---|---|---|---|
| inline code span, in prose | ``The command is `@flashinfer&#8203;-bot
run` -- type it on its own line.`` | run | **unknown** |
| inline code span at line start | code span first on the line, then
prose | run | **unknown** |
| fenced block | backtick fence listing the commands | stop |
**unknown** |
| fenced block with language | backtick fence tagged `bash` | run |
**unknown** |
| tilde fence | `~~~` block | rerun-failed | **unknown** |
| blockquote | `> @flashinfer&#8203;-bot run` | run | **unknown** |
| nested blockquote | `> > @flashinfer&#8203;-bot rerun` | rerun |
**unknown** |
| prose, mid-sentence | `I will ask a maintainer to
@flashinfer&#8203;-bot run this once...` | run | **unknown** |
| table cell | `\| `@flashinfer&#8203;-bot run` \| full suite \|` | stop
| **unknown** |
| cc mention only | `cc @flashinfer&#8203;-bot -- could you take a
look?` | unknown | unknown |
| bullet + inline span | `- **`fix(ci): bind COMMENT_BODY in the
@flashinfer&#8203;-bot run handler`**` | run | **unknown** |
| heading | ``### How `@flashinfer&#8203;-bot run` works`` | run |
**unknown** |
| indented fence in a numbered list | `1.` then an indented backtick
fence | run | **unknown** |
| prose, sentence start | `Someone should @flashinfer&#8203;-bot run the
suite again;` | run | **unknown** |
| quoted reply history | `> On Tue, alex wrote:\n>
@flashinfer&#8203;-bot run tests/g...` | run | **unknown** |
| the 4 real #4880 documentation comments | (verbatim from the API) |
run ×4 | **unknown ×4** |
| the 7 real #4880 genuine commands | (verbatim from the API) | run ×7 |
run ×7 |

### How it was verified

- **Offline**: an independent Python model of the pipeline agrees with
the shipped shell step on all
44 corpus cases and on all 725 real handle-bearing comments — 0
divergences.
- **Under the real shell**: the `Parse command` step extracted verbatim
from the committed file, run
as `bash -e` with `COMMENT_BODY` in the environment. All cases exit
`rc=0` and always write a
`command=` output, including empty, whitespace-only, and non-matching
bodies.
- **GNU toolchain**: the runner image is not macOS, so the corpus was
also run on `ubuntu-24.04`
  (`GNU grep 3.11`, `GNU Awk 5.2.1`) — **44/44 PASS, 0 FAIL**.
- **Live**: 12 headline cases posted as real PR comments in a sandbox
repo running a byte-identical
copy of the step, driven by a real `issue_comment` event — 6 fired, 6
did not, **0 mismatches**,
  matching predictions exactly.

## 🔬 Experimental Track

<!-- Not an experimental-track PR; section left as the template provides
it. -->

<!-- Only for PRs submitted under the experimental policy
(CONTRIBUTING.md → "Experimental APIs and Backends").
     Leave this section untouched for normal PRs. -->

- [ ] This PR is **experimental**: it adds or changes code under
`flashinfer/experimental/` and/or an `@flashinfer_experimental_api`.
Tracking issue: #
- [ ] The tracking issue names an owner, the reason for the experimental
path, and a graduation plan with a target release.
- [ ] Core changes are limited to a thin entry point (signature, shared
validation, feature-gate check, backend selection, handoff).
- [ ] Tests live in `tests/experimental/` and were validated on the
intended hardware; a runnable example is included.
- [ ] Nothing is registered in `flashinfer/aot.py`, and no experimental
backend is reachable from `backend="auto"` without
`FLASHINFER_ALLOW_EXPERIMENTAL_AUTO_BACKENDS=1`. (Calling an
`@flashinfer_experimental_api` or naming a backend explicitly is itself
the opt-in and needs no environment variable.)
- [ ] **Test scope declared below.** The experimental CI lane runs
exactly these targets, so keep them as narrow as the change allows.

<!-- Required for experimental PRs. Replace the commented lines below
with your targets.
Do not delete the fence or change its `experimental-tests` tag — the
experimental-track
watcher reads it verbatim to decide which targets to ask CI for. -->

```experimental-tests
# One target per line: a directory or a file. (A pytest ::selector is not
# supported -- the sharding runner cannot consume one.) Must be under
# tests/experimental/ and must exist. Delete these comment lines and add yours, e.g.
#
#   tests/experimental/test_my_backend.py
#   tests/experimental/my_backend/
#
# Declaring the whole tree (tests/experimental/) is allowed but means every
# experimental PR pays for every other feature's tests, in every matrix cell.
```

## Reviewer Notes

### Reviewability is the safety property

`issue_comment` workflows always load from the **default branch**, so
this file is executed by zero
CI runs on this PR and cannot be tested by any PR. Nothing here
validates it before it lands on
`main`. That is why the change is confined to one file with a small,
obvious diff, and why the
verification above was done out-of-band in a sandbox repo instead.

### Residual gaps

**Still trigger, arguably should not.** The fence handling is a simple
toggle, so a fence nested
inside another fence flips it back off, and these leak (verified live):

````
```
BOT run
```
````

The same applies to a `~~~` outer fence containing a ``` inner fence.
This is ordinary CommonMark
nesting and is exactly what a comment documenting *this change* would
type. Fixing it properly means
tracking the opening fence's character and length, which costs the diff
its obviousness; it occurs
**zero** times in 725 real comments. Also still triggering:
4-space-indented code blocks, and HTML
constructs (`<details>`, `<pre>`, `<!-- -->`), since none of these is a
fence.

A line that *begins* with the handle and continues into prose —
`@flashinfer​-bot run is the command you
want.` — still fires. This is inherent and unfixable: `run <paths>` is
documented, so the two forms
are textually identical.

**No longer trigger, arguably should.** All fail closed (no CI started,
never a false trigger):

- A command below an **unterminated** fence is swallowed. A line that
merely *starts* with a
triple-backtick while actually being a one-line inline code span in GFM
(a command wrapped in
triple backticks on its own line) also flips fence parity, dropping a
genuine command later in
  the same comment.
- Anything to the left of the handle on the line: a bullet (`- `), `1.
`, bold (`**`), a non-breaking
  space, or prose.
- `@flashinfer&#8203;-bot run-ci` and `@flashinfer&#8203;-bot running
...` now resolve to `unknown` (previously `run`), because of
  the added word boundary. Intentional tightening.
- Precedence is now positional rather than by-keyword: a `stop` line
above a `rerun failed` line
yields `stop`, where the old code yielded `rerun-failed`. Only
observable in a comment containing
  two different commands; occurs zero times in 725 real comments.

**Feedback loss.** Comments that no longer parse get no reaction at all,
since the "Unauthorized
user" step is gated on `command != 'unknown'`. A mis-shaped command is
now silent for authorized and
unauthorized commenters alike.

**Latent, not currently reachable.** The step is correct today because
the runner shell is
`bash -e 0` *without* `pipefail`, so the `CMD=$(... | grep ... | sed
...)` assignment takes `sed`'s
status 0 even when `grep` matches nothing. If anyone later adds `shell:
bash` to this step or a
`defaults.run.shell: bash` to the workflow, `pipefail` turns on and the
assignment returns 1 under
`set -e` — a red X on every prose comment mentioning the handle.
Fail-safe (never a false trigger),
but worth knowing; a trailing `|| true` would immunize it.

### Scope

Touches only `.github/workflows/ci-bot-commands.yml` (+25/-5). Confirmed
with `git grep` that lines
26 and 96-102 are the only consumers of `comment.body` on `main`, so
this is independent of #4880.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Improved command detection to avoid interpreting regular prose, quoted
replies, inline code, tables, and fenced code blocks as commands.
- Preserved content inside code fences when opening and closing
delimiters do not match.
- Improved handling when no valid command is found, preventing
unnecessary processing failures.
- **Documentation**
- Clarified workflow filtering behavior for more transparent command
processing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants