Skip to content

[Kernel] add Flashinfer cutedsl w4a16 linear - #53014

Merged
vadiklyutiy merged 4 commits into
vllm-project:mainfrom
IwakuraRein:flashinfer-cutedsl-w4a16
Sep 1, 2026
Merged

vadiklyutiy merged 4 commits into
vllm-project:mainfrom
IwakuraRein:flashinfer-cutedsl-w4a16

Conversation

@IwakuraRein

@IwakuraRein IwakuraRein commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

PLEASE FILL IN THE PR DESCRIPTION HERE ENSURING ALL CHECKLIST ITEMS (AT THE BOTTOM) HAVE BEEN CONSIDERED.

Purpose

This PR adds FlashInferCuteDslNvFp4W4A16LinearKernel. Flashinfer already includes a sm12x cute-dsl w4a16 linear kernel.

flashinfer-ai/flashinfer#4466 has added a sm100 cute-dsl w4a16 linear kernel and is included in 0.6.18. vLLM will default to flashinfer cute-dsl instead of Marlin when using sm100/103.

Test Plan

Test the accuracy and performance of qwen 3.6 27B nvfp4 checkpoint. It is quantized by ModelOpt thus contains w4a16 nvfp4 layer.

Test Result

Tested on a single B200:

pareto

Accuracy Tests

  • GSM8K: 94.8%

Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.

BEFORE SUBMITTING, PLEASE READ https://docs.vllm.ai/en/latest/contributing (anything written below this line will be removed by GitHub Actions)

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify

mergify Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Documentation preview: https://vllm--53014.org.readthedocs.build/en/53014/

@mergify mergify Bot added the documentation Improvements or additions to documentation label Aug 19, 2026
@mergify

mergify Bot commented Aug 28, 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, @IwakuraRein.

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 Aug 28, 2026
Signed-off-by: Siyuan Fu <siyuanf@nvidia.com>
Signed-off-by: Siyuan Fu <siyuanf@nvidia.com>
Signed-off-by: Siyuan Fu <siyuanf@nvidia.com>
@IwakuraRein
IwakuraRein force-pushed the flashinfer-cutedsl-w4a16 branch from 3999934 to d91d18f Compare August 31, 2026 16:46
@mergify mergify Bot removed the needs-rebase label Aug 31, 2026
Signed-off-by: Siyuan Fu <siyuanf@nvidia.com>
@vadiklyutiy

Copy link
Copy Markdown
Member

/ci run

@github-project-automation github-project-automation Bot moved this to Ready in NVIDIA Aug 31, 2026
@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #86433 for commit b6282bbc2b8a.

@vadiklyutiy
vadiklyutiy enabled auto-merge (squash) August 31, 2026 22:17
@github-actions github-actions Bot added the ready ONLY add when PR is ready to merge/full CI is needed label Aug 31, 2026
@vadiklyutiy vadiklyutiy added ready ONLY add when PR is ready to merge/full CI is needed and removed ready ONLY add when PR is ready to merge/full CI is needed labels Aug 31, 2026
@github-actions

Copy link
Copy Markdown

@IwakuraRein, CI is now available for this PR.

  • /ci run starts upstream CI; /amd-ci run starts AMD CI only.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.
  • /amd-ci retry retries failed jobs in AMD CI for the current PR head. Use /amd-ci run when the current head has no AMD CI build.
  • /ci cancel cancels scheduled or running CI builds for this PR branch; /amd-ci cancel does the same for AMD CI only.

Comment thread vllm/config/kernel.py Outdated
@IwakuraRein

Copy link
Copy Markdown
Contributor Author

/ci retry

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

✅ Queued 1 failed job(s) for retry in Buildkite CI #86433.

@vadiklyutiy
vadiklyutiy merged commit 882ca8d into vllm-project:main Sep 1, 2026
143 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to Done in NVIDIA Sep 1, 2026
am-cohere pushed a commit to am-cohere/vllm that referenced this pull request Sep 1, 2026
Signed-off-by: Siyuan Fu <siyuanf@nvidia.com>
puririshi98 added a commit to puririshi98/vllm that referenced this pull request Sep 1, 2026
…op (dequant + 16-bit GEMM, opt-in)

At prefill-sized M the weight-only Marlin kernel runs far below dense
16-bit rate (Nemotron-3.5-Lightning-30B-A3B-NVFP4 cache-miss profile:
137 ms GPU per 6576-token chunk, 96.5% GEMM, marlin-dominated at ~300
TFLOPS effective). Weight-only quantization only has to win while the
GEMM is memory-bound; at chunked-prefill M it is compute-bound and the
dequant tax is paid once per K x N instead of M times through the slow
kernel. When M >= threshold, apply_fp8_marlin_linear /
apply_fp4_marlin_linear dequantize the original-format weight into a
shared 16-bit workspace reserved at weight-loading time and run the
full-rate GEMM (torch.mm / F.linear); below the threshold every call
stays on Marlin, so decode and spec-verify launches (M <= 8, including
inside captured decode graphs) keep the memory-bound winner.

The M-vs-threshold branch lives behind a torch custom op
(vllm::marlin_large_m_gemm) because a Python
`reshaped_x.size(0) >= threshold` in the traced apply is decided ONCE
by dynamo (a BACKED symint guard vLLM drops with evaluate_guards=False)
and baked into every compiled artifact; inductor compile-range
endpoints cannot fix that (measured falsification: decode collapsed
1.3568 -> 3.1145 ms/tok with the dequant copy_mul kernels x23/x21 in
the decode window under every range config). As an opaque op the branch
is re-decided eagerly per call from the real x.size(0): register_fake
returns the single meta valid for BOTH branches (a fresh contiguous
[M, N] tensor in the activation dtype, enforced in the impl), both the
Marlin lock workspace and the shared dequant workspace are declared in
mutates_args (hidden mutation would let the compiler cache or reorder
around the op), and the threshold is an op argument wired to
VLLM_MARLIN_LARGE_M_BF16 (0 = off, default; 1 = 512; >= 16 sets it
directly, so 512 and 4096 are both reachable for the sweep; values in
[2, 16) are rejected to protect the decode guard). The branch is a pure
function of M, so a cudagraph captured at some padded M legally freezes
exactly the branch eager execution would take at that M -- FULL capture
at decode shapes stays legal -- and the small-M arm inside the op is
the same factored-out 16-bit Marlin call sequence
(_marlin_16bit_gemm) the non-dispatched apply path runs, by
construction.

