Skip to content

[SM120] Support GLM DSA serving on RTX PRO 6000 - #29580

Closed
ronhuafeng wants to merge 1 commit into
sgl-project:mainfrom
ronhuafeng:sglang-29562-sm120-dsa
Closed

ronhuafeng wants to merge 1 commit into
sgl-project:mainfrom
ronhuafeng:sglang-29562-sm120-dsa

Conversation

@ronhuafeng

@ronhuafeng ronhuafeng commented Jun 28, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Fixes #29562.

This PR adds a native SM120 serving path for GLM DSA / GLM-5.2-NVFP4 on RTX PRO
6000 Blackwell. With the default GLM launch, the patched path starts the TP8
server, captures CUDA graphs, passes /health, and completes a minimal
/generate request on exact SM120 hardware.

The reporter path had multiple independent blockers:

  1. GLM-5.2-NVFP4 ModelOpt metadata excludes mlp.shared_experts from FP4
    quantization, but shared-expert fusion remapped those unquantized tensors
    into routed packed FP4 expert slots. That caused the initial 3072 vs
    6144 loader mismatch.
  2. After disabling shared-expert fusion, SGLang still selected trtllm DSA
    backends for fp8 KV on SM120. The underlying FlashInfer TRTLLM-gen MLA path
    reports unsupported architecture on SM120.
  3. Other DSA backend choices still reached
    deep_gemm.get_paged_mqa_logits_metadata, which is unsupported on SM120 in
    the currently pinned sgl-deep-gemm wheel.
  4. With validated FlashInfer/DeepGEMM source builds, the old decode CUDA graph
    max_bs=512 default OOMed during graph capture on 96GB RTX PRO 6000. An
    independent dev-cu13 run also found full decode graph capture can hang.
    The current patch keeps SGLang's default full decode graph backend for
    consistency with the existing decode path, caps decode max_bs at 256 by
    default for this SM120 GLM DSA case, and still lets users explicitly select
    tc_piecewise as an alternative decode CUDA graph mode.
  5. GLM-5.2-NVFP4 ships index_topk_pattern: null; the DSA IndexShare layer
    layout is encoded in indexer_types, so SGLang needs to derive the same F/S
    sharing pattern from checkpoint metadata instead of requiring a manual model
    config override.

Modifications

  • glm4_moe.py: detect ModelOpt FP4 checkpoints where shared experts are not
    quantized and automatically disable GLM shared-expert fusion for that case.
  • model_config.py: derive the DSA top-k sharing pattern from
    checkpoint-native indexer_types when index_topk_pattern is absent,
    preserving explicit pattern precedence and the existing frequency/offset
    fallback.
  • server_args.py: add flashinfer_sparse_mla as a DSA backend choice and
    select it by default for fp8 DSA on SM120. For GLM DSA on SM120, preserve the
    default full decode CUDA graph backend, cap the default decode graph
    max_bs to 256, and use the derived DSA top-k sharing pattern for the TBO
    guard. Hopper and SM100 paths keep their existing default backend selections.
  • dsa_backend.py: call FlashInfer sparse MLA with the SM120 contract required
    by the native kernels: BF16 query, packed uint8 KV, page size 64, int32 sparse
    page tables, and kv_scale_format="arbitrary_fp32" for GLM.
  • forward_mla.py: keep TRTLLM-style FP8 RoPE fusion limited to the TRTLLM
    backend so the sparse MLA path uses its own input contract.
  • Dockerfile: add optional repo+commit source build args for FlashInfer and
    DeepGEMM so the image can use the validated dependency commits before wheels
    are released.

Validated source commits:

Released wheel gap checked on SM120:

  • flashinfer-python==0.6.13, flashinfer-cubin==0.6.13, and
    flashinfer-jit-cache==0.6.13+cu130 import, but the released
    trtllm_batch_decode_with_kv_cache_mla signature still lacks
    kv_scale_format, so this GLM sparse MLA path needs a FlashInfer source
    build until the next wheel release.
  • sgl-deep-gemm is still at 0.1.3, so the DeepGEMM SM120 metadata path needs
    the source commit above until a new SGLang DeepGEMM wheel is published.

The Dockerfile source override path pins the validated repos by commit hash:

--build-arg FLASHINFER_SOURCE_REPO=https://github.com/flashinfer-ai/flashinfer.git
--build-arg FLASHINFER_SOURCE_REF=5f2bdc41f9ffecef9d8ed590e688e7c0f108504f
--build-arg FLASHINFER_CUDA_ARCH_LIST=12.0f
--build-arg DEEPGEMM_SOURCE_REPO=https://github.com/deepseek-ai/DeepGEMM.git
--build-arg DEEPGEMM_SOURCE_REF=2073ddb2814892014c33ef4cd1c7d4c148baf1fe

I also validated this repo+commit install sequence on the exact SM120 base
image: DeepGEMM was installed from the pinned GitHub commit as
deep_gemm 2.5.0+2073ddb; FlashInfer was installed from the pinned GitHub
commit with FLASHINFER_CUDA_ARCH_LIST=12.0f, including flashinfer-python,
flashinfer-cubin, and flashinfer-jit-cache 0.6.13. The final probe printed
has_kv_scale_format True,
deepgemm_metadata_ok Tensor (189, 2) torch.int32 cuda, and
repo_commit_install_ok.

