Fix NaN: compute loss in fp32 outside AMP autocast

Root cause: GradScaler scales gradients by ~65536 in fp16, causing
logit_scale.exp() gradient to overflow. The learnable temperature
and similarity logits must stay in fp32.

Fix: model forward runs inside autocast(fp16), but loss computation
(similarity @ temperature + cross_entropy) runs outside in fp32.

Also: clamp logit_scale in logit-space before exp() and force
similarity computation to fp32.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
pikaliov
2026-04-21 18:42:29 +03:00
parent f41a0f27fe
commit bcb01bcb6d
2 changed files with 17 additions and 10 deletions

View File

@@ -19,14 +19,15 @@ import torch.nn.functional as F
def _symmetric_info_nce( def _symmetric_info_nce(
emb_a: torch.Tensor, emb_a: torch.Tensor,
emb_b: torch.Tensor, emb_b: torch.Tensor,
temperature: float, temperature: float | torch.Tensor,
label_smoothing: float, label_smoothing: float,
weight_a2b: float = 0.5, weight_a2b: float = 0.5,
weight_b2a: float = 0.5, weight_b2a: float = 0.5,
) -> torch.Tensor: ) -> torch.Tensor:
"""Weighted symmetric InfoNCE. Positives on the diagonal.""" """Weighted symmetric InfoNCE. Positives on the diagonal."""
batch_size = emb_a.size(0) batch_size = emb_a.size(0)
logits = emb_a @ emb_b.t() / temperature # Compute logits in fp32 to avoid overflow with small temperature.
logits = emb_a.float() @ emb_b.float().t() / temperature
targets = torch.arange(batch_size, device=emb_a.device) targets = torch.arange(batch_size, device=emb_a.device)
loss_a2b = F.cross_entropy(logits, targets, label_smoothing=label_smoothing) loss_a2b = F.cross_entropy(logits, targets, label_smoothing=label_smoothing)
loss_b2a = F.cross_entropy(logits.t(), targets, label_smoothing=label_smoothing) loss_b2a = F.cross_entropy(logits.t(), targets, label_smoothing=label_smoothing)
@@ -118,10 +119,14 @@ class InfoNCELoss(nn.Module):
Dict with 'total', 'temperature', 'gate'. Dict with 'total', 'temperature', 'gate'.
""" """
if self.learnable_temperature: if self.learnable_temperature:
# Clamp logit_scale to prevent tau from going out of bounds. # Clamp logit_scale in logit space first to prevent exp() overflow in fp16.
logit_scale = self.logit_scale.exp().clamp( # tau_min=0.01 -> max logit_scale=ln(1/0.01)=4.6
min=1.0 / self.tau_max, max=1.0 / self.tau_min, # tau_max=0.5 -> min logit_scale=ln(1/0.5)=0.69
clamped = self.logit_scale.float().clamp(
min=math.log(1.0 / self.tau_max),
max=math.log(1.0 / self.tau_min),
) )
logit_scale = clamped.exp()
tau = 1.0 / logit_scale tau = 1.0 / logit_scale
else: else:
tau = cosine_temperature( tau = cosine_temperature(

View File

@@ -361,6 +361,7 @@ def train(cfg: TrainConfigGTAUAV) -> None:
drone_img = batch["drone_img"].to(cfg.device, non_blocking=True) drone_img = batch["drone_img"].to(cfg.device, non_blocking=True)
sat_img = batch["sat_img"].to(cfg.device, non_blocking=True) sat_img = batch["sat_img"].to(cfg.device, non_blocking=True)
# Model forward in AMP (fp16 for DINOv3/DGTRS encoders).
with autocast(device_type="cuda", enabled=cfg.use_amp): with autocast(device_type="cuda", enabled=cfg.use_amp):
if cfg.baseline_mode: if cfg.baseline_mode:
embeddings = model(drone_img=drone_img, sat_img=sat_img) embeddings = model(drone_img=drone_img, sat_img=sat_img)
@@ -372,11 +373,12 @@ def train(cfg: TrainConfigGTAUAV) -> None:
caption_l2=batch["caption_l2"], caption_l2=batch["caption_l2"],
caption_l3=batch["caption_l3"], caption_l3=batch["caption_l3"],
) )
loss_dict = loss_fn( # Loss in fp32 (learnable temperature gradient overflows in fp16).
embeddings=embeddings, loss_dict = loss_fn(
epoch=epoch, embeddings=embeddings,
total_epochs=cfg.epochs, epoch=epoch,
) total_epochs=cfg.epochs,
)
total_loss = loss_dict["total"] total_loss = loss_dict["total"]
scaler.scale(total_loss).backward() scaler.scale(total_loss).backward()