Per-decode-call added overhead (the poison axis): with the flag OFF
(default) the only addition is one getattr per apply call,
constant-folded away at trace time -- the compiled artifact is
unchanged. With the flag ON, decode steps replayed from captured
cudagraphs pay nothing (the op's Python body runs at capture time
only); non-captured regions pay one dispatcher round-trip plus the
branch compare per dispatched dense layer call (order 1-2 us of host
work, x23 dense layers, i.e. sub-0.01 ms/tok class at AL 3.00) -- which
is exactly why the serve A/B's decode acceptance criterion is every-rep
ITL within +-0.01 ms/tok backed by a decode-window kernel diff, not an
assumption.

Admission and memory semantics are unchanged from the eager dispatch:
fail-closed at prepare time to dense FP8 non-blockwise with 16-bit
activations and dense NVFP4 g16 with a scalar global scale only; a
256 MiB per-layer cap excludes lm_head-class layers; MoE stays excluded
by measurement (honest per-call expert dequant loses 0.77x/0.29x at
miss/hit chunks). Enabling retains the original quantized tensor and
dequant-ready 16-bit scales of each dispatched layer, plus one shared
per-(device, dtype) workspace sized by the largest dispatched K x N,
grown only during weight loading through a stable holder object (layer
contexts keep a reference across boot-time growth) and never allocated
in the forward; if the reserved workspace cannot host a layer in the
activation dtype the op fails closed to Marlin. Numerics reproduce the
Marlin kernel's effective dequantization (NVFP4 processed-scale
clamp-to-zero, FP8 pre-exponent-fusion scales); accumulation order
differs, so outputs match at kernel-test tolerance, not bitwise.

Falsifiable predictions for the serve A/B: on the DSpark recipe (32K shared
prefix, C=1, AL exactly 3.00) with the flag on, steady cache-miss TTFT
lands in the 634 ms class measured in the first serve A/B (-260 ms vs
its same-allocation 894.55 ms baseline), rehit -44, hit -11.5; every-rep decode ITL
inside +-0.01 ms/tok of the same-allocation bracket with copy_mul_slice_view
x23/x21 ABSENT from the decode-window kernel diff and small-M marlin
launches PRESENT, on sm103 and sm100.

Tested (tests/kernels/quantization/test_marlin_gemm.py): both dispatch
arms against the dequantized reference and each other (FP8 both weight
layouts x bias x fp16/bf16; NVFP4 both dtypes), NVFP4 clamp semantics,
a marlin_gemm spy asserting M <= 8 always selects Marlin while
dispatched M never calls it, threshold floor and workspace-cap
fail-closed gates, custom-threshold boundary, 512/4096 sweep-point
boundary dispatch, capture-vs-eager bitwise equality at M in {1, 2, 4}
(fp8 + nvfp4, atomic-add reduce forced off), opcheck schema/fake on
both branches, and a compiled toy region (fullgraph, dynamic=True)
proving a single artifact serves both branches with error_on_recompile
armed -- the traced-branch failure mode reproduces as a hard test
failure. The dynamo mechanism check also passes CPU-side through the
real apply path with the kernel stubbed; the GPU suite runs in-container
on the measurement hardware.

Measured A/B (on main @ 85c1365; identical serve recipe and marlin
kernel build in both arms, max_num_batched_tokens 8770, AL 3.00
synthetic dspark, in-allocation brackets, one node per run): steady cache-miss TTFT 648.69 vs 911.21 ms
(-262.5) at threshold 512 and 651.73 (-259.5) at 4096 on GB300 sm103;
685.69 vs 917.34 (-231.7) at 4096 on GB200 sm100. Every-rep decode ITL within the +-0.01 ms/tok band at 4096
(worst +0.0014); at 512 every rep measured FASTER than the same-run
baseline (1.3976/1.3967 vs min 1.4101) -- 512 is the shipped default
when armed, 4096 the documented hit-neutral alternative. Decode-window
kernel diff: copy_mul_dequant 0, nvjet_splitK 0, dense marlin 7700 on
both thresholds and both arches; miss-window engagement
vllm::marlin_large_m_gemm x545 + fused triton call-site kernels x870 in
the traced 5-chunk miss window (plan [8768, 8768, 8768, 6576, 1952];
545 = 5 x 109 dispatch-eligible linears). Both engagement counts are
call-site counts and therefore threshold-invariant: the M-vs-threshold
branch lives inside the opaque op, which is invoked for every eligible
linear regardless of the branch taken. The per-threshold branch receipt
is the dense-marlin count in the same window -- 635 at threshold 512 vs
844 at 4096, every other kernel label identical: the plan's 1952-token
tail chunk is below 4096, stays on Marlin there (+209 marlin kernels),
and dispatches at 512, consistent with the 2192/1952 hit chunks
dispatching at 512 only. Cost: KV headroom
-1.71 GiB absolute (177.40 -> 175.69); hit TTFT at 512 +7.6 ms
standalone (dequant on the 2192/1952 hit chunks) -- composed with the
final-split elision the merged 4144-token hit chunk inverts this into
a win (combined candidate: hit 100.27 vs 136.86 in-allocation).
Engaged accuracy adjudication (run on the pre-rebase
base tree -- this patch's runtime (vllm/) hunks are patch-identical on
both bases (vllm/-restricted patch-id 1bb0141f); its test/bench files
later took a disclosed 9-line lint-only amend; gsm8k 5-shot
n=250 temp 0, real rejection sampling, APC off, every prompt through
the dispatched path): flex -0.0040 (+-0.0612), strict -0.0120 (+-0.0633), 109/250
byte-identical, flips symmetric, real acceptance length 3.5183 vs
3.5159.

