mattbitzesty's picture
fix(loss): prevent objective-starvation with fixed task weights; target pimt_v3.pt
9ddcf86 unverified
Raw
History Blame Contribute Delete
3.22 kB
import torch
import torch.nn as nn
class AdaptiveLossBalancer(nn.Module):
"""
Multi-task loss weighting with optional adaptive floor.
Two modes:
* ``fixed_weights``: directly supplied task weights (e.g., priority weighting
for the time-resolved objective head). This is the default in PINO because
the objective head is the only task that consumes the physics trajectory;
letting it be starved removes the model's ability to learn time dynamics.
* ``adaptive`` mode: weights are derived from per-task progress ratios
(current / initial loss) with a per-task floor so that fast-converging
tasks are not dropped to zero.
The computed weights are stored in ``self.last_weights`` as a detached CPU list
so training loops can log them without modifying the loss flow.
"""
def __init__(
self,
num_tasks: int = 4,
fixed_weights: torch.Tensor | None = None,
momentum: float = 0.9,
min_weight: float = 0.20,
) -> None:
super().__init__()
self.momentum = momentum
self.min_weight = min_weight
self.register_buffer("running_losses", torch.ones(num_tasks))
self.register_buffer("initial_losses", torch.zeros(num_tasks))
self.register_buffer("_initialized", torch.tensor(0, dtype=torch.bool))
if fixed_weights is not None:
fixed = torch.as_tensor(fixed_weights, dtype=torch.float32)
if fixed.dim() == 1 and fixed.numel() == num_tasks:
self.register_buffer("fixed_weights", fixed)
else:
raise ValueError(
f"fixed_weights must be a {num_tasks}-element vector, got shape {fixed.shape}"
)
else:
self.register_buffer("fixed_weights", torch.zeros(num_tasks))
def forward(self, loss_dict: dict[str, torch.Tensor]) -> torch.Tensor:
"""Combine task losses with either fixed or adaptive weights."""
losses = torch.stack(list(loss_dict.values()))
num_tasks = float(len(losses))
if self.fixed_weights.abs().sum().item() > 0:
weights = self.fixed_weights.to(losses.device)
self.last_weights = weights.detach().cpu().tolist()
return torch.sum(losses * weights)
# Adaptive fallback.
if not self._initialized.item() or torch.all(self.initial_losses == 0):
self.initial_losses.copy_(losses.detach().clamp(min=1e-5))
self.running_losses.copy_(losses.detach().clamp(min=1e-5))
self._initialized.fill_(True)
self.running_losses.copy_(
self.momentum * self.running_losses + (1 - self.momentum) * losses.detach()
)
ratios = self.running_losses / self.initial_losses
weights = ratios / (torch.sum(ratios) + 1e-8)
# Apply a floor so that fast-converging tasks are not starved.
weights = torch.clamp(weights, min=self.min_weight) # type: ignore[attr-defined]
weights = weights / (torch.sum(weights) + 1e-8) # type: ignore[attr-defined]
self.last_weights = weights.detach().cpu().tolist()
return torch.sum(losses * weights * num_tasks)