NVIDIA / NVIDIA/TensorRT-LLM

[Bug]: SM121 (GB10) excluded from FP8 block-scales SM120 Python paths while C++ getSMVersion() aliases 121→120

Open
#17,126 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Customized kernels
Dominant language
Python
Stars
14.7k
Forks
2.8k
Avg merge
2d 23h
Merged PRs (30d)
489

Description

System Info
  • GPU: a GB10 / DGX Spark (SM121) system; also relevant to SM120 consumer Blackwell as the reference behaviour
  • CPU architecture: aarch64
  • TensorRT-LLM branch: main, checked against commit d924d9f185e3cb0d811e905d2b4b5ddb340d71ee
  • Additional information: code-inspection issue — no GPU run was performed. See "Reproduction" for the exact minimal repro that would confirm it, which I was not able to execute.
Who can help?

(leave for triage — the FP8 block-scale linear/MLA owners)

Information
  • The official example scripts
  • My own modified scripts
Tasks
  • An officially supported task in the examples folder
  • My own task or dataset

Summary

Checked against NVIDIA/TensorRT-LLM at d924d9f185e3cb0d811e905d2b4b5ddb340d71ee.

Observed (from source). The C++ and Python layers disagree about what SM version a
GB10 is. C++ getSMVersion() deliberately reports 120 for a real SM 121, so that
SM120 kernels are reused on SM121:

https://github.com/NVIDIA/TensorRT-LLM/blob/d924d9f185e3cb0d811e905d2b4b5ddb340d71ee/cpp/include/tensorrt_llm/common/cudaUtils.h#L289-L307

/// @param queryRealSmArch Whether to query the real SM architecture. example usage: use real sm arch when do LUT tuning
/// and use fake sm arch when reuse sm120 code on sm121 devices.
inline int getSMVersion(bool queryRealSmArch = false)
{
    ...
    if (sm == 121 && !queryRealSmArch)
    {
        return 120;
    }
    return sm;
}

Python get_sm_version() performs no such aliasing — it returns 121 on a GB10:

https://github.com/NVIDIA/TensorRT-LLM/blob/d924d9f185e3cb0d811e905d2b4b5ddb340d71ee/tensorrt_llm/_utils.py#L676-L679

@lru_cache(maxsize=1)
def get_sm_version():
    prop = torch.cuda.get_device_properties(0)
    return prop.major * 10 + prop.minor

The FP8 block-scales code paths gate on get_sm_version() == 120 (or {90, 120}). On a
GB10 those gates are False, so Python selects the non-SM120 behaviour — while the C++
op it subsequently calls has already been normalised to SM120 and therefore runs the
SM120 kernel. The two sides pick incompatible scale layouts.

The concrete failure (dense FP8 block-scale linear)

FP8BlockScalesLinearMethod.apply:

https://github.com/NVIDIA/TensorRT-LLM/blob/d924d9f185e3cb0d811e905d2b4b5ddb340d71ee/tensorrt_llm/_torch/modules/linear.py#L1144-L1166

if is_sm_100f():                          # False on SM121 (100/103 only)
    ...
elif get_sm_version() == 120:             # False on SM121 -- it is 121
    act_input_fp8, act_input_sf = per_token_quant_and_transform(input)   # int32 scales
    output = torch.ops.trtllm.fp8_block_scaling_gemm(...)
else:                                     # SM121 lands here
    act_input_fp8, act_input_sf = torch.ops.trtllm.fp8_quantize_1x128(input)  # fp32 scales
    output = torch.ops.trtllm.fp8_block_scaling_gemm(...)

Scale dtypes on each branch:

  • per_token_quant_and_transformoutput_scale is int32
    (fp8_utils.py#L650-L653)
  • fp8_quantize_1x128 → scales are fp32 (FP8_BLOCK_SCALING_SF_DTYPE = torch::ScalarType::Float,
    thUtils.h#L67);
    its SM-specific scale post-processing is gated on isSM100Family() only
    (fp8Quantize.cpp#L72-L77),
    so SM120/121 receive the plain SM90 layout.

The op then dispatches on the aliased SM:

https://github.com/NVIDIA/TensorRT-LLM/blob/d924d9f185e3cb0d811e905d2b4b5ddb340d71ee/cpp/tensorrt_llm/thop/fp8BlockScalingGemm.cpp#L243-L256

auto const sm = tensorrt_llm::common::getSMVersion();   // 120 on a GB10
switch (sm)
{
...
case 120: return fp8_block_scale_gemm_blackwell_geforce(mat1, mat2, mat1Scale, mat2Scale);
default: TORCH_CHECK(false, "Unsupported SM version for FP8 block scaling GEMM");
}

and fp8_block_scale_gemm_blackwell_geforce requires int32 scales:

https://github.com/NVIDIA/TensorRT-LLM/blob/d924d9f185e3cb0d811e905d2b4b5ddb340d71ee/cpp/tensorrt_llm/thop/fp8BlockScalingGemm.cpp#L123-L124

TORCH_CHECK(mat1Scale.scalar_type() == at::ScalarType::Int, "Scale dtype must be Int32.");
TORCH_CHECK(mat2Scale.scalar_type() == at::ScalarType::Int, "Scale dtype must be Int32.");

Inferred. On a GB10, a dense FP8 block-scale linear layer should therefore abort with
Scale dtype must be Int32. The geforce path additionally tightens K % 128 == 0 where
the SM90 path only needs K % 16 == 0, so shapes valid on the branch Python selected are
not necessarily valid on the kernel C++ selected.

Affected sites

All of these use == 120 / {90, 120} where the surrounding code's convention is
(120, 121):

Site Consequence on SM121
linear.py#L1158 wrong activation scale layout → Scale dtype must be Int32.
mla.py#L356 same, via fp8_block_scaling_bmm_out (whose C++ side also branches on sm == 120, L353-L368)
trtllm_quant.py#L164 same, on the AutoDeploy path
fused_moe_cutlass.py#L105-L108 FP8_BLOCK_SCALES: ("in", {90, 120}) → CutlassFusedMoE reports it cannot implement FP8 block scales on SM121. Note NVFP4 immediately below is {100, 103, 120, 121}
fused_moe_cutlass.py#L902 SM121 does not take the Triton fallback that the comment says exists because "CUTLASS TMA fails on SM120"
linear.py#L1284-L1289 use_deep_gemm_layout False → weights not resmoothed to e8m0, though the SM120 kernel is the one selected
mla.py#L3234 k_b_proj_trans / v_b_proj not resmoothed on SM121
quantization.py#L1257 _needs_e8m0_resmooth() False → MoE weight layout mismatch
Why this looks unintentional

I could not find a comment, waiver, or doc note indicating a deliberate SM121 carve-out
for block scales. If one exists, this issue is invalid and I would appreciate the pointer.

Reproduction

I was not able to run this — I do not currently have access to an SM121 machine, and
this report is from source inspection only. The minimal check, on a GB10 / DGX Spark,
needs no model:

import torch
from tensorrt_llm._torch.modules.linear import per_token_quant_and_transform

m, n, k = 128, 512, 512
a = torch.randn(m, k, dtype=torch.bfloat16, device="cuda")
b = torch.randn(n, k, dtype=torch.bfloat16, device="cuda").to(torch.float8_e4m3fn)
b_scale = torch.ones(n // 128, k // 128, dtype=torch.float32, device="cuda")

# What linear.py's `else` branch does on SM121 (fp32 activation scales):
a_fp8, a_sf = torch.ops.trtllm.fp8_quantize_1x128(a)
print("activation scale dtype:", a_sf.dtype)          # expect torch.float32
torch.ops.trtllm.fp8_block_scaling_gemm_impl(a_fp8, b, a_sf, b_scale)
# expected: RuntimeError ... Scale dtype must be Int32.

# What the SM120 branch would have done (int32 activation scales):
a_fp8_2, a_sf_2 = per_token_quant_and_transform(a)
print("activation scale dtype:", a_sf_2.dtype)        # expect torch.int32

Also worth printing both views of the SM version on such a machine, which is the root of
the whole mismatch:

from tensorrt_llm._utils import get_sm_version
print(get_sm_version())                                  # expect 121
print(torch.cuda.get_device_capability(0))               # expect (12, 1)
# C++ side, for contrast, sees 120 via getSMVersion()
Expected behavior

On SM121 the Python FP8 block-scales gates should select the same SM120 behaviour that the
C++ layer has already committed to — i.e. get_sm_version() in (120, 121) and
FP8_BLOCK_SCALES: ("in", {90, 120, 121}), matching how NVFP4 and the other
(120, 121) sites in the repo are written.

Actual behavior

Python takes the non-SM120 branch while C++ runs the SM120 kernel, producing a scale-layout
mismatch (Scale dtype must be Int32.) on the dense/MLA paths and an unnecessary
"unsupported" verdict on the CutlassFusedMoE FP8-block-scales path.

Unknown
  • Whether any single model exercises all of these at once — the MoE support gate may
    refuse a MoE model before the dense crash is reached, so the dense failure is most
    likely to be seen with a dense FP8-block-scale checkpoint.
  • Whether the SM120 geforce block-scale kernels are numerically correct on SM121 once the
    gates are aligned. The aliasing implies upstream expects them to be, but I have not
    measured it, and a correctness check is the part of this that genuinely needs the
    hardware.
  • Whether the {90, 120} MoE entry is additionally constrained by something outside this
    dict.
Question for maintainers

Is the SM121 omission at these sites an oversight (as the adjacent {100, 103, 120, 121}
entries and the getSMVersion aliasing comment suggest), or is there a block-scales-specific
reason SM121 must be held back? If it is an oversight I am happy to send a PR aligning the
gates to (120, 121); I would want someone with GB10 access to confirm numerics.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with cpp/include/tensorrt_llm/common/cudaUtils.h and tensorrt_llm/_utils.py to compare C++ and Python SM-version handling. Then inspect the listed gates in _torch/modules/linear.py, mla.py, trtllm_quant.py, fused_moe_cutlass.py, and fused_moe/quantization.py; run the supplied minimal repro on SM121 if hardware is available. Done means the Python paths agree with the selected C++ kernel and the affected FP8 block-scale paths validate successfully.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python, pytorch
Domain
machine-learning, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.