Interaction with vllm-project#53014 (FlashInfer cute-dsl W4A16 NVFP4 default):
upstream now prefers FlashInferCuteDslNvFp4W4A16LinearKernel over
Marlin for W4A16_NVFP4 under --linear-backend=auto on SM100/103, so on
those parts the NVFP4 half of this patch no longer engages by default.
The measured recipe pins --linear-backend=marlin, whose backend-map
entry (MarlinNvFp4LinearKernel -> apply_fp4_marlin_linear, where the
large-M context attaches during Marlin weight processing) is untouched
by vllm-project#53014; under that pin -- and everywhere Marlin remains the
selected kernel -- the dispatch engages exactly as measured. The FP8
channelwise half (marlin_utils_fp8), which carries most of the win, is
unaffected by vllm-project#53014 entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Rishi Puri <riship@nvidia.com>
puririshi98 added a commit to puririshi98/vllm that referenced this pull request Sep 1, 2026
…op (dequant + 16-bit GEMM, opt-in)

At prefill-sized M the weight-only Marlin kernel runs far below dense
16-bit rate (Nemotron-3.5-Lightning-30B-A3B-NVFP4 cache-miss profile:
137 ms GPU per 6576-token chunk, 96.5% GEMM, marlin-dominated at ~300
TFLOPS effective). Weight-only quantization only has to win while the
GEMM is memory-bound; at chunked-prefill M it is compute-bound and the
dequant tax is paid once per K x N instead of M times through the slow
kernel. When M >= threshold, apply_fp8_marlin_linear /
apply_fp4_marlin_linear dequantize the original-format weight into a
shared 16-bit workspace reserved at weight-loading time and run the
full-rate GEMM (torch.mm / F.linear); below the threshold every call
stays on Marlin, so decode and spec-verify launches (M <= 8, including
inside captured decode graphs) keep the memory-bound winner.

The M-vs-threshold branch lives behind a torch custom op
(vllm::marlin_large_m_gemm) because a Python
`reshaped_x.size(0) >= threshold` in the traced apply is decided ONCE
by dynamo (a BACKED symint guard vLLM drops with evaluate_guards=False)
and baked into every compiled artifact; inductor compile-range
endpoints cannot fix that (measured falsification: decode collapsed
1.3568 -> 3.1145 ms/tok with the dequant copy_mul kernels x23/x21 in
the decode window under every range config). As an opaque op the branch
is re-decided eagerly per call from the real x.size(0): register_fake
returns the single meta valid for BOTH branches (a fresh contiguous
[M, N] tensor in the activation dtype, enforced in the impl), both the
Marlin lock workspace and the shared dequant workspace are declared in
mutates_args (hidden mutation would let the compiler cache or reorder
around the op), and the threshold is an op argument wired to
VLLM_MARLIN_LARGE_M_BF16 (0 = off, default; 1 = 512; >= 16 sets it
directly, so 512 and 4096 are both reachable for the sweep; values in
[2, 16) are rejected to protect the decode guard). The branch is a pure
function of M, so a cudagraph captured at some padded M legally freezes
exactly the branch eager execution would take at that M -- FULL capture
at decode shapes stays legal -- and the small-M arm inside the op is
the same factored-out 16-bit Marlin call sequence
(_marlin_16bit_gemm) the non-dispatched apply path runs, by
construction.

Per-decode-call added overhead (the poison axis): with the flag OFF
(default) the only addition is one getattr per apply call,
constant-folded away at trace time -- the compiled artifact is
unchanged. With the flag ON, decode steps replayed from captured
cudagraphs pay nothing (the op's Python body runs at capture time
only); non-captured regions pay one dispatcher round-trip plus the
branch compare per dispatched dense layer call (order 1-2 us of host
work, x23 dense layers, i.e. sub-0.01 ms/tok class at AL 3.00) -- which
is exactly why the serve A/B's decode acceptance criterion is every-rep
ITL within +-0.01 ms/tok backed by a decode-window kernel diff, not an
assumption.

Admission and memory semantics are unchanged from the eager dispatch:
fail-closed at prepare time to dense FP8 non-blockwise with 16-bit
activations and dense NVFP4 g16 with a scalar global scale only; a
256 MiB per-layer cap excludes lm_head-class layers; MoE stays excluded
by measurement (honest per-call expert dequant loses 0.77x/0.29x at
miss/hit chunks). Enabling retains the original quantized tensor and
dequant-ready 16-bit scales of each dispatched layer, plus one shared
per-(device, dtype) workspace sized by the largest dispatched K x N,
grown only during weight loading through a stable holder object (layer
contexts keep a reference across boot-time growth) and never allocated
in the forward; if the reserved workspace cannot host a layer in the
activation dtype the op fails closed to Marlin. Numerics reproduce the
Marlin kernel's effective dequantization (NVFP4 processed-scale
clamp-to-zero, FP8 pre-exponent-fusion scales); accumulation order
differs, so outputs match at kernel-test tolerance, not bitwise.

Falsifiable predictions for the serve A/B: on the DSpark recipe (32K shared
prefix, C=1, AL exactly 3.00) with the flag on, steady cache-miss TTFT
lands in the 634 ms class measured in the first serve A/B (-260 ms vs
its same-allocation 894.55 ms baseline), rehit -44, hit -11.5; every-rep decode ITL
inside +-0.01 ms/tok of the same-allocation bracket with copy_mul_slice_view
x23/x21 ABSENT from the decode-window kernel diff and small-M marlin
launches PRESENT, on sm103 and sm100.

Tested (tests/kernels/quantization/test_marlin_gemm.py): both dispatch
arms against the dequantized reference and each other (FP8 both weight
layouts x bias x fp16/bf16; NVFP4 both dtypes), NVFP4 clamp semantics,
a marlin_gemm spy asserting M <= 8 always selects Marlin while
dispatched M never calls it, threshold floor and workspace-cap
fail-closed gates, custom-threshold boundary, 512/4096 sweep-point
boundary dispatch, capture-vs-eager bitwise equality at M in {1, 2, 4}
(fp8 + nvfp4, atomic-add reduce forced off), opcheck schema/fake on
both branches, and a compiled toy region (fullgraph, dynamic=True)
proving a single artifact serves both branches with error_on_recompile
armed -- the traced-branch failure mode reproduces as a hard test
failure. The dynamo mechanism check also passes CPU-side through the
real apply path with the kernel stubbed; the GPU suite runs in-container
on the measurement hardware.

