Skip to content

Commit 7d49617

Browse files
committed
Refactor DeepSeek V4 attention, RoPE, and aux loss
1 parent 811953e commit 7d49617

10 files changed

Lines changed: 304 additions & 299 deletions

File tree

torchtitan/distributed/aux_loss.py

Lines changed: 0 additions & 120 deletions
This file was deleted.

torchtitan/models/common/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
RMSNorm,
3737
SiLU,
3838
)
39-
from .rope import ComplexRoPE, CosSinRoPE, RoPE
39+
from .rope import ComplexRoPE, CosSinRoPE, RoPE, SingleComplexRoPE
4040

4141
__all__ = [
4242
"Conv1d",
@@ -68,6 +68,7 @@
6868
"RoPE",
6969
"ScaledDotProductAttention",
7070
"SiLU",
71+
"SingleComplexRoPE",
7172
"TransformerBlock",
7273
"VarlenAttention",
7374
"VarlenMetadata",
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Model-internal auxiliary losses: gradient injection and logging.
8+
9+
Auxiliary objectives computed inside the model (MoE load-balance, DSA KL, ...)
10+
cannot reach the trainer's loss function under pipeline parallelism. The
11+
gradient injection decouples the gradient from the scalar output, and the
12+
logging framework accumulates per-step values with PP-safe collection.
13+
"""
14+
15+
from collections import defaultdict
16+
from dataclasses import dataclass
17+
from typing import ClassVar
18+
19+
import spmd_types as spmd
20+
import torch
21+
from torch.distributed._functional_collectives import all_reduce as _all_reduce
22+
23+
from torchtitan.protocols.module import Module
24+
from torchtitan.tools.utils import device_type
25+
26+
__all__ = [
27+
"LoggedAuxLoss",
28+
"collect_aux_loss_metrics",
29+
]
30+
31+
32+
@spmd.register_autograd_function
33+
class _AuxLossInjection(torch.autograd.Function):
34+
"""Identity-forward autograd that injects an aux-loss gradient on backward."""
35+
36+
@staticmethod
37+
# pyrefly: ignore [bad-override]
38+
def forward(ctx, carrier: torch.Tensor, aux_loss: torch.Tensor) -> torch.Tensor:
39+
ctx.save_for_backward(aux_loss)
40+
return carrier
41+
42+
@staticmethod
43+
def typecheck_forward(
44+
carrier: torch.Tensor, aux_loss: torch.Tensor
45+
) -> torch.Tensor:
46+
return _AuxLossInjection.apply(carrier, aux_loss)
47+
48+
@staticmethod
49+
def backward( # pyrefly: ignore [bad-override]
50+
ctx, grad_carrier: torch.Tensor
51+
) -> tuple[torch.Tensor, torch.Tensor]:
52+
(aux_loss,) = ctx.saved_tensors
53+
return grad_carrier, torch.ones_like(aux_loss)
54+
55+
56+
class LoggedAuxLoss(Module):
57+
"""Accumulates per-microbatch aux losses via ``inject()`` with deferred readout.
58+
59+
Subclasses call ``inject()`` each microbatch to inject the gradient and
60+
accumulate the scalar value. ``pop()`` reads and resets the accumulator.
61+
"""
62+
63+
# Populated during model build (before PP splitting) so counts and keys are
64+
# identical on every rank. PP stages without aux loss modules still join
65+
# collectives via pre-allocated zero accumulators in collect_aux_loss_metrics.
66+
_group_counts: ClassVar[dict[tuple[str, str], int]] = defaultdict(int)
67+
68+
@dataclass(kw_only=True, slots=True)
69+
class Config(Module.Config):
70+
coeff: float
71+
"""Aux loss coefficient. Scales the gradient contribution."""
72+
tag: str = "aux_loss"
73+
"""Metric name prefix for ``collect_aux_loss_metrics``."""
74+
reduce_mesh: str = "loss"
75+
"""Mesh to average logged values over. PP is reduced separately."""
76+
global_batch_size: int | None = None
77+
"""Per-token denominator, set by ``Decoder.update_from_config``."""
78+
ac_doubled: bool = False
79+
"""AC double-count flag, set by ``Decoder.update_from_config``."""
80+
81+
def __init__(self, config: Config):
82+
super().__init__()
83+
self.coeff = config.coeff
84+
self.tag = config.tag
85+
self.reduce_mesh = config.reduce_mesh
86+
self.global_batch_size = config.global_batch_size
87+
self.ac_doubled = config.ac_doubled
88+
self.register_buffer(
89+
"_acc", torch.zeros((), dtype=torch.float32), persistent=False
90+
)
91+
LoggedAuxLoss._group_counts[(config.reduce_mesh, config.tag)] += 1
92+
93+
def _init_self_buffers(self, *, buffer_device: torch.device | None = None) -> None:
94+
if buffer_device is None:
95+
buffer_device = self._acc.device
96+
with torch.device(buffer_device):
97+
self._acc = torch.zeros((), dtype=torch.float32)
98+
99+
def inject(self, carrier: torch.Tensor, raw_sum: torch.Tensor) -> torch.Tensor:
100+
"""Inject aux loss gradient into the forward graph and accumulate for logging."""
101+
if self.training:
102+
self._acc.add_(raw_sum.detach().float() / self.global_batch_size)
103+
return _AuxLossInjection.apply(
104+
carrier, raw_sum * (self.coeff / self.global_batch_size)
105+
)
106+
107+
def pop(self) -> torch.Tensor:
108+
"""Pop and reset the accumulated value, correcting for AC double-write."""
109+
val = self._acc.detach().clone()
110+
if self.ac_doubled:
111+
val = val / 2
112+
self._acc.zero_()
113+
return val
114+
115+
116+
def collect_aux_loss_metrics(model_parts, parallel_dims) -> dict[str, float]:
117+
"""Collect and reduce aux loss metrics from all model parts for logging.
118+
119+
Returns ``{tag}/mean`` per group; ``{}`` when no aux losses are configured.
120+
"""
121+
if not LoggedAuxLoss._group_counts:
122+
return {}
123+
124+
pp_mesh = parallel_dims.get_optional_mesh("pp")
125+
local_sums = {
126+
key: torch.zeros((), dtype=torch.float32, device=device_type)
127+
for key in LoggedAuxLoss._group_counts
128+
}
129+
130+
for part in model_parts:
131+
for block in getattr(part, "layers", {}).values():
132+
for module in block.modules():
133+
if isinstance(module, LoggedAuxLoss):
134+
local_sums[(module.reduce_mesh, module.tag)] += module.pop()
135+
136+
metrics = {}
137+
for key, total in sorted(local_sums.items()):
138+
mesh_name, tag = key
139+
for mesh in (parallel_dims.get_optional_mesh(mesh_name), pp_mesh):
140+
if mesh is not None:
141+
total = _all_reduce(total, reduceOp="sum", group=mesh)
142+
metrics[f"{tag}/mean"] = float(total.item()) / LoggedAuxLoss._group_counts[key]
143+
return metrics

torchtitan/models/common/decoder.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
get_efficient_causal_mask_mod_for_packed_document,
2626
VarlenAttention,
2727
)
28+
from torchtitan.models.common.aux_loss import LoggedAuxLoss
2829
from torchtitan.models.common.embedding import Embedding
2930
from torchtitan.models.common.feed_forward import FeedForward
3031
from torchtitan.models.common.moe import MoE
@@ -220,6 +221,18 @@ def update_from_config(
220221
debug.moe_force_load_balance
221222
)
222223

224+
# Runtime config fields for aux losses.
225+
for _fqn, aux_loss_cfg, _parent, _attr in self.traverse(
226+
LoggedAuxLoss.Config
227+
):
228+
aux_loss_cfg.global_batch_size = config.training.global_batch_size
229+
# Under compile, functionalize isolates AC recomputation's buffer
230+
# mutation from the module -- only the forward path counts.
231+
aux_loss_cfg.ac_doubled = (
232+
config.activation_checkpoint is not None
233+
and not config.compile.enable
234+
)
235+
223236
# Set by the trainer when ChunkedLossWrapper is used, so lm_head is applied
224237
# per-chunk inside the loss function instead of in forward().
225238
# TODO(#ISSUE): Remove after fixing PP backward to skip non-tensor

torchtitan/models/common/rope.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"ComplexRoPE",
2020
"CosSinRoPE",
2121
"RoPE",
22+
"SingleComplexRoPE",
2223
]
2324

2425

@@ -352,6 +353,37 @@ def _rotate_half(x: torch.Tensor) -> torch.Tensor:
352353
return torch.cat((-x2, x1), dim=-1)
353354

354355

356+
class SingleComplexRoPE(ComplexRoPE):
357+
"""Apply complex RoPE to a single tensor."""
358+
359+
@dataclass(kw_only=True, slots=True)
360+
class Config(ComplexRoPE.Config):
361+
pass
362+
363+
def forward(
364+
self,
365+
x: torch.Tensor,
366+
positions: torch.Tensor | None = None,
367+
*,
368+
inverse: bool = False,
369+
) -> torch.Tensor:
370+
rope_cache = self._reshape_cache(x, positions)
371+
return self.apply_rotary_emb(x, rope_cache, inverse=inverse)
372+
373+
@staticmethod
374+
def apply_rotary_emb(
375+
x: torch.Tensor,
376+
rope_cache: torch.Tensor,
377+
*,
378+
inverse: bool,
379+
) -> torch.Tensor:
380+
if inverse:
381+
rope_cache = rope_cache.conj()
382+
x_ = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
383+
x_out = torch.view_as_real(x_ * rope_cache).flatten(-2)
384+
return x_out.type_as(x)
385+
386+
355387
@spmd.local_map(out_types={"dp": spmd.S(0), "cp": spmd.S(1), "tp": spmd.R})
356388
def _reshape_for_broadcast(
357389
rope_cache: torch.Tensor,

0 commit comments

Comments
 (0)