Skip to content

Commit 64c3261

Browse files
committed
fix(torchtitan): harden ROCm compiled training against silent NaNs
Preserve eager precision-cast semantics in Inductor graphs, keep DeepSeek MoE blocks on TorchTitan's mixed compile strategy, and route gfx942 DeepSeek FP8 MLA backward through aiter CK instead of the invalid fmha_v3 ASM path. Also bind grouped-MM patches to the imported module so CI no longer depends on import order.
1 parent 94cf36e commit 64c3261

13 files changed

Lines changed: 766 additions & 38 deletions

primus/backends/torchtitan/patches/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
embedding_amp_patches,
3232
flex_attention_patches,
3333
fsdp_weight_tying_patches,
34+
inductor_precision_casts_patches,
3435
logger_patches,
3536
metrics_output_format,
3637
mock_dataset_patches,

primus/backends/torchtitan/patches/dsv3_v022_perf_patches.py

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,22 @@
1010
Reduce DSv3-16B HBM on MI355X with torchtitan v0.2.2 without modifying the
1111
upstream submodule. The balanced-routing field migration
1212
(``training.debug_moe_force_load_balance`` -> ``debug.moe_force_load_balance``)
13-
is already handled in the DeepSeek configs on main, so this module only carries
14-
the two memory fixes:
15-
16-
1. Replace v0.2.2 ``apply_compile`` (capture_scalar_outputs + per-submodule
17-
compile) with whole-block compile matching v0.1.0 behavior (drops the
18-
fragmented reserved HBM re-introduced by per-submodule compilation).
19-
2. Replace MoE combine fp32 ``bmm`` with a bf16 weighted sum (drops the fp32
20-
activation copy retained across the MoE layers).
13+
is already handled in the DeepSeek configs on main.
14+
15+
Whole-block compilation is still needed to avoid the fragmented reserved HBM
16+
caused by v0.2.2's per-submodule compilation. Compiling the entire MoE forward
17+
is unsafe, however: it pulls expert-parallel token dispatch/combine and
18+
GroupedExperts FSDP hooks into one inductor graph. On MI300X that silently
19+
produces a NaN forward loss from the first step.
20+
21+
Use a safe dense-only boundary instead: dense blocks compile as a full graph,
22+
while MoE TransformerBlocks remain eager. Compiling even the outer MoE block
23+
with a graph break around ``MoE.forward`` still produces the NaN, whereas
24+
v0.2.2's per-submodule policy reintroduces the fragmented reserved HBM this
25+
patch was created to avoid. Keeping MoE blocks eager avoids both failure modes;
26+
the expensive attention and grouped-GEMM work still uses Primus-Turbo kernels.
27+
The MoE forward replacement also changes its fp32 ``bmm`` combine into a bf16
28+
weighted sum, dropping the fp32 activation copy retained across MoE layers.
2129
"""
2230

2331
from __future__ import annotations
@@ -52,36 +60,38 @@ def _compile_enabled(ctx: PatchContext) -> bool:
5260
return bool(get_param(ctx, "compile.enable", False))
5361

5462

63+
def _apply_dense_only_compile(model: nn.Module, compile_config: Any, ep_enabled: bool) -> None:
64+
"""Whole-block compile dense layers and leave MoE TransformerBlocks eager."""
65+
from torchtitan.tools.logging import logger
66+
67+
del ep_enabled # MoE blocks stay eager, including EP dispatch.
68+
for layer_id, transformer_block in model.layers.named_children():
69+
if not transformer_block.moe_enabled:
70+
transformer_block = torch.compile(
71+
transformer_block,
72+
backend=compile_config.backend,
73+
fullgraph=True,
74+
)
75+
model.layers.register_module(layer_id, transformer_block)
76+
77+
logger.info("Compiling dense TransformerBlocks; leaving MoE TransformerBlocks eager (Primus patch)")
78+
79+
5580
@register_patch(
5681
"torchtitan.dsv3.whole_block_compile",
5782
backend="torchtitan",
5883
phase="setup",
59-
description="Whole-block torch.compile; leave capture_scalar_outputs disabled",
84+
description="Whole-block compile dense layers; leave MoE TransformerBlocks eager",
6085
condition=lambda ctx: _is_deepseek_model(ctx) and _compile_enabled(ctx),
6186
)
6287
def patch_whole_block_compile(ctx: PatchContext) -> None:
63-
"""Replace v0.2.2 apply_compile with v0.1.0-style whole TransformerBlock compile."""
64-
from torchtitan.config.job_config import Compile as CompileConfig
88+
"""Install low-fragment dense-only compilation."""
6589
from torchtitan.models.llama4.infra import parallelize as parallelize_module
66-
from torchtitan.tools.logging import logger
67-
68-
def apply_compile_patched(model: nn.Module, compile_config: CompileConfig, ep_enabled: bool) -> None:
69-
# Match v0.1.0: do NOT set capture_scalar_outputs=True (avoids fragmentation).
70-
for layer_id, transformer_block in model.layers.named_children():
71-
fullgraph = not transformer_block.moe_enabled
72-
transformer_block = torch.compile(
73-
transformer_block,
74-
backend=compile_config.backend,
75-
fullgraph=fullgraph,
76-
)
77-
model.layers.register_module(layer_id, transformer_block)
78-
79-
logger.info("Compiling each TransformerBlock with torch.compile (Primus whole-block patch)")
8090

81-
parallelize_module.apply_compile = apply_compile_patched
91+
parallelize_module.apply_compile = _apply_dense_only_compile
8292
log_rank_0(
8393
"[Patch:torchtitan.dsv3.whole_block_compile] "
84-
"Patched torchtitan.models.llama4.infra.parallelize.apply_compile",
94+
"Patched apply_compile with eager MoE TransformerBlocks",
8595
)
8696

8797

@@ -143,8 +153,10 @@ def patch_moe_bf16_combine(ctx: PatchContext) -> None:
143153
"""Replace MoE.forward to drop the fp32 bmm copy in the combine step."""
144154
import torchtitan.models.moe.moe as moe_module
145155

146-
moe_module.MoE.forward = _moe_forward_bf16_combine
156+
# Keep this explicit eager boundary in case a caller wraps a larger parent
157+
# module in torch.compile outside the DeepSeek parallelization path.
158+
moe_module.MoE.forward = torch.compiler.disable(_moe_forward_bf16_combine)
147159
log_rank_0(
148160
"[Patch:torchtitan.dsv3.moe_bf16_combine] "
149-
"Patched torchtitan.models.moe.moe.MoE.forward (bf16 weighted combine)",
161+
"Patched MoE.forward (eager boundary, bf16 weighted combine)",
150162
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
###############################################################################
2+
# Copyright (c) 2026, Advanced Micro Devices, Inc. All rights reserved.
3+
#
4+
# See LICENSE for license information.
5+
###############################################################################
6+
7+
"""Preserve eager low-precision casts in ROCm Inductor graphs.
8+
9+
On the pinned ROCm torch 2.12 build, compiled FSDP BF16 llama/qwen recipes
10+
produce a finite first loss but NaN gradients unless
11+
``emulate_precision_casts`` is enabled. Eager and ``aot_eager`` remain finite.
12+
13+
This is not attributed to pytorch#150859: that issue covered float8 rowwise
14+
training with selective activation checkpointing and is no longer reproducible
15+
on upstream 2.12 nightly. The plain-BF16 ROCm reproducer here is different even
16+
though the same compatibility flag fixes it.
17+
18+
The flag is part of Inductor's cache key, so no manual cache invalidation is
19+
required.
20+
"""
21+
22+
from primus.core.patches import PatchContext, get_param, register_patch
23+
from primus.core.utils.module_utils import log_rank_0
24+
25+
_PREFIX = "[Patch:torchtitan.torch.inductor_precision_casts]"
26+
27+
28+
def _compile_enabled(ctx: PatchContext) -> bool:
29+
if not bool(get_param(ctx, "compile.enable", False)):
30+
return False
31+
32+
import torch
33+
34+
return torch.version.hip is not None
35+
36+
37+
@register_patch(
38+
"torchtitan.torch.inductor_precision_casts",
39+
backend="torchtitan",
40+
phase="setup", # before parallelize_fn reaches apply_compile
41+
description="Emulate eager precision casts in ROCm Inductor BF16/FSDP graphs",
42+
condition=_compile_enabled,
43+
)
44+
def patch_inductor_precision_casts(ctx: PatchContext) -> None:
45+
"""Make inductor round intermediates the way eager does."""
46+
import torch._inductor.config as inductor_config
47+
48+
if inductor_config.emulate_precision_casts:
49+
log_rank_0(f"{_PREFIX} already enabled; leaving it alone")
50+
return
51+
52+
inductor_config.emulate_precision_casts = True
53+
log_rank_0(f"{_PREFIX} torch._inductor.config.emulate_precision_casts = True")

primus/backends/torchtitan/patches/turbo/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"""
1414

1515
from primus.backends.torchtitan.patches.turbo import ( # noqa: F401
16+
aiter_mla_capability_patches,
1617
async_tp_patches,
1718
attention_patches,
1819
deepseek_v3_classic_attention_patches,
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
###############################################################################
2+
# Copyright (c) 2026, Advanced Micro Devices, Inc. All rights reserved.
3+
#
4+
# See LICENSE for license information.
5+
###############################################################################
6+
7+
"""Route the failing gfx942 DeepSeek MLA backward directly to aiter CK."""
8+
9+
from typing import Any
10+
11+
from primus.backends.torchtitan.patches.turbo.attention_safety import (
12+
requires_ck_mla_backward,
13+
)
14+
from primus.core.patches import PatchContext, get_args, register_patch
15+
from primus.core.utils.module_utils import log_rank_0
16+
17+
_LOG_PREFIX = "[Patch:torchtitan.primus_turbo.aiter_mla_capability]"
18+
_WRAPPED_ATTR = "_primus_gfx942_mla_ck_backward"
19+
20+
21+
def _can_patch(ctx: PatchContext) -> bool:
22+
args = get_args(ctx)
23+
return requires_ck_mla_backward(ctx.model_name, args.primus_turbo)
24+
25+
26+
def _is_gfx942_tensor(tensor) -> bool:
27+
try:
28+
import torch
29+
30+
arch = torch.cuda.get_device_properties(tensor.device).gcnArchName or ""
31+
return "gfx942" in arch
32+
except Exception: # pragma: no cover - defensive
33+
return False
34+
35+
36+
def _should_use_ck(kwargs: dict[str, Any]) -> bool:
37+
q = kwargs["q"]
38+
v = kwargs["v"]
39+
return bool(
40+
kwargs.get("sink") is None
41+
and kwargs.get("qkv_format", "bshd") == "bshd"
42+
and q.ndim == 4
43+
and v.ndim == 4
44+
and q.shape[-1] in (128, 192)
45+
and v.shape[-1] == 128
46+
and kwargs["dropout_p"] == 0.0
47+
and kwargs.get("bias") is None
48+
and kwargs.get("alibi_slopes") is None
49+
and kwargs.get("dbias") is None
50+
and _is_gfx942_tensor(q)
51+
)
52+
53+
54+
def _run_ck_backward(mha, kwargs: dict[str, Any]):
55+
softmax_d = mha.mha_bwd(
56+
kwargs["dout"],
57+
kwargs["q"],
58+
kwargs["k"],
59+
kwargs["v"],
60+
kwargs["out"],
61+
kwargs["softmax_lse"],
62+
kwargs["dropout_p"],
63+
kwargs["softmax_scale"],
64+
kwargs["causal"],
65+
kwargs["window_size_left"],
66+
kwargs["window_size_right"],
67+
kwargs["deterministic"],
68+
kwargs["dq"],
69+
kwargs["dk"],
70+
kwargs["dv"],
71+
kwargs["dbias"],
72+
kwargs["bias"],
73+
kwargs["alibi_slopes"],
74+
kwargs["rng_state"],
75+
None,
76+
kwargs["sink"],
77+
kwargs["dsink"],
78+
)
79+
return (
80+
softmax_d,
81+
kwargs["dq"],
82+
kwargs["dk"],
83+
kwargs["dv"],
84+
kwargs["dbias"],
85+
kwargs["dsink"],
86+
)
87+
88+
89+
def _make_execute_wrapper(original, mha):
90+
def execute(*args, **kwargs):
91+
if not args and _should_use_ck(kwargs):
92+
return _run_ck_backward(mha, kwargs)
93+
return original(*args, **kwargs)
94+
95+
setattr(execute, _WRAPPED_ATTR, True)
96+
return execute
97+
98+
99+
@register_patch(
100+
"torchtitan.primus_turbo.aiter_mla_capability",
101+
backend="torchtitan",
102+
phase="setup",
103+
description="Route failing gfx942 DeepSeek MLA backward from v3 to aiter CK",
104+
condition=_can_patch,
105+
priority=51,
106+
)
107+
def patch_aiter_mla_capability(ctx: PatchContext) -> None:
108+
import aiter.ops.mha as mha
109+
from primus_turbo.pytorch.kernels.attention import attention_aiter_impl
110+
111+
backend = attention_aiter_impl.AttnBwdAiterBackend
112+
original = backend.execute
113+
if getattr(original, _WRAPPED_ATTR, False):
114+
return
115+
116+
backend.execute = staticmethod(_make_execute_wrapper(original, mha))
117+
log_rank_0(f"{_LOG_PREFIX} routing gfx942 DeepSeek MLA backward directly to aiter CK")

primus/backends/torchtitan/patches/turbo/attention_patches.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,30 @@
1515
expressed as a backend patch so it can be managed via the Primus patch system.
1616
"""
1717

18-
from primus.core.patches import PatchContext, get_param, register_patch
18+
from primus.backends.torchtitan.patches.turbo.attention_safety import (
19+
requires_ck_mla_backward,
20+
should_use_turbo_attention,
21+
)
22+
from primus.core.patches import PatchContext, get_args, register_patch
1923
from primus.core.utils.module_utils import log_rank_0
2024

2125

26+
def _can_patch_turbo_attention(ctx: PatchContext) -> bool:
27+
args = get_args(ctx)
28+
if requires_ck_mla_backward(ctx.model_name, args.primus_turbo):
29+
log_rank_0(
30+
"[Patch:torchtitan.primus_turbo.turbo_attention] "
31+
"Using Turbo attention with CK MLA backward on gfx942."
32+
)
33+
return should_use_turbo_attention(ctx.model_name, args.primus_turbo)
34+
35+
2236
@register_patch(
2337
"torchtitan.primus_turbo.turbo_attention",
2438
backend="torchtitan",
2539
phase="setup",
2640
description="Use Primus-Turbo Attention kernels for supported models",
27-
condition=lambda ctx: (
28-
get_param(ctx, "primus_turbo.enable_primus_turbo", False)
29-
and get_param(ctx, "primus_turbo.use_turbo_attention", False)
30-
),
41+
condition=_can_patch_turbo_attention,
3142
)
3243
def patch_turbo_attention(ctx: PatchContext) -> None:
3344
"""
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
###############################################################################
2+
# Copyright (c) 2026, Advanced Micro Devices, Inc. All rights reserved.
3+
#
4+
# See LICENSE for license information.
5+
###############################################################################
6+
7+
"""Runtime safety gate for TorchTitan Primus-Turbo attention."""
8+
9+
from typing import Any
10+
11+
12+
def _model_name(model: Any) -> str:
13+
if isinstance(model, str):
14+
return model
15+
return str(getattr(model, "name", model) or "")
16+
17+
18+
def _is_gfx942() -> bool:
19+
try:
20+
import torch
21+
22+
if not torch.cuda.is_available():
23+
return False
24+
return "gfx942" in (torch.cuda.get_device_properties(0).gcnArchName or "")
25+
except Exception: # pragma: no cover - fail open outside supported GPU runs
26+
return False
27+
28+
29+
def requires_ck_mla_backward(model: Any, turbo_config: Any) -> bool:
30+
"""Whether gfx942 MLA must reject the unsupported fmha_v3 backward path."""
31+
is_deepseek = "deepseek" in _model_name(model).lower()
32+
is_fp8_recipe = bool(
33+
getattr(turbo_config, "use_turbo_float8_linear", False) or getattr(turbo_config, "use_moe_fp8", False)
34+
)
35+
uses_nonclassic_attention = not bool(getattr(turbo_config, "use_classic_attention", False))
36+
return is_deepseek and is_fp8_recipe and uses_nonclassic_attention and _is_gfx942()
37+
38+
39+
def should_use_turbo_attention(model: Any, turbo_config: Any) -> bool:
40+
return bool(
41+
getattr(turbo_config, "enable_primus_turbo", False)
42+
and getattr(turbo_config, "use_turbo_attention", False)
43+
)

primus/backends/torchtitan/patches/turbo/moe_grouped_mm_patches.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def patch_torchtitan_moe(ctx: PatchContext) -> None:
4141
"""
4242
Patch TorchTitan MoE to use Primus-Turbo grouped_mm implementation.
4343
"""
44-
import torchtitan.models.moe.moe
44+
import torchtitan.models.moe.moe as moe_module
4545

4646
from primus.backends.torchtitan.models.moe.moe import _run_experts_grouped_mm
4747

@@ -64,7 +64,7 @@ def _run_experts_grouped_mm_dynamic(*args, **kwargs):
6464
_run_experts_grouped_mm_dynamic.__qualname__ = _ALREADY_PATCHED_SENTINEL
6565
_run_experts_grouped_mm_dynamic.__name__ = _ALREADY_PATCHED_SENTINEL
6666

67-
torchtitan.models.moe.moe._run_experts_grouped_mm = _run_experts_grouped_mm_dynamic
67+
moe_module._run_experts_grouped_mm = _run_experts_grouped_mm_dynamic
6868

6969
log_rank_0(
7070
"[Patch:torchtitan.primus_turbo.moe_grouped_mm] "

0 commit comments

Comments
 (0)