Measured A/B (on main @ 85c1365; identical serve recipe and marlin
kernel build in both arms, max_num_batched_tokens 8770, AL 3.00
synthetic dspark, in-allocation brackets, one node per run): steady cache-miss TTFT 648.69 vs 911.21 ms
(-262.5) at threshold 512 and 651.73 (-259.5) at 4096 on GB300 sm103;
685.69 vs 917.34 (-231.7) at 4096 on GB200 sm100. Every-rep decode ITL within the +-0.01 ms/tok band at 4096
(worst +0.0014); at 512 every rep measured FASTER than the same-run
baseline (1.3976/1.3967 vs min 1.4101) -- 512 is the shipped default
when armed, 4096 the documented hit-neutral alternative. Decode-window
kernel diff: copy_mul_dequant 0, nvjet_splitK 0, dense marlin 7700 on
both thresholds and both arches; miss-window engagement
vllm::marlin_large_m_gemm x545 + fused triton call-site kernels x870 in
the traced 5-chunk miss window (plan [8768, 8768, 8768, 6576, 1952];
545 = 5 x 109 dispatch-eligible linears). Both engagement counts are
call-site counts and therefore threshold-invariant: the M-vs-threshold
branch lives inside the opaque op, which is invoked for every eligible
linear regardless of the branch taken. The per-threshold branch receipt
is the dense-marlin count in the same window -- 635 at threshold 512 vs
844 at 4096, every other kernel label identical: the plan's 1952-token
tail chunk is below 4096, stays on Marlin there (+209 marlin kernels),
and dispatches at 512, consistent with the 2192/1952 hit chunks
dispatching at 512 only. Cost: KV headroom
-1.71 GiB absolute (177.40 -> 175.69); hit TTFT at 512 +7.6 ms
standalone (dequant on the 2192/1952 hit chunks) -- composed with the
final-split elision the merged 4144-token hit chunk inverts this into
a win (combined candidate: hit 100.27 vs 136.86 in-allocation).
Engaged accuracy adjudication (run on the pre-rebase
base tree -- this patch's runtime (vllm/) hunks are patch-identical on
both bases (vllm/-restricted patch-id 1bb0141f); its test/bench files
later took a disclosed 9-line lint-only amend; gsm8k 5-shot
n=250 temp 0, real rejection sampling, APC off, every prompt through
the dispatched path): flex -0.0040 (+-0.0612), strict -0.0120 (+-0.0633), 109/250
byte-identical, flips symmetric, real acceptance length 3.5183 vs
3.5159.

Interaction with vllm-project#53014 (FlashInfer cute-dsl W4A16 NVFP4 default):
upstream now prefers FlashInferCuteDslNvFp4W4A16LinearKernel over
Marlin for W4A16_NVFP4 under --linear-backend=auto on SM100/103, so on
those parts the NVFP4 half of this patch no longer engages by default.
The measured recipe pins --linear-backend=marlin, whose backend-map
entry (MarlinNvFp4LinearKernel -> apply_fp4_marlin_linear, where the
large-M context attaches during Marlin weight processing) is untouched
by vllm-project#53014; under that pin -- and everywhere Marlin remains the
selected kernel -- the dispatch engages exactly as measured. The FP8
channelwise half (marlin_utils_fp8), which carries most of the win, is
unaffected by vllm-project#53014 entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Rishi Puri <riship@nvidia.com>
puririshi98 added a commit to puririshi98/vllm that referenced this pull request Sep 1, 2026
…op (dequant + 16-bit GEMM, opt-in)

At prefill-sized M the weight-only Marlin kernel runs far below dense
16-bit rate (Nemotron-3.5-Lightning-30B-A3B-NVFP4 cache-miss profile:
137 ms GPU per 6576-token chunk, 96.5% GEMM, marlin-dominated at ~300
TFLOPS effective). Weight-only quantization only has to win while the
GEMM is memory-bound; at chunked-prefill M it is compute-bound and the
dequant tax is paid once per K x N instead of M times through the slow
kernel. When M >= threshold, apply_fp8_marlin_linear /
apply_fp4_marlin_linear dequantize the original-format weight into a
shared 16-bit workspace reserved at weight-loading time and run the
full-rate GEMM (torch.mm / F.linear); below the threshold every call
stays on Marlin, so decode and spec-verify launches (M <= 8, including
inside captured decode graphs) keep the memory-bound winner.

The M-vs-threshold branch lives behind a torch custom op
(vllm::marlin_large_m_gemm) because a Python
`reshaped_x.size(0) >= threshold` in the traced apply is decided ONCE
by dynamo (a BACKED symint guard vLLM drops with evaluate_guards=False)
and baked into every compiled artifact; inductor compile-range
endpoints cannot fix that (measured falsification: decode collapsed
1.3568 -> 3.1145 ms/tok with the dequant copy_mul kernels x23/x21 in
the decode window under every range config). As an opaque op the branch
is re-decided eagerly per call from the real x.size(0): register_fake
returns the single meta valid for BOTH branches (a fresh contiguous
[M, N] tensor in the activation dtype, enforced in the impl), both the
Marlin lock workspace and the shared dequant workspace are declared in
mutates_args (hidden mutation would let the compiler cache or reorder
around the op), and the threshold is an op argument wired to
VLLM_MARLIN_LARGE_M_BF16 (0 = off, default; 1 = 512; >= 16 sets it
directly, so 512 and 4096 are both reachable for the sweep; values in
[2, 16) are rejected to protect the decode guard). The branch is a pure
function of M, so a cudagraph captured at some padded M legally freezes
exactly the branch eager execution would take at that M -- FULL capture
at decode shapes stays legal -- and the small-M arm inside the op is
the same factored-out 16-bit Marlin call sequence
(_marlin_16bit_gemm) the non-dispatched apply path runs, by
construction.

Per-decode-call added overhead (the poison axis): with the flag OFF
(default) the only addition is one getattr per apply call,
constant-folded away at trace time -- the compiled artifact is
unchanged. With the flag ON, decode steps replayed from captured
cudagraphs pay nothing (the op's Python body runs at capture time
only); non-captured regions pay one dispatcher round-trip plus the
branch compare per dispatched dense layer call (order 1-2 us of host
work, x23 dense layers, i.e. sub-0.01 ms/tok class at AL 3.00) -- which
is exactly why the serve A/B's decode acceptance criterion is every-rep
ITL within +-0.01 ms/tok backed by a decode-window kernel diff, not an
assumption.