Accuracy Tests

This PR changes the serving path for a previously unsupported SM120 GLM DSA
configuration. I did not run a full downstream accuracy benchmark such as GSM8K.
The issue-specific accuracy-relevant validation was functional generation on the
exact target hardware:

  • default GLM-5.2-NVFP4 TP8 launch reached /health
  • minimal /generate completed with HTTP 200 and 16 completion tokens
  • the default backend change is gated to SM120, and focused tests cover the
    SM120 selection path plus existing non-SM120 routing behavior

FlashInfer sparse MLA correctness for the SM120 kernels is covered upstream by
FlashInfer's SM120 sparse MLA test suite in
flashinfer-ai/flashinfer#3395.

Speed Tests and Profiling

I did not run a throughput benchmark or profiler sweep for this patch. The
performance-relevant validation was that the SM120 path keeps CUDA graphs
enabled:

  • old decode CUDA graph max_bs=512 OOMed during graph capture on 96GB RTX PRO
    6000
  • independent dev-cu13 validation found full decode graph capture can hang,
    while tc_piecewise decode capture works
  • the patched default keeps the full decode graph backend for consistency
    with the existing decode path and caps GLM DSA SM120 decode graph max_bs to
    256
  • explicit tc_piecewise remains available as an alternative decode CUDA graph
    mode
  • prefill CUDA graph capture completed
  • decode CUDA graph capture completed
  • FlashInfer autotune completed

Tests

Exact SM120 hardware:

  • 8 x NVIDIA RTX PRO 6000 Blackwell Server Edition
  • compute capability SM120
  • driver 580.159.03, CUDA 13.0 host / 13.0.1 container
  • base image lmsysorg/sglang:dev-glm52-nvfp4
  • PyTorch 2.11.0+cu130

Baseline:

  • reporter-style launch failed in the GLM shared-expert fusion loader path with
    the 3072 vs 6144 mismatch
  • --disable-shared-experts-fusion got past load/KV allocation, then failed in
    deep_gemm.get_paged_mqa_logits_metadata(...): Unsupported architecture
  • explicit legacy --dsa-prefill-backend trtllm --dsa-decode-backend trtllm
    failed with TllmGenFmhaRunner ... Unsupported architecture

Patched dependency probe:

torch 2.11.0+cu130
cuda_cap (12, 0)
flashinfer_python 0.6.13
flashinfer-cubin 0.6.13
flashinfer-jit-cache 0.6.13
deep-gemm 2.5.0+2073ddb
deep_gemm 2.5.0+2073ddb
sgl-deep-gemm 0.1.3
has_backend True
has_kv_scale_format True
deepgemm_metadata_ok Tensor (189, 2) torch.int32 cuda
repo_commit_install_ok

Patched default launch:

python3 -m sglang.launch_server \
  --model-path nvidia/GLM-5.2-NVFP4 \
  --tensor-parallel-size 8 \
  --quantization modelopt_fp4 \
  --tool-call-parser glm47 \
  --reasoning-parser glm45 \
  --trust-remote-code \
  --mem-fraction-static 0.8 \
  --host 0.0.0.0 \
  --port 30000

Result:

  • selected prefill=flashinfer_sparse_mla, decode=flashinfer_sparse_mla
  • auto-disabled incompatible shared-expert fusion
  • allocated fp8 KV cache successfully (368768 tokens/rank, 21.11 GB/rank)
  • FlashInfer autotune completed
  • prefill CUDA graph capture completed
  • decode CUDA graph capture completed
  • /health passed
  • minimal /generate completed with HTTP 200 and 16 completion tokens

Latest SM120 rerun after the tc_piecewise decode graph follow-up, on PR head
cf638d6e6b0b1138128196d2349da18460b06d44:

  • source-pinned dependency image still reports has_kv_scale_format True and
    DeepGEMM metadata support
  • focused unit checks passed:
    99 passed, 18 warnings, 10 subtests passed
  • default launch above selected flashinfer_sparse_mla
  • cuda_graph_config.decode.backend defaulted to tc_piecewise with
    max_bs=256
  • all eight ranks loaded GlmMoeDsaForCausalLM with modelopt_fp4 / NVFP4
  • allocated fp8 KV cache successfully (368768 tokens/rank, 21.11 GB/rank)
  • prefill CUDA graph capture completed with backend=tc_piecewise
  • decode CUDA graph capture completed with backend=tc_piecewise and
    bs=[1, 2, 4, ..., 256]
  • /health passed
  • minimal /generate completed with HTTP 200 and 16 completion tokens
  • failure grep was clean for Unsupported architecture, TllmGenFmhaRunner,
    Traceback, OutOfMemory, RuntimeError, ValueError, 3072, and 6144

Local checks:

git diff --check
python3 -m py_compile \
  python/sglang/srt/layers/attention/dsa_backend.py \
  python/sglang/srt/server_args.py
python3 scripts/ci/check_registered_tests.py
python3 scripts/ci/check_no_registered_tests_in_package.py

Follow-up after independent dev-cu13 validation of the native path:

git diff --check
python3 -m py_compile \
  python/sglang/srt/server_args.py \
  test/registered/unit/server_args/test_server_args.py
python3 scripts/ci/check_registered_tests.py
python3 scripts/ci/check_no_registered_tests_in_package.py
PYTHONPATH=python python3 -m pytest \
  test/registered/unit/server_args/test_server_args.py \
  test/registered/unit/models/test_glm4_moe.py \
  test/registered/unit/models/test_deepseek_mla_forward.py -q
# 99 passed, 18 warnings, 10 subtests passed

SM120 runtime rerun for the earlier tc_piecewise default follow-up:

python3 -m sglang.launch_server \
  --model-path nvidia/GLM-5.2-NVFP4 \
  --tensor-parallel-size 8 \
  --quantization modelopt_fp4 \
  --tool-call-parser glm47 \
  --reasoning-parser glm45 \
  --trust-remote-code \
  --mem-fraction-static 0.8 \
  --host 0.0.0.0 \
  --port 30000

Result: selected flashinfer_sparse_mla, defaulted decode CUDA graph backend to
tc_piecewise, completed prefill and decode CUDA graph capture, passed
/health, returned HTTP 200 for minimal /generate, and had a clean failure
grep.

Latest follow-up for GLM-5.2 indexer_types metadata and default full decode
graph validation, on PR head 005ffc65dc0f72d42a1436c31ef129ed98cccc58:

  • cached GLM-5.2-NVFP4 config has index_topk_pattern: null and 78
    indexer_types entries describing the full/shared layer layout
  • source-pinned image focused tests passed:
    97 passed, 18 warnings, 10 subtests passed
  • the default launch above selected flashinfer_sparse_mla
  • cuda_graph_config.decode.backend remained full with max_bs=256
  • prefill CUDA graph capture completed with backend=tc_piecewise
  • decode CUDA graph capture completed with backend=full and
    bs=[1, 2, 4, ..., 256]
  • /health passed
  • minimal /generate completed with HTTP 200 and 16 completion tokens
  • no index_topk_pattern override or manual decode backend flag was used
  • focused tests cover default full, explicit full, explicit tc_piecewise,
    and explicit disabled decode CUDA graph behavior
  • previous explicit full checks also passed at --cuda-graph-max-bs-decode 32
    and at the PR's automatic max_bs=256 cap

GitHub Actions:

  • lint passed on head cf638d6e6b0b1138128196d2349da18460b06d44
  • the latest amended head 005ffc65dc0f72d42a1436c31ef129ed98cccc58 needs the
    normal GitHub check rerun
  • remaining red PR checks are pr-gate failures due missing maintainer run-ci
    authorization/label, not test job failures

Validation gaps:

  • I did not rerun Hopper/H200 or SM100/B200 full-server controls in this patch.
  • The backend default change is gated to SM120, and the focused tests cover the
    SM120 selection path plus existing non-SM120 routing behavior.
  • Released FlashInfer and sgl-deep-gemm wheels still need follow-up releases.
    Until then, the Dockerfile source-pinned build args above are the validated
    way for the SGLang image to carry this path.

Checklist

  • Format your code according to the Format code with pre-commit. git diff --check and GitHub lint passed.
  • Add unit tests according to the Run and add unit tests. Focused unit tests were added/updated for GLM fusion, SM120 DSA routing, sparse MLA call contract, DSA indexer_types pattern derivation, and non-SM120 routing.
  • Update documentation according to Write documentations. No separate user-facing docs are needed; the PR body documents the source-pinned dependency path and wheel gap.
  • Provide accuracy and speed benchmark results according to Test the accuracy and Benchmark the speed. See the Accuracy Tests and Speed Tests sections for the issue-specific validation and remaining benchmark gap.
  • Follow the SGLang code style guidance.

Review and Merge Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • Common commands include /tag-and-rerun-ci, /tag-run-ci-label, /rerun-failed-ci
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

CI States

Latest PR Test (Base): ❌ Run #28357834679
Latest PR Test (Extra): ❌ Run #28357834575


CI States

Latest PR Test (Base): ❌ Run #28352062088
Latest PR Test (Extra): ❌ Run #28352061942

@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 the flashinfer_sparse_mla backend as the default prefill and decode attention mechanism for SM120 architectures, adding fallback implementations (such as a chunked PyTorch fallback for FP8 paged MQA logits and a Triton-based sparse MLA fallback) to support environments with older wheels. It also adds logic to disable shared expert fusion for GLM ModelOpt FP4 checkpoints when shared experts are excluded from quantization, and disables CUDA graphs by default during SM120 fallbacks to prevent excessive memory allocation. The review feedback highlights several critical issues, including multiple missing imports (Phase, log_info_on_rank0, envs, and QuantizationConfig) that would cause runtime NameError exceptions, a potential IndexError when parsing empty architecture lists, unreachable dead code in _forward_trtllm, and a performance optimization to increase pages_per_chunk to reduce loop overhead during eager execution.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread python/sglang/srt/server_args.py Outdated
Comment on lines +3120 to +3128
def _disable_cuda_graph_for_sm120_dsa_fallback(self):
"""Keep the SM120 DSA torch fallback on eager execution by default.

The fallback is selected when older DeepGEMM/FlashInfer wheels do not
have native sm_120 paged-MQA/TRTLLM-MLA kernels. It is functional under
eager execution, while decode CUDA graph capture uses an over-allocated
static page table that makes the PyTorch fallback allocate/capture
impractically large tensors.
"""

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 Phase enum is used in this method (e.g., Phase.PREFILL and Phase.DECODE), but it might not be imported in server_args.py. To prevent a potential NameError on startup, we should import Phase from sglang.srt.model_executor.cuda_graph_config.

Suggested change
def _disable_cuda_graph_for_sm120_dsa_fallback(self):
"""Keep the SM120 DSA torch fallback on eager execution by default.
The fallback is selected when older DeepGEMM/FlashInfer wheels do not
have native sm_120 paged-MQA/TRTLLM-MLA kernels. It is functional under
eager execution, while decode CUDA graph capture uses an over-allocated
static page table that makes the PyTorch fallback allocate/capture
impractically large tensors.
"""
def _disable_cuda_graph_for_sm120_dsa_fallback(self):
"""Keep the SM120 DSA torch fallback on eager execution by default.
The fallback is selected when older DeepGEMM/FlashInfer wheels do not
have native sm_120 paged-MQA/TRTLLM-MLA kernels. It is functional under
eager execution, while decode CUDA graph capture uses an over-allocated
static page table that makes the PyTorch fallback allocate/capture
impractically large tensors.
"""
from sglang.srt.model_executor.cuda_graph_config import Phase

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in the current head: Phase is imported from sglang.srt.model_executor.cuda_graph_config.

Comment on lines +1502 to +1507
log_info_on_rank0(
logger,
"GLM ModelOpt FP4 checkpoint excludes shared experts from "
"quantization, so they cannot be fused into routed FP4 expert slots. "
"Shared experts fusion optimization is disabled.",
)

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 utility function log_info_on_rank0 is used here but might not be imported in glm4_moe.py. To prevent a potential NameError at runtime, we should import it.

Suggested change
log_info_on_rank0(
logger,
"GLM ModelOpt FP4 checkpoint excludes shared experts from "
"quantization, so they cannot be fused into routed FP4 expert slots. "
"Shared experts fusion optimization is disabled.",
)
from sglang.srt.utils import log_info_on_rank0
log_info_on_rank0(
logger,
"GLM ModelOpt FP4 checkpoint excludes shared experts from "
"quantization, so they cannot be fused into routed FP4 expert slots. "
"Shared experts fusion optimization is disabled.",
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in the current head: log_info_on_rank0 is imported from sglang.srt.utils before this call site.

Comment on lines 49 to 50
is_sm120_supported,
)

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 envs module is used in dsa_indexer.py (e.g., envs.SGLANG_FP8_PAGED_MQA_LOGITS_TORCH.get()), but it is not imported in this file. This will cause a NameError during execution. We should import envs from sglang.srt.utils.

Suggested change
is_sm120_supported,
)
is_sm120_supported,
)
from sglang.srt.utils import envs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in the current head: envs is imported from sglang.srt.environ in dsa_indexer.py.


kv_value = kv_value_raw.contiguous().view(dtype=FP8_DTYPE).to(torch.float32)
kv_value = kv_value.view(batch_size, max_padded_seq, head_dim)
pages_per_chunk = 4

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.

medium

The pages_per_chunk is set to a very small constant value of 4. While this keeps the memory footprint extremely low during CUDA graph warmup, it introduces significant Python loop overhead and kernel launch latency during eager execution (e.g., for a sequence length of 2048 and block size of 16, the loop will run 32 times, launching hundreds of PyTorch kernels).

Consider increasing pages_per_chunk to a larger value like 64 or 128. This still keeps the memory footprint very small (a few tens of megabytes) while dramatically reducing the loop iterations and CPU overhead in the latency-sensitive decode path.

Suggested change
pages_per_chunk = 4
pages_per_chunk = 64

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This thread is outdated against the current head. The pages_per_chunk = 4 loop is no longer present in python/sglang/srt/layers/attention/dsv4/indexer.py; the current implementation gathers the page table slice and runs the batched computation directly, so there is no chunk-size constant to increase here.

I am leaving this as no-code-change rather than adding a new tuning knob that is not used by the current code.

Comment on lines +346 to +348
model_arch = getattr(
model_runner.model_config.hf_config, "architectures", [None]
)[0]

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.

medium

If architectures is an empty list [] in the model's Hugging Face config, calling [0] on it will raise an IndexError: list index out of range and crash the server startup. It is safer to handle empty lists defensively.

        architectures = getattr(
            model_runner.model_config.hf_config, "architectures", []
        )
        model_arch = architectures[0] if architectures else None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Updated the current head to handle an empty architectures list defensively:

architectures = getattr(model_runner.model_config.hf_config, "architectures", [])
model_arch = architectures[0] if architectures else None

This keeps the GLM path unchanged when the architecture is present and falls back to "auto" instead of raising IndexError when the list is empty.

Local checks: python3 -m py_compile python/sglang/srt/layers/attention/dsa_backend.py and git diff --check passed.

Comment on lines +114 to +116
def _modelopt_fp4_excludes_shared_experts(
quant_config: Optional[QuantizationConfig],
) -> bool:

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.

medium

Using QuantizationConfig as a type annotation can raise a NameError at import time if QuantizationConfig is not imported in glm4_moe.py. Since type annotations are optional, we can remove it or use a string literal to be safe.

Suggested change
def _modelopt_fp4_excludes_shared_experts(
quant_config: Optional[QuantizationConfig],
) -> bool:
def _modelopt_fp4_excludes_shared_experts(
quant_config,
) -> bool:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in the current head: QuantizationConfig is imported, so this annotation is defined at import time.

Comment on lines +2513 to +2517
extra_kwargs = {}
if flashinfer_backend == "sparse" and _flashinfer_trtllm_mla_accepts_kwarg(
"kv_scale_format"
):
extra_kwargs["kv_scale_format"] = self.flashinfer_sparse_mla_kv_scale_format

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.

medium

Since _forward_trtllm returns early on line 2496 when flashinfer_backend == "sparse", the check if flashinfer_backend == "sparse" on line 2514 is unreachable dead code. Consequently, extra_kwargs will always be empty, and backend in trtllm_batch_decode_with_kv_cache_mla will always be "trtllm-gen". We can simplify this and remove the unreachable code.

Suggested change
extra_kwargs = {}
if flashinfer_backend == "sparse" and _flashinfer_trtllm_mla_accepts_kwarg(
"kv_scale_format"
):
extra_kwargs["kv_scale_format"] = self.flashinfer_sparse_mla_kv_scale_format
extra_kwargs = {}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This thread appears outdated. In the current head, the sparse path still populates extra_kwargs["kv_scale_format"] and passes it into the backend call, so this simplification does not apply to the current code.

@ronhuafeng
ronhuafeng force-pushed the sglang-29562-sm120-dsa branch from 5312378 to 66cb38c Compare June 28, 2026 22:50
@ronhuafeng

Copy link
Copy Markdown
Contributor Author

Follow-up pushed in 8af780a13:

  • Added a defensive guard for an empty hf_config.architectures list before selecting the GLM-specific sparse MLA KV-scale format.
  • Removed the unreachable kv_scale_format/signature-probe branch in _forward_trtllm; the flashinfer_backend == "sparse" path returns through the Triton sparse MLA fallback before the TRTLLM call.
  • The earlier import/type-annotation concerns for Phase, log_info_on_rank0, envs, and QuantizationConfig are already covered in the current head.

I kept pages_per_chunk = 4 for now because this fallback is intentionally conservative on peak temporary memory; I would prefer to tune that separately with latency/memory data rather than change the review patch mechanically.

Validation after this follow-up:

  • PYTHONPATH=python python3 -m py_compile python/sglang/srt/layers/attention/dsa_backend.py
  • git diff --check
  • pre-commit run --files python/sglang/srt/layers/attention/dsa_backend.py
  • PYTHONPATH=python python3 -m pytest test/registered/unit/server_args/test_server_args.py test/registered/unit/models/test_glm4_moe.py test/registered/unit/models/test_deepseek_mla_forward.py -q (97 passed, plus 10 subtests)

@zorrofox

Copy link
Copy Markdown

Independently validated on a separate 8× RTX PRO 6000 Blackwell Server (SM120) box, base image lmsysorg/sglang:dev-cu13-glm52-nvfp4 + the exact dependency commits from this PR (DeepGEMM@2073ddb, flashinfer@5f2bdc41). Hotfix-style install (pip from source inside the container, ~21 min) + this PR's diff applied to the in-image sglang. A few data points:

1. End-to-end correctness ✅

All auto-selects work as described: prefill=decode=flashinfer_sparse_mla, moe_runner=flashinfer_cutlass, shared-experts-fusion auto-disabled with the expected log line. With --json-model-override-args '{"index_topk_pattern":"FFF...SSS"}' (78-char F/S pattern derived from the checkpoint's indexer_types), GSM8K-style spot checks pass 5/5 with coherent reasoning output. deepgemm_metadata_ok and has_kv_scale_format=True both confirmed.

2. Decode CUDA graph: full backend hangs, tc_piecewise works

With the default cuda_graph_config.decode.backend=full, capture begins at the first bs (=32 in my run) and never returns — observed 36 min with no progress before timeout. Prefill tc_piecewise capture completes in 75 s, and eager-mode decode runs fine, so the kernels themselves work; the hang is specific to full-graph capture of the flashinfer_sparse_mla decode path.

[02:22:36 TP0] Capture target decode CUDA graph begin. backend=full, bs=[1, 2, 4, 8, 12, 16, 24, 32], avail mem=13.46 GB
(no further output for 36 min)

Workaround: --cuda-graph-backend-decode tc_piecewise captures fine (~same 290 s to health as eager) and recovers full performance — see table below. Might be worth defaulting decode to tc_piecewise in _apply_glm_dsa_sm120_cuda_graph_memory_defaults until the full capture issue is sorted.

(Tested against this PR's diff at 35450ad0 minus the 1-line architectures guard, on base lmsysorg/sglang:dev-cu13-glm52-nvfp4 = sglang 22dce572. Your validation used dev-glm52-nvfp4 and reports decode capture completing — so the hang is likely a base-image / sglang-commit delta rather than this PR's code, but users on the cu13 dev image will hit it.)

3. Throughput (TP=8, 2K/512 fixed-len, bench_serving --random-range-ratio 1.0, ignore_eos)

cc eager tok/s eager TPOT tc_piecewise tok/s TPOT
1 11.4 87.4 ms 44.7 22.0 ms
8 85.6 89.1 ms 224.7 31.4 ms
16 168.1 91.5 ms 382.9 37.8 ms
32 332.1 90.8 ms 572.7 49.9 ms
8 (8K/1K) 81.1 90.2 ms 197.0 32.1 ms

Eager TPOT is flat ~90 ms (host overhead floor). With tc_piecewise decode the same box outperforms the community vLLM B12X build (442.7 tok/s @ cc=32, TPOT 63 ms, same workload/tokens) by ~29% — so the FlashInfer #3395 SM120 sparse MLA kernel looks genuinely faster than the B12X equivalent. Single-run numbers; happy to rerun if useful.

4. Minor: index_topk_pattern for GLM-5.2

The nvidia checkpoint ships index_topk_pattern: null in config.json, but the IndexShare layout needs a 78-char F/S string (derivable from indexer_types). Without the override the model loads but produces garbage. Might be worth a note in the PR description or an auto-derive in server_args since it's a 5.2-specific footgun independent of this PR.

Happy to re-test if you push a decode-capture fix.

@ronhuafeng
ronhuafeng force-pushed the sglang-29562-sm120-dsa branch from 35450ad to cf638d6 Compare June 29, 2026 06:05
@ronhuafeng

Copy link
Copy Markdown
Contributor Author

Thanks for the independent run; I pushed follow-up commit cf638d6e6 to make the working workaround the GLM DSA SM120 default.

Changes:

  • In _apply_glm_dsa_sm120_cuda_graph_memory_defaults, when decode CUDA graph is enabled and the user did not explicitly choose a decode graph backend, the GLM DSA SM120 path now defaults decode to tc_piecewise instead of full.
  • Explicit --cuda-graph-backend-decode ... / JSON decode.backend is still preserved.
  • The existing default decode max_bs and auto mem_fraction_static caps still apply to the default batch/memory settings.
  • Added unit coverage for the default tc_piecewise selection and explicit backend preservation.

This should address the full graph capture hang you saw on lmsysorg/sglang:dev-cu13-glm52-nvfp4 while leaving the native sparse MLA path itself unchanged.

Follow-up validation in this workspace:

  • git diff --check
  • python3 -m py_compile python/sglang/srt/server_args.py test/registered/unit/server_args/test_server_args.py
  • python3 scripts/ci/check_registered_tests.py
  • python3 scripts/ci/check_no_registered_tests_in_package.py

I did not rerun the SM120 runtime here, so an independent retest on the dev-cu13 image would be the most useful final signal.

@lllllinux

Copy link
Copy Markdown

cc eager tok/s eager TPOT tc_piecewise tok/s TPOT
1 11.4 87.4 ms 44.7 22.0 ms
8 85.6 89.1 ms 224.7 31.4 ms
16 168.1 91.5 ms 382.9 37.8 ms
32 332.1 90.8 ms 572.7 49.9 ms
8 (8K/1K) 81.1 90.2 ms 197.0 32.1 ms
Eager TPOT is flat ~90 ms (host overhead floor). With tc_piecewise decode the same box outperforms the community vLLM B12X build (442.7 tok/s @ cc=32, TPOT 63 ms, same workload/tokens) by ~29% — so the FlashInfer #3395 SM120 sparse MLA kernel looks genuinely faster than the B12X equivalent. Single-run numbers; happy to rerun if useful.

which version do you use to get vllm b12x performance data? in this repo rtx6kpro,it seems that vllm is way faster than sglang

@zorrofox

Copy link
Copy Markdown

@zhangvia Fair point — should have been clearer about what that comparison is and isn't.

The 442.7 baseline is voipmonitor/vllm:eldritch-fullstack-v420dbb3-b12x284a2ea-indexerfix-cu132-20260626, TP=8, with most of the same b12x env (B12X_DENSE_SPLITK_TURBO=1, B12X_W4A16_TC_DECODE=1, B12X_MOE_FORCE_A16=1, VLLM_USE_B12X_SPARSE_INDEXER=1), measured with vllm bench serve --dataset-name random --random-input-len 2048 --random-output-len 512 --random-range-ratio 0.0 --max-concurrency 32 (i.e. 2K prefill + 512 decode, fixed-len). The main deltas vs the rtx6kpro v12 config:

my vLLM run rtx6kpro v12
image eldritch-fullstack-...20260626 dark-devotion-release-...20260622
bench vllm bench serve 2K/512 llm_decode_bench.py ctx0 decode-only
allreduce NCCL_P2P_DISABLE=1, VLLM_ENABLE_PCIE_ALLREDUCE=0 VLLM_PCIE_ALLREDUCE_BACKEND=b12x
cudagraph 32 128

The ctx0 vs 2K/512 workload difference alone accounts for a large chunk of 703 vs 443 (prefill steps drag down output-token throughput in the bench-serve metric). The allreduce config is the other suspect — my run inherited NCCL_P2P_DISABLE=1 from a 4-GPU recipe and that's likely suboptimal at TP=8.

So the +29% is strictly "same box, same 2K/512 bench-serve workload, my vLLM config vs my sglang config" — not a claim that sglang beats the tuned dark-devotion ctx0 numbers. I don't have an apples-to-apples ctx0 number for sglang, and my vLLM baseline is clearly not the best-known vLLM config for this card. Thanks for the pointer to the v12 doc.

@ronhuafeng

Copy link
Copy Markdown
Contributor Author

Reran the exact SM120 runtime validation on current head cf638d6e6b0b1138128196d2349da18460b06d44.

Environment summary:

  • 8 x NVIDIA RTX PRO 6000 Blackwell Server Edition, SM120
  • source-pinned dependency image with FlashInfer exposing kv_scale_format and DeepGEMM metadata support
  • PyTorch 2.11.0+cu130

Checks before server launch:

  • dependency probe: has_kv_scale_format=True, DeepGEMM metadata API present
  • focused unit tests: 99 passed, 18 warnings, 10 subtests passed

Server launch used the default GLM command from the PR body, without workaround flags:

python3 -m sglang.launch_server \
  --model-path nvidia/GLM-5.2-NVFP4 \
  --tensor-parallel-size 8 \
  --quantization modelopt_fp4 \
  --tool-call-parser glm47 \
  --reasoning-parser glm45 \
  --trust-remote-code \
  --mem-fraction-static 0.8 \
  --host 0.0.0.0 \
  --port 30000

Result:

  • selected prefill=flashinfer_sparse_mla, decode=flashinfer_sparse_mla
  • GLM shared-expert fusion guard triggered automatically
  • decode CUDA graph defaulted to tc_piecewise with max_bs=256
  • all 8 ranks loaded GlmMoeDsaForCausalLM / modelopt_fp4 / NVFP4
  • FP8 KV cache allocated: 368768 tokens/rank, 21.11 GB/rank
  • prefill CUDA graph capture completed with backend=tc_piecewise
  • decode CUDA graph capture completed with backend=tc_piecewise, bs=[1, 2, 4, ..., 256]
  • /health returned HTTP 200
  • minimal /generate returned HTTP 200 with 16 completion tokens
  • failure grep was clean for Unsupported architecture, TllmGenFmhaRunner, Traceback, OutOfMemory, RuntimeError, ValueError, 3072, and 6144

I also updated the PR body so the validation gaps no longer say the latest tc_piecewise follow-up lacks an SM120 runtime rerun.

@zorrofox

Copy link
Copy Markdown

Retested cf638d6e6 on the dev-cu13-glm52-nvfp4 base (sglang 22dce572), no manual --cuda-graph-backend-decode flag. Works:

[07:10:09] Setting decode CUDA graph backend to tc_piecewise for GLM DSA on SM120 to avoid full CUDA graph capture hangs.

Health 290 s, GSM8K-style 5/5, bench reproduces the earlier manual-flag run within <0.5%:

cc tok/s TPOT (ms)
1 44.6 22.1
8 225.5 31.5
16 383.0 37.9
32 572.0 50.2
8 (8K/1K) 196.6 32.2

So the cu13-image hang is covered by the new default. LGTM from the SM120 runtime side.

@ronhuafeng
ronhuafeng force-pushed the sglang-29562-sm120-dsa branch from cf638d6 to 330483a Compare June 29, 2026 07:49
@ronhuafeng

Copy link
Copy Markdown
Contributor Author

Thanks for the retest on dev-cu13; that is a useful final runtime signal.

I pushed amended head 330483aeb with the GLM-5.2 DSA metadata follow-up:

  • model_config.py now derives the DSA top-k sharing pattern from checkpoint-native indexer_types when index_topk_pattern is absent.
  • Explicit index_topk_pattern still takes precedence.
  • If indexer_types is absent or unknown, the old frequency/offset fallback is preserved.
  • The TBO guard now uses the same normalized pattern helper, so this no longer requires users to pass a manual --json-model-override-args '{"index_topk_pattern": ...}' workaround.

I also updated the PR body with this root cause and validation.

Validation for this follow-up:

  • git diff --check
  • python3 -m py_compile python/sglang/srt/configs/model_config.py python/sglang/srt/server_args.py test/registered/unit/configs/test_model_config_dsa.py
  • python3 scripts/ci/check_registered_tests.py
  • python3 scripts/ci/check_no_registered_tests_in_package.py
  • focused tests in the source-pinned SM120 validation image:
    95 passed, 22 warnings, 10 subtests passed

Exact SM120 runtime checks:

  • GLM-5.2-NVFP4 config has index_topk_pattern: null and 78 indexer_types entries for the full/shared layer layout.
  • With no index_topk_pattern override and explicit --cuda-graph-backend-decode full, the patched path reached /health and external /generate HTTP 200 on 8 x RTX PRO 6000 Blackwell Server Edition.
  • I also reran explicit full decode graph capture on current PR code at max_bs=32 and at the PR's capped max_bs=256; neither reproduced the capture hang in this source-pinned environment.

So the default remains tc_piecewise for GLM DSA on SM120 to cover the dev-cu13 behavior you found, while the checkpoint metadata footgun is now fixed in SGLang instead of relying on a user override.

@ronhuafeng
ronhuafeng force-pushed the sglang-29562-sm120-dsa branch from 330483a to 79e42d2 Compare June 29, 2026 08:05
@ronhuafeng
ronhuafeng force-pushed the sglang-29562-sm120-dsa branch from 79e42d2 to 005ffc6 Compare June 29, 2026 08:09
@ronhuafeng

Copy link
Copy Markdown
Contributor Author

One more follow-up pushed in 005ffc65d to keep the SM120 path aligned with the existing SGLang decode CUDA graph default.

Change from the previous head:

  • GLM DSA on SM120 no longer changes the default decode CUDA graph backend to tc_piecewise.
  • The default decode backend remains full, matching the normal decode CUDA graph path.
  • The SM120 GLM DSA default still caps decode graph max_bs to 256, which was the memory-sensitive part validated on RTX PRO 6000.
  • Explicit decode backend choices are preserved; tc_piecewise remains available as an explicit alternative decode CUDA graph mode, not a forced default.
  • If decode CUDA graph is explicitly disabled, the SM120 graph-memory cap no longer adjusts decode graph batch size or auto mem_fraction_static.

Validation for the latest head:

  • git diff --check
  • python3 -m py_compile python/sglang/srt/server_args.py test/registered/unit/server_args/test_server_args.py
  • python3 scripts/ci/check_registered_tests.py
  • python3 scripts/ci/check_no_registered_tests_in_package.py
  • source-pinned SM120 image focused tests:
    97 passed, 18 warnings, 10 subtests passed

Exact SM120 runtime default launch, with no manual --cuda-graph-backend-decode and no index_topk_pattern override:

  • selected prefill=flashinfer_sparse_mla, decode=flashinfer_sparse_mla
  • cuda_graph_config.decode.backend remained full
  • default decode graph max_bs was capped to 256
  • prefill CUDA graph capture completed with backend=tc_piecewise
  • decode CUDA graph capture completed with backend=full, bs=[1, 2, 4, ..., 256]
  • /health returned HTTP 200
  • external /generate returned HTTP 200 with 16 completion tokens

I also updated the PR body to make the current behavior explicit: default full for consistency, max_bs=256 for SM120 GLM DSA memory safety, and tc_piecewise as an explicit alternative mode.

@Fridge003

Fridge003 commented Jun 29, 2026 •

Copy link
Copy Markdown
Collaborator

This is a spam PR

@Fridge003 Fridge003 closed this Jun 29, 2026
@ronhuafeng

Copy link
Copy Markdown
Contributor Author

@Fridge003 Sorry for the noise here, and sorry for taking maintainer time on this. I understand why this came across as a broad parallel PR to #26928. I should have searched more carefully for existing PRs related to this issue at the beginning, found #26928 earlier, and coordinated there before opening this as a ready standalone PR and requesting many reviewers.

A bit of context on how this happened: I started from #29562, where the first blocker looked like a GLM-5.2-NVFP4 checkpoint/loading issue on SM120. As I debugged it on exact RTX PRO 6000 hardware, each fix exposed the next blocker: shared-expert fusion, then the SM120 DSA backend path, then the FlashInfer/DeepGEMM wheel gap, CUDA graph memory behavior, and finally the index_topk_pattern / indexer_types metadata issue. I kept following the path because I had a validated machine in front of me and wanted to turn the issue into something reproducible for the reporter.

One important part of that history is the dependency setup. I found that the released FlashInfer / DeepGEMM wheels did not yet contain the SM120 support needed for this path, so I added a temporary source-build path pinned to upstream GitHub commit hashes. I realize that pinning dependency commits in a PR build path is not ideal and should not be the long-term shape for SGLang. My original intent was narrower: to give the #29562 reporter a reproducible way to try the fix in their own environment before official wheels are ready, while documenting the release gap.

In hindsight, I let a focused bug investigation grow into a broad PR. My intent was not to replace or compete with #26928, but bundling all of these pieces together, especially with a default backend change, temporary source-pinned dependencies, and many review requests, created unnecessary review burden and made the relationship to the existing SM120 work unclear.

I’ll keep this PR closed and won’t reopen it as-is. If any part of it is useful, I’ll split it into much smaller GLM-5.2-specific follow-ups or issue validation work around #26928.

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.

[Bug] GLM-5.2-NVFP4 error on pro6000

4 participants