Skip to content

WIP: b12x updates - #41243

Closed
askliar wants to merge 54 commits into
vllm-project:mainfrom
askliar:askliar/b12x-with-tinygemm
Closed

askliar wants to merge 54 commits into
vllm-project:mainfrom
askliar:askliar/b12x-with-tinygemm

Conversation

@askliar

@askliar askliar commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

No description provided.

meena-at-work and others added 30 commits April 20, 2026 19:11
  Adds FlashInferCuteDSLSM12xExperts targeting SM120/SM121 (RTX Pro
  6000 / DGX Spark) using cute_dsl_fused_moe_nvfp4 from FlashInfer
  PRs vllm-project#3051 and vllm-project#3066. The kernel fuses token dispatch, W1 GEMM, SwiGLU,
  and W2 GEMM into a single call; BF16 hidden states are passed directly
  as activation quantization is fused internally.

  - vllm/utils/flashinfer.py: lazy import wrappers for
    cute_dsl_fused_moe_nvfp4 and convert_sf_to_mma_layout; adds
    has_flashinfer_cutedsl_sm12x_moe() availability probe
  - experts/flashinfer_cutedsl_moe.py: FlashInferCuteDSLSM12xExperts
    with TODO to adopt plan/run() API from PR vllm-project#3066
  - oracle/nvfp4.py: FLASHINFER_CUTEDSL_SM12X backend enum and routing;
    falls back to FLASHINFER_CUTLASS on SM12x when PRs are absent
  - flashinfer_fp4_moe.py: SM12X added to FI weight-prep path and
    w1/w3 → w3/w1 reorder list
  - tests/kernels/moe/test_cutedsl_sm12x_moe.py: correctness tests vs
    BF16 torch reference; module-level skip when SM120 hw or FlashInfer
    PRs are absent

Signed-off-by: Meenakshi Venkataraman <meenakshiv@nvidia.com>
…_unquantized_inputs

The test was failing (all 24 cases, abs diff ~3904) because it used vLLM's
NVFP4 convention: scaled_fp4_quant(w, w_gs) bakes w_gs≈8960 into block
scales and sets g1_alphas=1/w_gs.  But launch_sm120_moe uses w1_alpha as
*both* the activation input_gs (arg 17) and the weight dequant factor (arg
18), conflating the two roles.  With input_gs=1/w_gs, activations are
scaled up by w_gs inside the kernel, producing outputs ~8960× too large.

Fix the test to use FlashInfer's convention: fp4_quantize(global_scale=1.0,
is_sf_swizzled_layout=True) so block_scale=max_abs/fp4_max and all alphas
are 1.0, satisfying both conflated roles simultaneously.

Also add expects_unquantized_inputs=True to FlashInferCuteDSLSM12xExperts:
cute_dsl_fused_moe_nvfp4 quantizes activations internally and must receive
BF16 hidden states.  Without this override the modular kernel pre-quantizes
to FP4 (size k//2) before apply(), breaking convert_sf_to_mma_layout which
expects the full k dimension.

Verified: 24/24 passed on SM121 (DGX SparkX2, p4242-0064).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- FlashInferCuteDSLSM12xExperts.process_weights_after_loading: normalize
  float8 block scales by w13_weight_scale_2 (=1/w_gs) at load time, then
  set weight_scale_2=1.0.  This converts vLLM's NVFP4 convention
  (block_scale = max_abs * w_gs / fp4_max) to the SM12x kernel's required
  convention (block_scale = max_abs / fp4_max, g1_alphas = 1.0), without
  re-quantising packed FP4 values which are identical in both conventions.
  Unlike other backends, activation scale is NOT baked in (would break the
  conflated activation-gs role in launch_sm120_moe).
- kernel.py: add "flashinfer_cutedsl_sm12x" to MoEBackend Literal so the
  --moe-backend CLI arg accepts it without "invalid choice" error.
- test skip message: "RTX Pro 6000 / DGX Spark" (not "Blackwell GeForce").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Andrii Skliar <askliar@nvidia.com>
- Updated references from FlashInferCuteDSLSM12xExperts to FlashInferB12xExperts across the codebase.
- Modified test cases to reflect the new B12x naming convention and adjusted skip conditions accordingly.
- Enhanced the `make_dummy_moe_config` function to accept activation type and is_act_and_mul parameters.
- Introduced a new test for FlashInferB12x with ReLU2 activation.
- Updated backend handling in various modules to support the B12x configuration.

This change aligns with the new backend structure and improves clarity in the codebase.
…ty family

- FlashInferB12xExperts.apply(): convert 3D swizzled scale factors
  [E, M, K_sf] to 6D MMA layout before passing to B12xMoEWrapper.run(),
  fixing "permute(sparse_coo): number of dimensions does not match (3 vs 6)"
- flashinfer_cutlass_moe.py: use is_device_capability_family(120) instead
  of is_device_capability(120) so SM121 is not excluded

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ion)