Admission and memory semantics are unchanged from the eager dispatch:
fail-closed at prepare time to dense FP8 non-blockwise with 16-bit
activations and dense NVFP4 g16 with a scalar global scale only; a
256 MiB per-layer cap excludes lm_head-class layers; MoE stays excluded
by measurement (honest per-call expert dequant loses 0.77x/0.29x at
miss/hit chunks). Enabling retains the original quantized tensor and
dequant-ready 16-bit scales of each dispatched layer, plus one shared
per-(device, dtype) workspace sized by the largest dispatched K x N,
grown only during weight loading through a stable holder object (layer
contexts keep a reference across boot-time growth) and never allocated
in the forward; if the reserved workspace cannot host a layer in the
activation dtype the op fails closed to Marlin. Numerics reproduce the
Marlin kernel's effective dequantization (NVFP4 processed-scale
clamp-to-zero, FP8 pre-exponent-fusion scales); accumulation order
differs, so outputs match at kernel-test tolerance, not bitwise.

Falsifiable predictions for the serve A/B: on the DSpark recipe (32K shared
prefix, C=1, AL exactly 3.00) with the flag on, steady cache-miss TTFT
lands in the 634 ms class measured in the first serve A/B (-260 ms vs
its same-allocation 894.55 ms baseline), rehit -44, hit -11.5; every-rep decode ITL
inside +-0.01 ms/tok of the same-allocation bracket with copy_mul_slice_view
x23/x21 ABSENT from the decode-window kernel diff and small-M marlin
launches PRESENT, on sm103 and sm100.

Tested (tests/kernels/quantization/test_marlin_gemm.py): both dispatch
arms against the dequantized reference and each other (FP8 both weight
layouts x bias x fp16/bf16; NVFP4 both dtypes), NVFP4 clamp semantics,
a marlin_gemm spy asserting M <= 8 always selects Marlin while
dispatched M never calls it, threshold floor and workspace-cap
fail-closed gates, custom-threshold boundary, 512/4096 sweep-point
boundary dispatch, capture-vs-eager bitwise equality at M in {1, 2, 4}
(fp8 + nvfp4, atomic-add reduce forced off), opcheck schema/fake on
both branches, and a compiled toy region (fullgraph, dynamic=True)
proving a single artifact serves both branches with error_on_recompile
armed -- the traced-branch failure mode reproduces as a hard test
failure. The dynamo mechanism check also passes CPU-side through the
real apply path with the kernel stubbed; the GPU suite runs in-container
on the measurement hardware.

Measured A/B (on main @ 85c1365; identical serve recipe and marlin
kernel build in both arms, max_num_batched_tokens 8770, AL 3.00
synthetic dspark, in-allocation brackets, one node per run): steady cache-miss TTFT 648.69 vs 911.21 ms
(-262.5) at threshold 512 and 651.73 (-259.5) at 4096 on GB300 sm103;
685.69 vs 917.34 (-231.7) at 4096 on GB200 sm100. Every-rep decode ITL within the +-0.01 ms/tok band at 4096
(worst +0.0014); at 512 every rep measured FASTER than the same-run
baseline (1.3976/1.3967 vs min 1.4101) -- 512 is the shipped default
when armed, 4096 the documented hit-neutral alternative. Decode-window
kernel diff: copy_mul_dequant 0, nvjet_splitK 0, dense marlin 7700 on
both thresholds and both arches; miss-window engagement
vllm::marlin_large_m_gemm x545 + fused triton call-site kernels x870 in
the traced 5-chunk miss window (plan [8768, 8768, 8768, 6576, 1952];
545 = 5 x 109 dispatch-eligible linears). Both engagement counts are
call-site counts and therefore threshold-invariant: the M-vs-threshold
branch lives inside the opaque op, which is invoked for every eligible
linear regardless of the branch taken. The per-threshold branch receipt
is the dense-marlin count in the same window -- 635 at threshold 512 vs
844 at 4096, every other kernel label identical: the plan's 1952-token
tail chunk is below 4096, stays on Marlin there (+209 marlin kernels),
and dispatches at 512, consistent with the 2192/1952 hit chunks
dispatching at 512 only. Cost: KV headroom
-1.71 GiB absolute (177.40 -> 175.69); hit TTFT at 512 +7.6 ms
standalone (dequant on the 2192/1952 hit chunks) -- composed with the
final-split elision the merged 4144-token hit chunk inverts this into
a win (combined candidate: hit 100.27 vs 136.86 in-allocation).
Engaged accuracy adjudication (run on the pre-rebase
base tree -- this patch's runtime (vllm/) hunks are patch-identical on
both bases (vllm/-restricted patch-id 1bb0141f); its test/bench files
later took a disclosed 9-line lint-only amend; gsm8k 5-shot
n=250 temp 0, real rejection sampling, APC off, every prompt through
the dispatched path): flex -0.0040 (+-0.0612), strict -0.0120 (+-0.0633), 109/250
byte-identical, flips symmetric, real acceptance length 3.5183 vs
3.5159.

Interaction with vllm-project#53014 (FlashInfer cute-dsl W4A16 NVFP4 default):
upstream now prefers FlashInferCuteDslNvFp4W4A16LinearKernel over
Marlin for W4A16_NVFP4 under --linear-backend=auto on SM100/103, so on
those parts the NVFP4 half of this patch no longer engages by default.
The measured recipe pins --linear-backend=marlin, whose backend-map
entry (MarlinNvFp4LinearKernel -> apply_fp4_marlin_linear, where the
large-M context attaches during Marlin weight processing) is untouched
by vllm-project#53014; under that pin -- and everywhere Marlin remains the
selected kernel -- the dispatch engages exactly as measured. The FP8
channelwise half (marlin_utils_fp8), which carries most of the win, is
unaffected by vllm-project#53014 entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Rishi Puri <riship@nvidia.com>
puririshi98 added a commit to puririshi98/vllm that referenced this pull request Sep 2, 2026
…op (dequant + 16-bit GEMM, opt-in)

