Skip to content

Commit af97041

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 af97041

2 files changed

Lines changed: 36 additions & 14 deletions

File tree

tensorrt_llm/_torch/modules/attention.py

Lines changed: 4 additions & 1 deletion
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

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

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,27 @@ def test_select_dsv4_ob_split_k_overrides(monkeypatch):
4444
_select_dsv4_ob_split_k(32, configured_split=3)
4545

4646

47+
def test_dsv4_fused_oproj_is_enabled_by_default(monkeypatch):
48+
module = SimpleNamespace(
49+
n_local_groups=16,
50+
num_groups=16,
51+
o_a_proj=torch.empty((), device="meta", dtype=torch.float8_e4m3fn),
52+
o_b_proj=SimpleNamespace(
53+
tp_size=1,
54+
has_fp8_block_scales=True,
55+
use_cute_dsl_blockscaling_mm=False,
56+
),
57+
)
58+
monkeypatch.setattr(attention_module, "IS_CUTLASS_DSL_AVAILABLE", True)
59+
monkeypatch.setattr(attention_module, "is_sm_100f", lambda: True)
60+
monkeypatch.delenv("TRTLLM_DSV4_DISABLE_FUSED_OPROJ", raising=False)
61+
62+
assert MLA._should_use_fused_oproj(module)
63+
64+
monkeypatch.setenv("TRTLLM_DSV4_DISABLE_FUSED_OPROJ", "1")
65+
assert not MLA._should_use_fused_oproj(module)
66+
67+
4768
def test_dsv4_fmha_epilogue_output_uses_fused_oproj():
4869
attn_fp8 = torch.empty((16, 4, 4096), device="meta", dtype=torch.float8_e4m3fn)
4970
attn_scale = torch.empty((16, 32, 4), device="meta")
@@ -368,7 +389,7 @@ def calculate_reference_deepseek_v4_o_proj(
368389

369390
def _build_dsv4_o_proj_case(num_tokens: int, dtype_str: str, device: torch.device):
370391
"""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
392+
o_proj tests. Shared by the correctness test and the default fused
372393
fp8-equivalence test so both exercise an identical setup.
373394
374395
Returns:
@@ -622,18 +643,16 @@ def test_deepseek_v4_o_proj(num_tokens: int, dtype_str: str):
622643
@pytest.mark.skip_less_device_memory(80000)
623644
@pytest.mark.parametrize("num_tokens", [1, 16, 32, 128, 256])
624645
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.
646+
"""The default fused FP8 epilogue must match the disabled baseline.
627647
628-
Fused (DSV4_FUSE_OPROJ=1): o_a's CuTe-DSL GEMM emits o_lora directly as fp8
648+
Fused (default): o_a's CuTe-DSL GEMM emits o_lora directly as fp8
629649
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.
650+
Disabled: o_a emits bf16 o_lora, then ``o_b_proj`` runs the separate 1x128
651+
quant + DeepGEMM. Since the fusion only folds the *same* quant into o_a's
652+
epilogue, the two paths must match far tighter than the fp8-vs-bf16 reference.
634653
"""
635654
if get_sm_version() < 100:
636-
pytest.skip("DSV4_FUSE_OPROJ fp8 fusion requires Blackwell (SM100+)")
655+
pytest.skip("fused DeepSeek-V4 FP8 O-projection requires Blackwell (SM100+)")
637656

638657
device = torch.device("cuda")
639658
mla, attn_out_latent, position_ids, refs = _build_dsv4_o_proj_case(num_tokens, "fp8", device)
@@ -645,12 +664,12 @@ def test_deepseek_v4_o_proj_fused_fp8_equivalence(num_tokens: int, monkeypatch):
645664
assert mla.o_b_proj.has_fp8_block_scales
646665
assert not getattr(mla.o_b_proj, "use_cute_dsl_blockscaling_mm", False)
647666

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

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

0 commit comments

Comments
 (0)