For batch size 1 (and up to M=8), replace cuBLAS BF16 GEMM with
FlashInfer's tinygemm_bf16 kernel which is latency-optimized for
small M using warp-specialized TMA + HMMA design from TRT-LLM.

This affects all unquantized BF16 linear layers: attention QKV/O
projections, dense MLP up/down projections, shared MoE experts,
and latent MoE projections. Requires SM90+ (Hopper/Blackwell).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Handle inputs that may be 3D (batch, seq, hidden) by computing
the total token count and reshaping for tinygemm, preserving
the original shape in the output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add @torch._dynamo.assume_constant_result to _tinygemm_bf16_available()
so dynamo doesn't trace into importlib/shutil calls that it can't handle.
The function returns a hardware-capability constant, so this is safe.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve tinygemm availability at import time and call
torch.ops.flashinfer.tinygemm2_op directly instead of going
through the lazy wrapper. This avoids importlib calls that
dynamo cannot trace in fullgraph AOT compilation mode.

- _init_tinygemm() runs at import: checks SM90+, JIT-compiles
  and registers the flashinfer::tinygemm2_op custom op
- dispatch_unquantized_gemm() reads a plain bool global
- _tinygemm_unquantized_gemm() only uses tensor ops and the
  registered custom op — all dynamo-safe

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Follow the established vLLM pattern for FlashInfer ops in compiled
code (matching scaled_mm/flashinfer.py):

- Register torch.ops.vllm.tinygemm_bf16 via direct_register_custom_op
- Real impl calls FlashInfer lazily (runs at execution time)
- Fake impl returns empty tensor (used during graph tracing)
- Compiled graph captures the custom op as an opaque FX node

This avoids calling importlib/FlashInfer wrappers during dynamo
tracing while keeping the actual FlashInfer kernel in the graph.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Andrii Skliar <askliar@nvidia.com>
- Introduced unit tests for the FlashInfer tinygemm_bf16 operation, covering correctness and dispatch behavior.
- Implemented tests to validate the operation under various conditions, including input shapes and bias handling.
- Ensured compatibility with torch.compile by verifying the dispatch mechanism and fallback scenarios.

This enhances the test coverage for the new tinygemm functionality and ensures robustness in different execution contexts.

Signed-off-by: Andrii Skliar <askliar@nvidia.com>
Signed-off-by: Andrii Skliar <askliar@nvidia.com>
Signed-off-by: Andrii Skliar <askliar@nvidia.com>
Integrates FlashInfer PR vllm-project#3051 b12x dense GEMM backend into the NVFP4
linear layer path. b12x uses CuTe DSL warp-level MMA with adaptive tile
sizing to improve SM utilization on small-M decode shapes.

Changes:
- has_flashinfer_b12x_gemm(): availability check via Sm120BlockScaledDenseGemmKernel
- FlashInferB12xNvFp4LinearKernel: new NvFp4LinearKernel subclass
- Auto-selects b12x on SM120/SM121 (has_device_capability(120)), falls
  back to FLASHINFER_CUTLASS when unavailable
- Adds "flashinfer-b12x" to VLLM_NVFP4_GEMM_BACKEND valid choices
- b12x test cases in test_flashinfer_nvfp4_scaled_mm.py

Measured on DGX Spark (SM121, Qwen3-30B-A3B-NVFP4, same MoE backend):
  b12x:               71.81 out tok/s (1P), 229.24 (8P)
  flashinfer-cutlass:  70.52 out tok/s (1P), 216.28 (8P)
  (+1.8% 1P, +6.0% 8P)