At prefill-sized M the weight-only Marlin kernel runs far below dense
16-bit rate (Nemotron-3.5-Lightning-30B-A3B-NVFP4 cache-miss profile:
137 ms GPU per 6576-token chunk, 96.5% GEMM, marlin-dominated at ~300
TFLOPS effective). Weight-only quantization only has to win while the
GEMM is memory-bound; at chunked-prefill M it is compute-bound and the
dequant tax is paid once per K x N instead of M times through the slow
kernel. When M >= threshold, apply_fp8_marlin_linear /
apply_fp4_marlin_linear dequantize the original-format weight into a
shared 16-bit workspace reserved at weight-loading time and run the
full-rate GEMM (torch.mm / F.linear); below the threshold every call
stays on Marlin, so decode and spec-verify launches (M <= 8, including
inside captured decode graphs) keep the memory-bound winner.

The M-vs-threshold branch lives behind a torch custom op
(vllm::marlin_large_m_gemm) because a Python
`reshaped_x.size(0) >= threshold` in the traced apply is decided ONCE
by dynamo (a BACKED symint guard vLLM drops with evaluate_guards=False)
and baked into every compiled artifact; inductor compile-range
endpoints cannot fix that (measured falsification: decode collapsed
1.3568 -> 3.1145 ms/tok with the dequant copy_mul kernels x23/x21 in
the decode window under every range config). As an opaque op the branch
is re-decided eagerly per call from the real x.size(0): register_fake
returns the single meta valid for BOTH branches (a fresh contiguous
[M, N] tensor in the activation dtype, enforced in the impl), both the
Marlin lock workspace and the shared dequant workspace are declared in
mutates_args (hidden mutation would let the compiler cache or reorder
around the op), and the threshold is an op argument wired to
VLLM_MARLIN_LARGE_M_BF16 (0 = off, default; 1 = 512; >= 16 sets it
directly, so 512 and 4096 are both reachable for the sweep; values in
[2, 16) are rejected to protect the decode guard). The branch is a pure
function of M, so a cudagraph captured at some padded M legally freezes
exactly the branch eager execution would take at that M -- FULL capture
at decode shapes stays legal -- and the small-M arm inside the op is
the same factored-out 16-bit Marlin call sequence
(_marlin_16bit_gemm) the non-dispatched apply path runs, by
construction.

Per-decode-call added overhead (the poison axis): with the flag OFF
(default) the only addition is one getattr per apply call,
constant-folded away at trace time -- the compiled artifact is
unchanged. With the flag ON, decode steps replayed from captured
cudagraphs pay nothing (the op's Python body runs at capture time
only); non-captured regions pay one dispatcher round-trip plus the
branch compare per dispatched dense layer call (order 1-2 us of host
work, x23 dense layers, i.e. sub-0.01 ms/tok class at AL 3.00) -- which
is exactly why the serve A/B's decode acceptance criterion is every-rep
ITL within +-0.01 ms/tok backed by a decode-window kernel diff, not an
assumption.

Admission and memory semantics are unchanged from the eager dispatch:
fail-closed at prepare time to dense FP8 non-blockwise with 16-bit
activations and dense NVFP4 g16 with a scalar global scale only; a
256 MiB per-layer cap excludes lm_head-class layers; MoE stays excluded
by measurement (honest per-call expert dequant loses 0.77x/0.29x at
miss/hit chunks). Enabling retains the original quantized tensor and
dequant-ready 16-bit scales of each dispatched layer, plus one shared
per-(device, dtype) workspace sized by the largest dispatched K x N,
grown only during weight loading through a stable holder object (layer
contexts keep a reference across boot-time growth) and never allocated
in the forward; if the reserved workspace cannot host a layer in the
activation dtype the op fails closed to Marlin. Numerics reproduce the
Marlin kernel's effective dequantization (NVFP4 processed-scale
clamp-to-zero, FP8 pre-exponent-fusion scales); accumulation order
differs, so outputs match at kernel-test tolerance, not bitwise.

Falsifiable predictions for the serve A/B: on the DSpark recipe (32K shared
prefix, C=1, AL exactly 3.00) with the flag on, steady cache-miss TTFT
lands in the 634 ms class measured in the first serve A/B (-260 ms vs
its same-allocation 894.55 ms baseline), rehit -44, hit -11.5; every-rep decode ITL
inside +-0.01 ms/tok of the same-allocation bracket with copy_mul_slice_view
x23/x21 ABSENT from the decode-window kernel diff and small-M marlin
launches PRESENT, on sm103 and sm100.

Tested (tests/kernels/quantization/test_marlin_gemm.py): both dispatch
arms against the dequantized reference and each other (FP8 both weight
layouts x bias x fp16/bf16; NVFP4 both dtypes), NVFP4 clamp semantics,
a marlin_gemm spy asserting M <= 8 always selects Marlin while
dispatched M never calls it, threshold floor and workspace-cap
fail-closed gates, custom-threshold boundary, 512/4096 sweep-point
boundary dispatch, capture-vs-eager bitwise equality at M in {1, 2, 4}
(fp8 + nvfp4, atomic-add reduce forced off), opcheck schema/fake on
both branches, and a compiled toy region (fullgraph, dynamic=True)
proving a single artifact serves both branches with error_on_recompile
armed -- the traced-branch failure mode reproduces as a hard test
failure. The dynamo mechanism check also passes CPU-side through the
real apply path with the kernel stubbed; the GPU suite runs in-container
on the measurement hardware.

