|
| 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 |
0 commit comments