Signed-off-by: Meenakshi Venkataraman <meenakshiv@nvidia.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 6473f3bb2e8024798297529195fc3d0ff419c134)
Follow-up from sglang's force_inductor_max_autotune_gemm_on_small_gpus.
The 68-SM gate is only one of several inductor hurdles on sub-data-center
Blackwell hardware (GB10/DGX Spark SM121, RTX Pro 6000 SM120). Also
patched under VLLM_INDUCTOR_OVERRIDE_BIG_GPU=1:

- torch._inductor.codegen.cuda.cuda_env.is_datacenter_blackwell_arch:
  currently whitelists only arch in [100, 110) so SM120/121 falls back to
  non-Blackwell codegen. Extend to any major >= 10.
- Global inductor config: max_autotune / max_autotune_gemm = True,
  max_autotune_gemm_backends = "ATEN,TRITON",
  triton.enable_persistent_tma_matmul = True.
- is_big_gpu lambda now takes *args, **kwargs to survive upstream
  signature changes.

Log line hints at VLLM_FLOAT32_MATMUL_PRECISION=high for FP32 GEMMs
(e.g. Nemotron routers), which is vLLM's existing TF32 knob and the
analogue of sglang's torch.backends.cuda.matmul.allow_tf32=True +
set_float32_matmul_precision("high") pair.

Tests updated to cover all three patches plus config state, with a
fixture that captures and restores each mutated attribute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Updated test names for clarity and added new tests to cover scenarios where both Tinygemm and torch.compile are enabled or when Tinygemm is unavailable.
- Refactored utility functions to improve the handling of BF16 linear operations, separating eligibility checks and application logic for Tinygemm.
- Enhanced comments in the environment variable handling to clarify the interaction between Tinygemm and torch.compile paths.

This improves the overall dispatch mechanism for unquantized BF16 GEMM operations, ensuring better performance and maintainability.
- Updated the global inductor configuration to support the CUTLASS backend alongside ATEN and TRITON for max-autotune-gemm.
- Modified the related utility function and test to ensure CUTLASS is included in the backend list.
- Enhanced test coverage to validate the inclusion of CUTLASS in the autotune configuration.

This change improves flexibility and performance for various GPU architectures.
The inductor CUTLASS backend requires cutlass_dir to point to a CUTLASS
source tree with python/cutlass_library, python/cutlass_cppgen, and
python/pycute. The default (../third_party/cutlass/) only exists in
developer source builds.

Add fallback to /tmp/vllm/.deps/cutlass-src (CMake FetchContent dir)
so CUTLASS kernels are available as autotune candidates alongside
ATEN and Triton.
Switch from dynamic=True to dynamic=False + fullgraph=True for the
compiled BF16 linear fast path (matching sglang approach). Each unique
tensor shape gets its own maximally-optimized kernel via a unique exec-ed
function to avoid Dynamo guard collisions.

Also increase dynamo cache limits to 65536/1048576 to accommodate the
larger number of shape-specialized compilations, and use exact
(shape, stride, dtype, device) cache keys instead of approximate ones.
- Removed the `VLLM_INDUCTOR_OVERRIDE_BIG_GPU` environment variable to streamline the inductor configuration process.
- Introduced new utility functions to force inductor's full GEMM autotune template pool and to raise Dynamo cache limits, ensuring better performance on small GPUs.
- Updated tests to reflect changes in the inductor configuration and to validate the new utility functions, enhancing test coverage for BF16 linear operations.

These changes improve the handling of GPU configurations and ensure optimal performance across various architectures.
- Removed the deprecated `VLLM_INDUCTOR_OVERRIDE_BIG_GPU` environment variable and replaced it with a more robust mechanism for forcing inductor's max-autotune-gemm on small GPUs.
- Updated utility functions to include new helpers for managing Dynamo cache limits and inductor configurations.
- Enhanced test coverage to validate the new inductor configuration and its interaction with BF16 linear dispatch logic.

These changes improve the flexibility and performance of the inductor autotuning process, ensuring better compatibility with various GPU architectures.
- Removed the deprecated `VLLM_INDUCTOR_OVERRIDE_BIG_GPU` environment variable and replaced it with a more robust mechanism for forcing inductor's max-autotune-gemm on small GPUs.
- Updated utility functions to include new helpers for managing Dynamo cache limits and inductor configurations.
- Enhanced test coverage to validate the new inductor configuration and its interaction with BF16 linear dispatch logic.

These changes improve the flexibility and performance of the inductor autotuning process, ensuring better compatibility with various GPU architectures.