Measured A/B (on main @ 85c1365; identical serve recipe and marlin
kernel build in both arms, max_num_batched_tokens 8770, AL 3.00
synthetic dspark, in-allocation brackets, one node per run): steady cache-miss TTFT 648.69 vs 911.21 ms
(-262.5) at threshold 512 and 651.73 (-259.5) at 4096 on GB300 sm103;
685.69 vs 917.34 (-231.7) at 4096 on GB200 sm100. Every-rep decode ITL within the +-0.01 ms/tok band at 4096
(worst +0.0014); at 512 every rep measured FASTER than the same-run
baseline (1.3976/1.3967 vs min 1.4101) -- 512 is the shipped default
when armed, 4096 the documented hit-neutral alternative. Decode-window
kernel diff: copy_mul_dequant 0, nvjet_splitK 0, dense marlin 7700 on
both thresholds and both arches; miss-window engagement
vllm::marlin_large_m_gemm x545 + fused triton call-site kernels x870 in
the traced 5-chunk miss window (plan [8768, 8768, 8768, 6576, 1952];
545 = 5 x 109 dispatch-eligible linears). Both engagement counts are
call-site counts and therefore threshold-invariant: the M-vs-threshold
branch lives inside the opaque op, which is invoked for every eligible
linear regardless of the branch taken. The per-threshold branch receipt
is the dense-marlin count in the same window -- 635 at threshold 512 vs
844 at 4096, every other kernel label identical: the plan's 1952-token
tail chunk is below 4096, stays on Marlin there (+209 marlin kernels),
and dispatches at 512, consistent with the 2192/1952 hit chunks
dispatching at 512 only. Cost: KV headroom
-1.71 GiB absolute (177.40 -> 175.69); hit TTFT at 512 +7.6 ms
standalone (dequant on the 2192/1952 hit chunks) -- composed with the
final-split elision the merged 4144-token hit chunk inverts this into
a win (combined candidate: hit 100.27 vs 136.86 in-allocation).
Engaged accuracy adjudication (run on the pre-rebase
base tree -- this patch's runtime (vllm/) hunks are patch-identical on
both bases (vllm/-restricted patch-id 1bb0141f); its test/bench files
later took a disclosed 9-line lint-only amend; gsm8k 5-shot
n=250 temp 0, real rejection sampling, APC off, every prompt through
the dispatched path): flex -0.0040 (+-0.0612), strict -0.0120 (+-0.0633), 109/250
byte-identical, flips symmetric, real acceptance length 3.5183 vs
3.5159.

Interaction with vllm-project#53014 (FlashInfer cute-dsl W4A16 NVFP4 default):
upstream now prefers FlashInferCuteDslNvFp4W4A16LinearKernel over
Marlin for W4A16_NVFP4 under --linear-backend=auto on SM100/103, so on
those parts the NVFP4 half of this patch no longer engages by default.
The measured recipe pins --linear-backend=marlin, whose backend-map
entry (MarlinNvFp4LinearKernel -> apply_fp4_marlin_linear, where the
large-M context attaches during Marlin weight processing) is untouched
by vllm-project#53014; under that pin -- and everywhere Marlin remains the
selected kernel -- the dispatch engages exactly as measured. The FP8
channelwise half (marlin_utils_fp8), which carries most of the win, is
unaffected by vllm-project#53014 entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Rishi Puri <riship@nvidia.com>
mylibrar pushed a commit to tanyuqian/vllm that referenced this pull request Sep 3, 2026
Signed-off-by: Siyuan Fu <siyuanf@nvidia.com>
sheralskumar pushed a commit to sheralskumar/vllm that referenced this pull request Sep 8, 2026
Signed-off-by: Siyuan Fu <siyuanf@nvidia.com>
D-G-Dimitrov pushed a commit to D-G-Dimitrov/vllm that referenced this pull request Sep 8, 2026
Signed-off-by: Siyuan Fu <siyuanf@nvidia.com>
(cherry picked from commit 882ca8d)
puririshi98 added a commit to puririshi98/vllm that referenced this pull request Sep 9, 2026
…op (dequant + 16-bit GEMM, opt-in)

At prefill-sized M the weight-only Marlin kernel runs far below dense
16-bit rate (Nemotron-3.5-Lightning-30B-A3B-NVFP4 cache-miss profile:
137 ms GPU per 6576-token chunk, 96.5% GEMM, marlin-dominated at ~300
TFLOPS effective). Weight-only quantization only has to win while the
GEMM is memory-bound; at chunked-prefill M it is compute-bound and the
dequant tax is paid once per K x N instead of M times through the slow
kernel. When M >= threshold, apply_fp8_marlin_linear /
apply_fp4_marlin_linear dequantize the original-format weight into a
shared 16-bit workspace reserved at weight-loading time and run the
full-rate GEMM (torch.mm / F.linear); below the threshold every call
stays on Marlin, so decode and spec-verify launches (M <= 8, including
inside captured decode graphs) keep the memory-bound winner.

The M-vs-threshold branch lives behind a torch custom op
(vllm::marlin_large_m_gemm) because a Python
`reshaped_x.size(0) >= threshold` in the traced apply is decided ONCE
by dynamo (a BACKED symint guard vLLM drops with evaluate_guards=False)
and baked into every compiled artifact; inductor compile-range
endpoints cannot fix that (measured falsification: decode collapsed
1.3568 -> 3.1145 ms/tok with the dequant copy_mul kernels x23/x21 in
the decode window under every range config). As an opaque op the branch
is re-decided eagerly per call from the real x.size(0): register_fake
returns the single meta valid for BOTH branches (a fresh contiguous
[M, N] tensor in the activation dtype, enforced in the impl), both the
Marlin lock workspace and the shared dequant workspace are declared in
mutates_args (hidden mutation would let the compiler cache or reorder
around the op), and the threshold is an op argument wired to
VLLM_MARLIN_LARGE_M_BF16 (0 = off, default; 1 = 512; >= 16 sets it
directly, so 512 and 4096 are both reachable for the sweep; values in
[2, 16) are rejected to protect the decode guard). The branch is a pure
function of M, so a cudagraph captured at some padded M legally freezes
exactly the branch eager execution would take at that M -- FULL capture
at decode shapes stays legal -- and the small-M arm inside the op is
the same factored-out 16-bit Marlin call sequence
(_marlin_16bit_gemm) the non-dispatched apply path runs, by
construction.

Per-decode-call added overhead (the poison axis): with the flag OFF
(default) the only addition is one getattr per apply call,
constant-folded away at trace time -- the compiled artifact is
unchanged. With the flag ON, decode steps replayed from captured
cudagraphs pay nothing (the op's Python body runs at capture time
only); non-captured regions pay one dispatcher round-trip plus the
branch compare per dispatched dense layer call (order 1-2 us of host
work, x23 dense layers, i.e. sub-0.01 ms/tok class at AL 3.00) -- which
is exactly why the serve A/B's decode acceptance criterion is every-rep
ITL within +-0.01 ms/tok backed by a decode-window kernel diff, not an
assumption.

Admission and memory semantics are unchanged from the eager dispatch:
fail-closed at prepare time to dense FP8 non-blockwise with 16-bit
activations and dense NVFP4 g16 with a scalar global scale only; a
256 MiB per-layer cap excludes lm_head-class layers; MoE stays excluded
by measurement (honest per-call expert dequant loses 0.77x/0.29x at
miss/hit chunks). Enabling retains the original quantized tensor and
dequant-ready 16-bit scales of each dispatched layer, plus one shared
per-(device, dtype) workspace sized by the largest dispatched K x N,
grown only during weight loading through a stable holder object (layer
contexts keep a reference across boot-time growth) and never allocated
in the forward; if the reserved workspace cannot host a layer in the
activation dtype the op fails closed to Marlin. Numerics reproduce the
Marlin kernel's effective dequantization (NVFP4 processed-scale
clamp-to-zero, FP8 pre-exponent-fusion scales); accumulation order
differs, so outputs match at kernel-test tolerance, not bitwise.

Falsifiable predictions for the serve A/B: on the DSpark recipe (32K shared
prefix, C=1, AL exactly 3.00) with the flag on, steady cache-miss TTFT
lands in the 634 ms class measured in the first serve A/B (-260 ms vs
its same-allocation 894.55 ms baseline), rehit -44, hit -11.5; every-rep decode ITL
inside +-0.01 ms/tok of the same-allocation bracket with copy_mul_slice_view
x23/x21 ABSENT from the decode-window kernel diff and small-M marlin
launches PRESENT, on sm103 and sm100.

Tested (tests/kernels/quantization/test_marlin_gemm.py): both dispatch
arms against the dequantized reference and each other (FP8 both weight
layouts x bias x fp16/bf16; NVFP4 both dtypes), NVFP4 clamp semantics,
a marlin_gemm spy asserting M <= 8 always selects Marlin while
dispatched M never calls it, threshold floor and workspace-cap
fail-closed gates, custom-threshold boundary, 512/4096 sweep-point
boundary dispatch, capture-vs-eager bitwise equality at M in {1, 2, 4}
(fp8 + nvfp4, atomic-add reduce forced off), opcheck schema/fake on
both branches, and a compiled toy region (fullgraph, dynamic=True)
proving a single artifact serves both branches with error_on_recompile
armed -- the traced-branch failure mode reproduces as a hard test
failure. The dynamo mechanism check also passes CPU-side through the
real apply path with the kernel stubbed; the GPU suite runs in-container
on the measurement hardware.

Measured A/B (on main @ 85c1365; identical serve recipe and marlin
kernel build in both arms, max_num_batched_tokens 8770, AL 3.00
synthetic dspark, in-allocation brackets, one node per run): steady cache-miss TTFT 648.69 vs 911.21 ms
(-262.5) at threshold 512 and 651.73 (-259.5) at 4096 on GB300 sm103;
685.69 vs 917.34 (-231.7) at 4096 on GB200 sm100. Every-rep decode ITL within the +-0.01 ms/tok band at 4096
(worst +0.0014); at 512 every rep measured FASTER than the same-run
baseline (1.3976/1.3967 vs min 1.4101) -- 512 is the shipped default
when armed, 4096 the documented hit-neutral alternative. Decode-window
kernel diff: copy_mul_dequant 0, nvjet_splitK 0, dense marlin 7700 on
both thresholds and both arches; miss-window engagement
vllm::marlin_large_m_gemm x545 + fused triton call-site kernels x870 in
the traced 5-chunk miss window (plan [8768, 8768, 8768, 6576, 1952];
545 = 5 x 109 dispatch-eligible linears). Both engagement counts are
call-site counts and therefore threshold-invariant: the M-vs-threshold
branch lives inside the opaque op, which is invoked for every eligible
linear regardless of the branch taken. The per-threshold branch receipt
is the dense-marlin count in the same window -- 635 at threshold 512 vs
844 at 4096, every other kernel label identical: the plan's 1952-token
tail chunk is below 4096, stays on Marlin there (+209 marlin kernels),
and dispatches at 512, consistent with the 2192/1952 hit chunks
dispatching at 512 only. Cost: KV headroom
-1.71 GiB absolute (177.40 -> 175.69); hit TTFT at 512 +7.6 ms
standalone (dequant on the 2192/1952 hit chunks) -- composed with the
final-split elision the merged 4144-token hit chunk inverts this into
a win (combined candidate: hit 100.27 vs 136.86 in-allocation).
Engaged accuracy adjudication (run on the pre-rebase
base tree, where this patch's runtime (vllm/) hunks had vllm/-restricted
patch-id 1bb0141f; its test/bench files later took a disclosed 9-line
lint-only amend. On this base the vllm/ patch-id is 78146196: the only
interdiff is dropping the `g_idx=None, perm=None` kwargs at the
`ops.marlin_gemm` call sites in `_marlin_16bit_gemm`, which vllm-project#54809
removed from the op signature -- they selected the non-act-order path,
the only one vllm-project#54809 left, so the patch's Python is behaviorally
unchanged. vllm-project#54809 also rewrote the Marlin kernel itself; the gsm8k
receipt has not been re-run on it. gsm8k 5-shot
n=250 temp 0, real rejection sampling, APC off, every prompt through
the dispatched path): flex -0.0040 (+-0.0612), strict -0.0120 (+-0.0633), 109/250
byte-identical, flips symmetric, real acceptance length 3.5183 vs
3.5159.

Interaction with vllm-project#53014 (FlashInfer cute-dsl W4A16 NVFP4 default):
upstream now prefers FlashInferCuteDslNvFp4W4A16LinearKernel over
Marlin for W4A16_NVFP4 under --linear-backend=auto on SM100/103, so on
those parts the NVFP4 half of this patch no longer engages by default.
The measured recipe pins --linear-backend=marlin, whose backend-map
entry (MarlinNvFp4LinearKernel -> apply_fp4_marlin_linear, where the
large-M context attaches during Marlin weight processing) is untouched
by vllm-project#53014; under that pin -- and everywhere Marlin remains the
selected kernel -- the dispatch engages exactly as measured. The FP8
channelwise half (marlin_utils_fp8), which carries most of the win, is
unaffected by vllm-project#53014 entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Rishi Puri <riship@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation nvidia quantization ready ONLY add when PR is ready to merge/full CI is needed

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants