mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-24 10:16:09 +00:00
Aggregate policy sub-losses through MetricsTracker (#4024)
This commit is contained in:
@@ -171,6 +171,9 @@ def update_policy(
|
|||||||
train_metrics.update_s = time.perf_counter() - start_time
|
train_metrics.update_s = time.perf_counter() - start_time
|
||||||
if torch.cuda.is_available():
|
if torch.cuda.is_available():
|
||||||
train_metrics.gpu_mem_gb = torch.cuda.max_memory_allocated() / (1024**3)
|
train_metrics.gpu_mem_gb = torch.cuda.max_memory_allocated() / (1024**3)
|
||||||
|
# Aggregate the policy's scalar outputs for logging and rank-reduction across the log window.
|
||||||
|
if output_dict:
|
||||||
|
train_metrics.update_metrics(output_dict)
|
||||||
return train_metrics, output_dict
|
return train_metrics, output_dict
|
||||||
|
|
||||||
|
|
||||||
@@ -572,7 +575,7 @@ def train(cfg: TrainPipelineConfig, accelerator: "Accelerator | None" = None):
|
|||||||
batch = preprocessor(batch)
|
batch = preprocessor(batch)
|
||||||
train_tracker.dataloading_s = time.perf_counter() - start_time
|
train_tracker.dataloading_s = time.perf_counter() - start_time
|
||||||
|
|
||||||
train_tracker, output_dict = update_policy(
|
train_tracker, _ = update_policy(
|
||||||
train_tracker,
|
train_tracker,
|
||||||
policy,
|
policy,
|
||||||
batch,
|
batch,
|
||||||
@@ -605,9 +608,10 @@ def train(cfg: TrainPipelineConfig, accelerator: "Accelerator | None" = None):
|
|||||||
train_tracker.samples_per_s = effective_batch_size / step_time
|
train_tracker.samples_per_s = effective_batch_size / step_time
|
||||||
logging.info(train_tracker)
|
logging.info(train_tracker)
|
||||||
if wandb_logger:
|
if wandb_logger:
|
||||||
|
# Policy sub-losses (latent_loss, action_loss, ...) are aggregated into the
|
||||||
|
# tracker by update_policy, so to_dict() already carries their windowed,
|
||||||
|
# rank-reduced averages — no per-step output_dict passthrough needed.
|
||||||
wandb_log_dict = train_tracker.to_dict()
|
wandb_log_dict = train_tracker.to_dict()
|
||||||
if output_dict:
|
|
||||||
wandb_log_dict.update(output_dict)
|
|
||||||
# Log sample weighting statistics if enabled
|
# Log sample weighting statistics if enabled
|
||||||
if sample_weighter is not None:
|
if sample_weighter is not None:
|
||||||
weighter_stats = sample_weighter.get_stats()
|
weighter_stats = sample_weighter.get_stats()
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class MetricsTracker:
|
|||||||
"episodes",
|
"episodes",
|
||||||
"epochs",
|
"epochs",
|
||||||
"accelerator",
|
"accelerator",
|
||||||
|
"_caller_metrics",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -129,6 +130,9 @@ class MetricsTracker:
|
|||||||
self.episodes = self.samples / self._avg_samples_per_ep
|
self.episodes = self.samples / self._avg_samples_per_ep
|
||||||
self.epochs = self.samples / self._num_frames
|
self.epochs = self.samples / self._num_frames
|
||||||
self.accelerator = accelerator
|
self.accelerator = accelerator
|
||||||
|
# Meter names the caller registered up front. update_metrics() leaves these untouched, so a
|
||||||
|
# policy that echoes e.g. "loss" in its output dict can't clobber the aggregated meter.
|
||||||
|
self._caller_metrics: set[str] = set(self.metrics)
|
||||||
|
|
||||||
def __getattr__(self, name: str) -> int | dict[str, AverageMeter] | AverageMeter | Any:
|
def __getattr__(self, name: str) -> int | dict[str, AverageMeter] | AverageMeter | Any:
|
||||||
if name in self.__dict__:
|
if name in self.__dict__:
|
||||||
@@ -156,6 +160,21 @@ class MetricsTracker:
|
|||||||
self.episodes = self.samples / self._avg_samples_per_ep
|
self.episodes = self.samples / self._avg_samples_per_ep
|
||||||
self.epochs = self.samples / self._num_frames
|
self.epochs = self.samples / self._num_frames
|
||||||
|
|
||||||
|
def update_metrics(self, values: dict[str, Any]) -> None:
|
||||||
|
"""Accumulate a dict of scalar metrics, auto-registering a meter for each new key.
|
||||||
|
|
||||||
|
Non-numeric values and bools are ignored.
|
||||||
|
Caller-registered metrics (those passed to the constructor) are never overridden.
|
||||||
|
"""
|
||||||
|
for name, value in values.items():
|
||||||
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||||
|
continue
|
||||||
|
if name in self._caller_metrics:
|
||||||
|
continue
|
||||||
|
if name not in self.metrics:
|
||||||
|
self.metrics[name] = AverageMeter(name, ":.3f", reduction="mean")
|
||||||
|
self.metrics[name].update(float(value))
|
||||||
|
|
||||||
def reduce_across_ranks(self) -> None:
|
def reduce_across_ranks(self) -> None:
|
||||||
"""
|
"""
|
||||||
Synchronises the running averages of every metric whose ``reduction`` is not ``"none"``
|
Synchronises the running averages of every metric whose ``reduction`` is not ``"none"``
|
||||||
|
|||||||
@@ -233,3 +233,37 @@ def test_metrics_tracker_reduce_across_ranks_invokes_reduce():
|
|||||||
# accumulate against the cluster view rather than the stale per-rank sum.
|
# accumulate against the cluster view rather than the stale per-rank sum.
|
||||||
meter = tracker.update_s
|
meter = tracker.update_s
|
||||||
assert meter.sum / meter.count == pytest.approx(meter.avg)
|
assert meter.sum / meter.count == pytest.approx(meter.avg)
|
||||||
|
|
||||||
|
|
||||||
|
def test_metrics_tracker_update_metrics_registers_and_averages():
|
||||||
|
tracker = MetricsTracker(batch_size=32, num_frames=1000, num_episodes=50, metrics={})
|
||||||
|
tracker.update_metrics({"latent_loss": 0.2, "action_loss": 0.4})
|
||||||
|
tracker.update_metrics({"latent_loss": 0.4, "action_loss": 0.6})
|
||||||
|
|
||||||
|
# New keys are auto-registered as mean-reduced meters and averaged over the window.
|
||||||
|
assert tracker.metrics["latent_loss"].reduction == "mean"
|
||||||
|
assert tracker.metrics["latent_loss"].avg == pytest.approx(0.3)
|
||||||
|
assert tracker.metrics["action_loss"].avg == pytest.approx(0.5)
|
||||||
|
assert tracker.to_dict()["latent_loss"] == pytest.approx(0.3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_metrics_tracker_update_metrics_skips_non_numeric():
|
||||||
|
tracker = MetricsTracker(batch_size=32, num_frames=1000, num_episodes=50, metrics={})
|
||||||
|
tracker.update_metrics({"loss": 0.5, "head_mode": "sparse", "enabled": True})
|
||||||
|
|
||||||
|
# strings and bools ignored
|
||||||
|
assert "loss" in tracker.metrics
|
||||||
|
assert "head_mode" not in tracker.metrics
|
||||||
|
assert "enabled" not in tracker.metrics
|
||||||
|
|
||||||
|
|
||||||
|
def test_metrics_tracker_update_metrics_does_not_override_caller_meter():
|
||||||
|
# A policy that echoes "loss" in its output dict must not overwrite the caller-owned,
|
||||||
|
# already-aggregated loss meter.
|
||||||
|
metrics = {"loss": AverageMeter("loss", ":.3f", reduction="mean")}
|
||||||
|
tracker = MetricsTracker(batch_size=32, num_frames=1000, num_episodes=50, metrics=metrics)
|
||||||
|
tracker.loss = 1.0 # caller-set optimized loss
|
||||||
|
tracker.update_metrics({"loss": 99.0, "latent_loss": 0.2})
|
||||||
|
|
||||||
|
assert tracker.metrics["loss"].avg == pytest.approx(1.0) # snapshot ignored
|
||||||
|
assert tracker.metrics["latent_loss"].avg == pytest.approx(0.2)
|
||||||
|
|||||||
Reference in New Issue
Block a user