Skip to content

Commit c797edf

Browse files
committed
[None][fix] Enable DSV4 fused O-projection by default
Signed-off-by: Mingyang Hao <200044211+mingyangHao@users.noreply.github.com>
1 parent 5f70bd2 commit c797edf

2 files changed

Lines changed: 41 additions & 29 deletions

File tree

tensorrt_llm/_torch/modules/attention.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,9 +2033,12 @@ def _validate_dsv4_epilogue_buffers(
20332033
return fp8_o, output_sf
20342034

20352035
def _should_use_fused_oproj(self) -> bool:
2036-
return (os.environ.get("DSV4_FUSE_OPROJ", "0") == "1"
2036+
# Eligible Blackwell paths are on by default; keep an emergency fallback.
2037+
return (not _is_env_truthy("TRTLLM_DSV4_DISABLE_FUSED_OPROJ")
2038+
and is_sm_100f() and IS_CUTLASS_DSL_AVAILABLE
20372039
and self.n_local_groups == self.num_groups
20382040
and getattr(self.o_b_proj, "tp_size", 1) == 1
2041+
and self.o_a_proj.dtype == torch.float8_e4m3fn
20392042
and self.o_b_proj.has_fp8_block_scales and not getattr(
20402043
self.o_b_proj, "use_cute_dsl_blockscaling_mm", False))
20412044

@@ -2073,16 +2076,22 @@ def _fused_ob_gemm(self, o_lora_fp8: torch.Tensor, sf_out: torch.Tensor,
20732076
SK = _select_dsv4_ob_split_k(M, getattr(self, "ob_split_k", None))
20742077
packed_k = K_ob // _DSV4_OB_PACKED_SCALE_K
20752078
# CuTe specializes the DSV4-Pro O_b shape.
2076-
can_try_cute = (IS_CUTLASS_DSL_AVAILABLE and M > 0
2077-
and (M >= _DSV4_OB_CUTE_SK1_MIN_M or SK > 1)
2078-
and N == _DSV4_PRO_OB_N and K_ob == _DSV4_PRO_OB_K
2079-
and K_ob % (_DSV4_OB_PACKED_SCALE_K * SK) == 0
2080-
and sf_out.dtype == torch.int32 and sf_out.dim() == 2
2081-
and sf_out.shape == (M, packed_k)
2082-
and sf_out.stride(0) == 1 and sf_out.stride(1) >= M
2083-
and sf_out.stride(1) % _UE8M0_SCALES_PER_WORD == 0)
2079+
use_cute_tactic = (IS_CUTLASS_DSL_AVAILABLE and M > 0
2080+
and (M >= _DSV4_OB_CUTE_SK1_MIN_M or SK > 1)
2081+
and N == _DSV4_PRO_OB_N and K_ob == _DSV4_PRO_OB_K
2082+
and K_ob % (_DSV4_OB_PACKED_SCALE_K * SK) == 0)
2083+
activation_layout_ok = (sf_out.dtype == torch.int32
2084+
and sf_out.dim() == 2
2085+
and sf_out.shape == (M, packed_k)
2086+
and sf_out.stride(0) == 1
2087+
and sf_out.stride(1) >= M and
2088+
sf_out.stride(1) % _UE8M0_SCALES_PER_WORD == 0)
2089+
if use_cute_tactic and not activation_layout_ok:
2090+
raise RuntimeError(
2091+
"DSV4-Pro O_b CuTe tactic requires packed activation scales.")
2092+
20842093
cute_wsf = wsf
2085-
if can_try_cute and cute_wsf.dtype != torch.int32:
2094+
if use_cute_tactic and cute_wsf.dtype != torch.int32:
20862095
from tensorrt_llm.quantization.utils.fp8_utils import \
20872096
transform_sf_into_required_layout
20882097
cute_wsf = getattr(self.o_b_proj, "_ob_wsf_int", None)
@@ -2100,12 +2109,17 @@ def _fused_ob_gemm(self, o_lora_fp8: torch.Tensor, sf_out: torch.Tensor,
21002109
is_sfa=False)
21012110
self.o_b_proj._ob_wsf_int = cute_wsf
21022111

2103-
can_use_cute = (can_try_cute and cute_wsf.dtype == torch.int32
2104-
and cute_wsf.dim() == 2
2105-
and cute_wsf.shape == (N, packed_k)
2106-
and cute_wsf.stride(0) == 1 and cute_wsf.stride(1) == N)
2107-
if not can_use_cute:
2108-
# Keep DeepGEMM for unsupported shapes and layouts.
2112+
weight_layout_ok = (cute_wsf.dtype == torch.int32
2113+
and cute_wsf.dim() == 2
2114+
and cute_wsf.shape == (N, packed_k)
2115+
and cute_wsf.stride(0) == 1
2116+
and cute_wsf.stride(1) == N)
2117+
if use_cute_tactic and not weight_layout_ok:
2118+
raise RuntimeError(
2119+
"DSV4-Pro O_b CuTe tactic requires packed weight scales.")
2120+
2121+
if not use_cute_tactic:
2122+
# Keep DeepGEMM for shapes outside the tuned CuTe buckets.
21092123
hidden = torch.empty([M, N],
21102124
device=o_lora_fp8.device,
21112125
dtype=self.dtype)

tests/unittest/_torch/attention/sparse/deepseek_v4/test_deepseek_v4_o_proj.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ def calculate_reference_deepseek_v4_o_proj(
368368

369369
def _build_dsv4_o_proj_case(num_tokens: int, dtype_str: str, device: torch.device):
370370
"""Build an MLA module, inputs, and reference-path tensors for the DeepSeek-V4
371-
o_proj tests. Shared by the correctness test and the DSV4_FUSE_OPROJ fused
371+
o_proj tests. Shared by the correctness test and the default fused
372372
fp8-equivalence test so both exercise an identical setup.
373373
374374
Returns:
@@ -622,18 +622,16 @@ def test_deepseek_v4_o_proj(num_tokens: int, dtype_str: str):
622622
@pytest.mark.skip_less_device_memory(80000)
623623
@pytest.mark.parametrize("num_tokens", [1, 16, 32, 128, 256])
624624
def test_deepseek_v4_o_proj_fused_fp8_equivalence(num_tokens: int, monkeypatch):
625-
"""The opt-in DSV4_FUSE_OPROJ fused fp8 epilogue must be numerically
626-
equivalent to the default unfused path.
625+
"""The default fused FP8 epilogue must match the disabled baseline.
627626
628-
Fused (DSV4_FUSE_OPROJ=1): o_a's CuTe-DSL GEMM emits o_lora directly as fp8
627+
Fused (default): o_a's CuTe-DSL GEMM emits o_lora directly as fp8
629628
e4m3 + packed-UE8M0 1x128 scale factors, fed straight to o_b.
630-
Unfused (default): o_a emits bf16 o_lora, then ``o_b_proj`` runs the separate
631-
1x128 quant + DeepGEMM. Since the fusion only folds the *same* quant into o_a's
632-
epilogue, the two production paths must match far tighter than the fp8-vs-bf16
633-
reference bar.
629+
Disabled: o_a emits bf16 o_lora, then ``o_b_proj`` runs the separate 1x128
630+
quant + DeepGEMM. Since the fusion only folds the *same* quant into o_a's
631+
epilogue, the two paths must match far tighter than the fp8-vs-bf16 reference.
634632
"""
635633
if get_sm_version() < 100:
636-
pytest.skip("DSV4_FUSE_OPROJ fp8 fusion requires Blackwell (SM100+)")
634+
pytest.skip("fused DeepSeek-V4 FP8 O-projection requires Blackwell (SM100+)")
637635

638636
device = torch.device("cuda")
639637
mla, attn_out_latent, position_ids, refs = _build_dsv4_o_proj_case(num_tokens, "fp8", device)
@@ -645,12 +643,12 @@ def test_deepseek_v4_o_proj_fused_fp8_equivalence(num_tokens: int, monkeypatch):
645643
assert mla.o_b_proj.has_fp8_block_scales
646644
assert not getattr(mla.o_b_proj, "use_cute_dsl_blockscaling_mm", False)
647645

648-
# Unfused (default): DSV4_FUSE_OPROJ unset.
649-
monkeypatch.delenv("DSV4_FUSE_OPROJ", raising=False)
646+
# Explicit kill switch retains the unfused fallback.
647+
monkeypatch.setenv("TRTLLM_DSV4_DISABLE_FUSED_OPROJ", "1")
650648
out_unfused = mla._deepseek_v4_o_proj(attn_out_latent.clone(), position_ids)
651649

652-
# Fused: opt in.
653-
monkeypatch.setenv("DSV4_FUSE_OPROJ", "1")
650+
# Unset is the production default and must take the fused path.
651+
monkeypatch.delenv("TRTLLM_DSV4_DISABLE_FUSED_OPROJ", raising=False)
654652
mla.ob_split_k = 1
655653
out_fused = mla._deepseek_v4_o_proj(attn_out_latent.clone(), position_ids)
656654

0 commit comments

Comments
 (0)