Signed-off-by: Andrii Skliar <askliar@nvidia.com>

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces support for FlashInfer B12x fused MoE kernels on SM12x devices, adds a torch.compile-based fast path for BF16 linear layers, and includes a tinygemm BF16 dispatch mechanism. My review identified several performance and correctness issues: the scale factor layout conversion in FlashInferB12xExperts should be cached to avoid per-pass overhead, the dynamo cache limit constants in utils.py need to be moved to the module level to resolve test failures, _apply_tinygemm should avoid repeated zero-tensor allocations, and the test case for inductor backends needs to be corrected to reflect the actual configuration.

Comment on lines +383 to +397
w1_sf_mma = flashinfer_convert_sf_to_mma_layout(
self.w1_scale.reshape(E_w1 * M_w1, K_sf_w1),
m=M_w1,
k=K_sf_w1 * sf_vec_size,
num_groups=E_w1,
sf_vec_size=sf_vec_size,
)
E_w2, M_w2, K_sf_w2 = self.w2_scale.shape
w2_sf_mma = flashinfer_convert_sf_to_mma_layout(
self.w2_scale.reshape(E_w2 * M_w2, K_sf_w2),
m=M_w2,
k=K_sf_w2 * sf_vec_size,
num_groups=E_w2,
sf_vec_size=sf_vec_size,
)

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.

high

The layout conversion flashinfer_convert_sf_to_mma_layout is called on every forward pass for both w1 and w2 scale factors. Since expert weights and their scales are typically static after loading, this conversion is a significant performance bottleneck. It should be performed once during weight processing or cached using the tensor's data pointer as a key.

        # Convert swizzled 3D scale factors [E, M, K_sf] to 6D MMA layout
        # expected by the SM12x kernel's _get_weight_views().
        sf_vec_size = 16
        if not hasattr(self, "_w1_sf_mma") or self._w1_sf_mma_key != self.w1_scale.data_ptr():
            E_w1, M_w1, K_sf_w1 = self.w1_scale.shape
            self._w1_sf_mma = flashinfer_convert_sf_to_mma_layout(
                self.w1_scale.reshape(E_w1 * M_w1, K_sf_w1),
                m=M_w1,
                k=K_sf_w1 * sf_vec_size,
                num_groups=E_w1,
                sf_vec_size=sf_vec_size,
            )
            self._w1_sf_mma_key = self.w1_scale.data_ptr()
        w1_sf_mma = self._w1_sf_mma

        if not hasattr(self, "_w2_sf_mma") or self._w2_sf_mma_key != self.w2_scale.data_ptr():
            E_w2, M_w2, K_sf_w2 = self.w2_scale.shape
            self._w2_sf_mma = flashinfer_convert_sf_to_mma_layout(
                self.w2_scale.reshape(E_w2 * M_w2, K_sf_w2),
                m=M_w2,
                k=K_sf_w2 * sf_vec_size,
                num_groups=E_w2,
                sf_vec_size=sf_vec_size,
            )
            self._w2_sf_mma_key = self.w2_scale.data_ptr()
        w2_sf_mma = self._w2_sf_mma

Comment on lines +250 to +252
_CACHE_LIMIT = 65536
_ACCUMULATED_LIMIT = 1048576

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.

high

The constants _CACHE_LIMIT and _ACCUMULATED_LIMIT are defined as local variables within force_inductor_max_autotune_gemm. However, the tests in tests/model_executor/test_compile_bf16_linear_dispatch.py attempt to access them as module-level attributes of vllm.model_executor.layers.utils using names like _DYNAMO_CACHE_SIZE_LIMIT. This will result in an AttributeError during testing. These should be moved to the module level and renamed to match the test expectations.

_DYNAMO_CACHE_SIZE_LIMIT = 65536
_DYNAMO_ACCUMULATED_CACHE_SIZE_LIMIT = 1048576

Comment on lines +494 to +499
if bias is None:
bias = torch.zeros(
weight.shape[0],
dtype=torch.bfloat16,
device=x.device,
)

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.

high

Allocating a new zero tensor via torch.zeros on every call to _apply_tinygemm when bias is None introduces unnecessary overhead, especially for small batch sizes where tinygemm is intended to provide the most benefit. Consider using a cached zero tensor or modifying the underlying kernel to handle a None bias directly.

assert inductor_config.coordinate_descent_tuning is True
assert "TRITON" in inductor_config.max_autotune_gemm_backends
assert "ATEN" in inductor_config.max_autotune_gemm_backends
assert "CUTLASS" in inductor_config.max_autotune_gemm_backends

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.

high

This assertion will fail because the implementation in vllm/model_executor/layers/utils.py explicitly sets inductor_config.max_autotune_gemm_backends = "ATEN,TRITON", excluding CUTLASS. The implementation comment also notes "no inductor CUTLASS". Please update the test to match the intended implementation.

Suggested change
assert "CUTLASS" in inductor_config.max_autotune_gemm_backends
assert "CUTLASS" not in inductor_config.max_autotune_gemm_backends

@mergify mergify Bot added the qwen Related to Qwen models label May 12, 2026
@mergify

mergify Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @askliar.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label May 16, 2026
Andrii and others added 12 commits May 17, 2026 07:31
…class

Two fixes for FlashInfer 0.6.11 (post the upstream W4A16 kernel refactor in
PRs vllm-project#3271 / vllm-project#3307) on the b12x path:

1) flashinfer_fp4_moe.py: in prepare_nvfp4_moe_layer_for_fi_or_cutlass, the
   ELSE branch (the path for FLASHINFER_B12X, FLASHINFER_CUTLASS, and
   VLLM_CUTLASS) already pads w13, w2, and w2_scale when the swizzled block-
   scale row count exceeds w13's row count (e.g. 1856 -> 1920 for Nemotron-
   Nano-3.5 W4A16). The TRTLLM branch above also updates
   layer.moe_config.intermediate_size_per_partition to the padded value; the
   ELSE branch did not. As a result, FlashInferB12xExperts (constructed
   later in the same process_weights_after_loading) reads the unpadded
   intermediate_size from moe_config and pre-allocates the W4A16 workspace
   with the wrong n dimension. FI 0.6.11's _validate_w4a16_workspace then
   raises "pre-allocated W4A16 workspace hidden geometry mismatch:
   workspace.n=1856 runtime_n=1920". Mirror the TRTLLM branch's
   intermediate-size write inside the existing `if pad_size > 0:` block.

2) vllm/utils/flashinfer.py: has_flashinfer_b12x_gemm() looked only for
   Sm120BlockScaledDenseGemmKernel, but FlashInfer renamed it to
   Sm120B12xBlockScaledDenseGemmKernel in 0.6.11. Accept either name so the
   b12x NVFP4 GEMM backend remains detected after the upstream rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Roi Koren <roik@nvidia.com>
Signed-off-by: Roi Koren <roik@nvidia.com>
…en speculative decoding is enabled (vllm-project#40454)"

This reverts commit f819265.

Signed-off-by: Roi Koren <roik@nvidia.com>
Signed-off-by: Roi Koren <roik@nvidia.com>
Signed-off-by: Roi Koren <roik@nvidia.com>
Signed-off-by: Roi Koren <roik@nvidia.com>
Signed-off-by: Roi Koren <roik@nvidia.com>
Signed-off-by: Roi Koren <roik@nvidia.com>
Signed-off-by: Roi Koren <roik@nvidia.com>
Signed-off-by: Roi Koren <roik@nvidia.com>
…de when no mamba block boundary can be crossed

Re-applies vllm-project#42574 on top of the PR vllm-project#41233 series. The
upstream patch targets the pre-vllm-project#41233 two-branch (`is_align` / else)
structure where postprocess_mamba was only called in the align branch.
vllm-project#41233 unified the flow so postprocess_mamba is always called. This
commit adapts the same skip optimisation to the unified structure:

  - Add `can_skip_mamba_postprocess` helper in mamba_utils.py (verbatim
    from PR vllm-project#42574).
  - In `_update_states_after_model_execute`, when in align mode, decide
    on CPU whether any request can cross a mamba block boundary. If
    not, defer the device-to-host `.cpu().numpy()` sync via the
    existing non_blocking copy + event.record() path that the else
    branch uses, and early-return to skip the no-op
    `postprocess_mamba`. Otherwise fall through to the original
    blocking sync + postprocess_mamba call.

Benchmarks from the upstream PR description (Nemotron-Super-120B-A12B-
NVFP4, MTP=3, GB300 single GPU): +17% overall TPS, -13.7% ITL, slow
cudaMemcpyAsync count -92.6%, GPU kernel time unchanged.

Co-Authored-By: Mingyuan Ma <minma@nvidia.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants