Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/rfdetr/training/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,21 @@ def training_step(self, batch: Tuple, batch_idx: int) -> torch.Tensor:
sync_dist=train_log_sync_dist,
batch_size=batch_size,
)
optimizer = self.optimizers()
if isinstance(optimizer, list):
optimizer = optimizer[0]
# Optimizer may have multiple param groups with different LRs (e.g., backbone/decoder).
# Preserve the first group's LR for backward compatibility, but also log the
# min/max across all groups so the progress bar reflects the full schedule.
group_lrs = [pg["lr"] for pg in optimizer.param_groups if "lr" in pg]
if group_lrs:
base_lr = group_lrs[0]
min_lr = min(group_lrs)
max_lr = max(group_lrs)
# Keep LR visible in the live progress bar every step.
self.log("train/lr", base_lr, prog_bar=True, on_step=True, on_epoch=False)
self.log("train/lr_min", min_lr, prog_bar=True, on_step=True, on_epoch=False)
self.log("train/lr_max", max_lr, prog_bar=True, on_step=True, on_epoch=False)
return loss_scaled

def validation_step(self, batch: Tuple, batch_idx: int) -> Dict[str, Any]:
Expand Down
1 change: 1 addition & 0 deletions tests/training/test_metrics_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def _fit_and_read_csv(mc: RFDETRBaseConfig, tc: TrainConfig, criterion=None) ->
_REQUIRED_DETECTION = frozenset(
{
"train/loss",
"train/lr",
"val/loss",
"val/mAP_50",
"val/mAP_50_95",
Expand Down
16 changes: 16 additions & 0 deletions tests/training/test_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,10 @@ def _run_step(self, tmp_path, loss_dict=None, weight_dict=None, accumulate_grad_
fake_criterion.weight_dict = weight_dict or {"loss_ce": 1.0}
module.log = MagicMock()
module.log_dict = MagicMock()
# Provide a real optimizer so param_groups carries a real "lr" key.
real_param = nn.Parameter(torch.randn(4))
real_optimizer = torch.optim.SGD([real_param], lr=1e-3)
module.optimizers = MagicMock(return_value=real_optimizer)
trainer = MagicMock()
trainer.accumulate_grad_batches = accumulate_grad_batches
module._trainer = trainer
Expand Down Expand Up @@ -622,6 +626,18 @@ def test_logs_train_loss_to_prog_bar(self, tmp_path):
assert len(train_loss_calls) == 1
assert train_loss_calls[0].kwargs.get("prog_bar") is True

def test_logs_learning_rate_to_prog_bar(self, tmp_path):
"""Current learning rate must be logged as train/lr with prog_bar=True for monitoring."""
module, samples, targets, _, _ = self._run_step(tmp_path)

module.training_step((samples, targets), batch_idx=0)

lr_calls = [c for c in module.log.call_args_list if c[0][0] == "train/lr"]
assert len(lr_calls) == 1
assert lr_calls[0].kwargs.get("prog_bar") is True
Comment thread
Borda marked this conversation as resolved.
assert lr_calls[0].kwargs.get("on_step") is True
assert lr_calls[0].kwargs.get("on_epoch") is False

def test_logs_individual_losses_as_dict(self, tmp_path):
"""Each component loss must be logged separately under train/ prefix."""
loss_dict = {"loss_ce": torch.tensor(0.5), "loss_bbox": torch.tensor(0.3)}
Expand Down
Loading