Skip to content

Commit bfb1975

Browse files
committed
Update logging, EMA value in console (), separate rate for W&B / TB metric loggers. Epoch avg loss at end of epoch. Reduce loss when needed for metrics.
1 parent db2db2b commit bfb1975

3 files changed

Lines changed: 144 additions & 61 deletions

File tree

src/open_clip_train/legacy_train.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def train_one_epoch(model, data, loss, epoch, optimizer, scaler, scheduler, dist
237237
"batch_time": batch_time_m.val,
238238
"samples_per_second": samples_per_second,
239239
"samples_per_second_per_gpu": samples_per_second_per_gpu,
240-
"scale": logit_scale_scalar,
240+
"logit_scale": logit_scale_scalar,
241241
"lr": optimizer.param_groups[0]["lr"]
242242
}
243243
log_data.update({name:val.val for name,val in losses_m.items()})

src/open_clip_train/params.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,23 @@ def parse_args(args):
622622
"--log-every-n-steps",
623623
type=int,
624624
default=100,
625-
help="Log every n steps to tensorboard/console/wandb.",
625+
help="Log every n steps to the console (the human-readable line).",
626+
)
627+
parser.add_argument(
628+
"--log-metric-every-n-steps",
629+
type=int,
630+
default=10,
631+
help="Log scalars to tensorboard/wandb every n steps (denser than the console for smooth curves). "
632+
"Set 1 to log every step. The loss is all-reduced across ranks here so the logged value is the "
633+
"true global-batch loss (under --local-loss each rank only sees a 1/world_size slice).",
634+
)
635+
parser.add_argument(
636+
"--train-loss-ema-samples",
637+
type=int,
638+
default=50000,
639+
help="Smoothing horizon (in samples) for the console loss EMA shown in parentheses. Robust to batch "
640+
"size / accum / world size / NaFlex packing. 0 disables it (console parentheses revert to the "
641+
"epoch running average).",
626642
)
627643
parser.add_argument(
628644
"--coca-caption-loss-weight",
@@ -817,6 +833,10 @@ def parse_args(args):
817833
if args.text_pad_multiple is not None and args.text_pad_multiple <= 0:
818834
raise ValueError(f"--text-pad-multiple must be > 0 when set, got {args.text_pad_multiple}.")
819835

836+
# A negative EMA horizon would make the decay exp(-n/h) > 1 and diverge the EMA; 0 disables it.
837+
if args.train_loss_ema_samples < 0:
838+
raise ValueError(f"--train-loss-ema-samples must be >= 0 (0 disables), got {args.train_loss_ema_samples}.")
839+
820840
# GenLIP is a generative model with its own NaFlex linear patch-embed: it consumes the NaFlex data
821841
# pipeline but must NOT have its vision tower converted to a timm NaFlexVit (force_naflex_vision).
822842
args.genlip = 'genlip' in args.model.lower()

src/open_clip_train/train.py

Lines changed: 122 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import os
77
import time
88
from contextlib import nullcontext
9-
from dataclasses import dataclass
9+
from dataclasses import dataclass, field
1010
from typing import Any, Callable, Optional
1111

1212
import torch
@@ -44,6 +44,8 @@ class TrainState:
4444
global_step: int = 0
4545
samples_seen: int = 0
4646
compiled_train_step: Optional[Callable] = None
47+
# Persistent cross-epoch loss EMA meters (runtime logging state; intentionally not checkpointed).
48+
losses_ema: dict = field(default_factory=dict)
4749

4850

4951
def estimate_train_state_counters(epoch: int, data: dict, args) -> tuple[int, int]:
@@ -93,6 +95,25 @@ def update(self, val, n=1):
9395
self.avg = self.sum / self.count
9496

9597

98+
class SampleWeightedEMA:
99+
"""Sample-count-weighted EMA of a scalar.
100+
101+
The smoothing horizon is expressed in samples (``ema_samples``), so it is invariant to batch size, gradient
102+
accumulation, world size, and NaFlex packing -- ``decay = exp(-n / ema_samples)`` per update of ``n`` samples.
103+
The first observation seeds ``value`` directly (no cold-start ramp from 0), which keeps logs clean after a
104+
``--resume`` since this is runtime-only logging state and is not checkpointed.
105+
"""
106+
107+
def __init__(self, ema_samples: float):
108+
self.ema_samples = float(ema_samples)
109+
self.value = None
110+
111+
def update(self, val, n):
112+
decay = math.exp(-n / self.ema_samples)
113+
self.value = val if self.value is None else decay * self.value + (1.0 - decay) * val
114+
return self.value
115+
116+
96117
def postprocess_clip_output(model_out):
97118
return {
98119
"image_features": model_out[0],
@@ -340,6 +361,14 @@ def train_one_epoch(state: TrainState, data, args, tb_writer=None):
340361
data_time_m = AverageMeter()
341362
end = time.time()
342363
num_samples = 0
364+
# Log the global-batch loss: under --local-loss each rank's loss is a 1/world_size slice (generative LM losses
365+
# are likewise per-rank), so all-reduce-mean at log time. Mean is a no-op when ranks already agree.
366+
reduce_loss = args.distributed and args.world_size > 1
367+
ema_samples = getattr(args, "train_loss_ema_samples", 0)
368+
metric_every = max(1, getattr(args, "log_metric_every_n_steps", args.log_every_n_steps))
369+
# Global samples observed at the previous EMA update (seeded from the persistent counter so the EMA horizon
370+
# stays continuous across epochs); the EMA decays by the sample delta between metric logs.
371+
prev_metric_samples = state.samples_seen
343372
for i, batch in enumerate(dataloader):
344373
i_accum = i // args.accum_freq
345374
step = num_batches_per_epoch * epoch + i_accum
@@ -375,66 +404,100 @@ def train_one_epoch(state: TrainState, data, args, tb_writer=None):
375404
num_samples += step_batch_size * args.world_size
376405
state.global_step = step + 1
377406
state.samples_seen += step_batch_size * args.world_size
378-
if is_master(args) and (i_accum % args.log_every_n_steps == 0 or batch_count == num_batches_per_epoch):
379-
samples_per_epoch = dataloader.num_samples
380-
percent_complete = 100.0 * batch_count / num_batches_per_epoch
381-
382-
# NOTE loss is coarsely sampled, just master node and per log update
383-
for key, val in losses.items():
384-
if key not in losses_m:
385-
losses_m[key] = AverageMeter()
386-
losses_m[key].update(val.item(), step_batch_size)
387-
388-
# logit_scale / logit_bias are per-step report scalars (not loss terms), logged once from `report`.
389-
logit_scale = report.get("logit_scale", None)
390-
logit_scale_scalar = logit_scale.item() if logit_scale is not None else 0.0
391-
logit_bias = report.get("logit_bias", None)
392-
logit_bias_scalar = logit_bias.item() if logit_bias is not None else None
393-
loss_log = " ".join(
394-
[
395-
f"{loss_name.capitalize()}: {loss_m.val:#.5g} ({loss_m.avg:#.5g})"
396-
for loss_name, loss_m in losses_m.items()
397-
]
398-
)
399-
samples_per_second = step_batch_size * args.world_size / batch_time_m.val
400-
samples_per_second_per_gpu = step_batch_size / batch_time_m.val
401-
learning_rate = get_learning_rate(optimizer)
402-
_logger.info(
403-
f"Train Epoch: {epoch} [{num_samples:>{sample_digits}}/{samples_per_epoch} ({percent_complete:.0f}%)] "
404-
f"Data (t): {data_time_m.avg:.3f} "
405-
f"Batch (t): {batch_time_m.avg:.3f}, {samples_per_second:#g}/s, {samples_per_second_per_gpu:#g}/s/gpu "
406-
f"LR: {learning_rate:5f} "
407-
f"Logit Scale: {logit_scale_scalar:.3f} " + loss_log
408-
)
407+
last_batch = batch_count == num_batches_per_epoch
408+
is_console_step = (i_accum % args.log_every_n_steps == 0) or last_batch
409+
is_metric_step = (i_accum % metric_every == 0) or is_console_step
410+
411+
if is_metric_step:
412+
# Reduce the loss across ranks so the logged value is the true global-batch loss. Collective -> ALL
413+
# ranks call it (the rank-synced schedule keeps them in lockstep); detach()+clone() since backward
414+
# already ran and all_reduce mutates in place. Mean is a no-op when ranks already agree.
415+
reduced = {key: val.detach().float().clone() for key, val in losses.items()}
416+
if reduce_loss:
417+
for v in reduced.values():
418+
dist.all_reduce(v)
419+
v /= args.world_size
420+
421+
if is_master(args):
422+
n_ema = state.samples_seen - prev_metric_samples # global samples since the previous EMA update
423+
prev_metric_samples = state.samples_seen
424+
for key, v in reduced.items():
425+
vi = v.item()
426+
losses_m.setdefault(key, AverageMeter()).update(vi, step_batch_size)
427+
if ema_samples and n_ema > 0:
428+
state.losses_ema.setdefault(key, SampleWeightedEMA(ema_samples)).update(vi, n_ema)
429+
430+
# logit_scale / logit_bias are per-step report scalars (not loss terms), logged once from `report`.
431+
logit_scale = report.get("logit_scale", None)
432+
logit_scale_scalar = logit_scale.item() if logit_scale is not None else 0.0
433+
logit_bias = report.get("logit_bias", None)
434+
logit_bias_scalar = logit_bias.item() if logit_bias is not None else None
435+
samples_per_second = step_batch_size * args.world_size / batch_time_m.val
436+
samples_per_second_per_gpu = step_batch_size / batch_time_m.val
437+
learning_rate = get_learning_rate(optimizer)
438+
439+
# Raw scalars to tensorboard/wandb at the (dense) metric cadence (see the per-loss block below).
440+
log_data = {
441+
"data_time": data_time_m.val,
442+
"batch_time": batch_time_m.val,
443+
"samples_per_second": samples_per_second,
444+
"samples_per_second_per_gpu": samples_per_second_per_gpu,
445+
"logit_scale": logit_scale_scalar,
446+
"lr": learning_rate,
447+
}
448+
if logit_bias_scalar is not None:
449+
log_data["logit_bias"] = logit_bias_scalar
450+
# Raw current value only -- dashboards do their own smoothing, so the EMA stays console-only (add
451+
# train/<loss>_ema behind an explicit flag if a deterministic logged series is ever wanted). The
452+
# epoch running average is NOT logged per-step (half-formed mid-epoch); it goes out once per epoch.
453+
for name, m in losses_m.items():
454+
log_data[name] = m.val
455+
log_data = {"train/" + name: val for name, val in log_data.items()}
456+
457+
if tb_writer is not None:
458+
for name, val in log_data.items():
459+
tb_writer.add_scalar(name, val, step)
460+
461+
if args.wandb:
462+
assert wandb is not None, 'Please install wandb.'
463+
log_data['step'] = step # for backwards compatibility
464+
wandb.log(log_data, step=step)
465+
466+
# Console at the (sparse) cadence. Parentheses show the cross-epoch EMA trend (the epoch average
467+
# moves to the End-epoch summary line); falls back to the epoch avg when the EMA is disabled.
468+
if is_console_step:
469+
samples_per_epoch = dataloader.num_samples
470+
percent_complete = 100.0 * batch_count / num_batches_per_epoch
471+
loss_log = " ".join(
472+
f"{name.capitalize()}: {m.val:#.5g} "
473+
f"({(state.losses_ema[name].value if ema_samples else m.avg):#.5g})"
474+
for name, m in losses_m.items()
475+
)
476+
_logger.info(
477+
f"Train Epoch: {epoch} [{num_samples:>{sample_digits}}/{samples_per_epoch} ({percent_complete:.0f}%)] "
478+
f"Data (t): {data_time_m.avg:.3f} "
479+
f"Batch (t): {batch_time_m.avg:.3f}, {samples_per_second:#g}/s, {samples_per_second_per_gpu:#g}/s/gpu "
480+
f"LR: {learning_rate:5f} "
481+
f"Logit Scale: {logit_scale_scalar:.3f} " + loss_log
482+
)
409483

410-
# Save train loss / etc. Using non avg meter values as loggers have their own smoothing
411-
log_data = {
412-
"data_time": data_time_m.val,
413-
"batch_time": batch_time_m.val,
414-
"samples_per_second": samples_per_second,
415-
"samples_per_second_per_gpu": samples_per_second_per_gpu,
416-
"scale": logit_scale_scalar,
417-
"lr": learning_rate,
418-
}
419-
if logit_bias_scalar is not None:
420-
log_data["bias"] = logit_bias_scalar
421-
log_data.update({name:val.val for name,val in losses_m.items()})
422-
423-
log_data = {"train/" + name: val for name, val in log_data.items()}
424-
425-
if tb_writer is not None:
426-
for name, val in log_data.items():
427-
tb_writer.add_scalar(name, val, step)
428-
429-
if args.wandb:
430-
assert wandb is not None, 'Please install wandb.'
431-
log_data['step'] = step # for backwards compatibility
432-
wandb.log(log_data, step=step)
433-
434-
# resetting batch / data time meters per log window
435-
batch_time_m.reset()
436-
data_time_m.reset()
484+
# reset batch / data time meters per metric window
485+
batch_time_m.reset()
486+
data_time_m.reset()
437487
# end for
488+
if is_master(args) and losses_m:
489+
summary = " ".join(f"Avg {name.capitalize()}: {m.avg:#.5g}" for name, m in losses_m.items())
490+
_logger.info(f"End epoch {epoch} | {summary}")
491+
# One epoch-average point per epoch (distinct, sparse series from the dense per-step curve). Logged at the
492+
# epoch's final step so it merges with that step's record rather than starting a new one.
493+
last_step = num_batches_per_epoch * (epoch + 1) - 1
494+
epoch_log = {f"train/{name}_epoch_avg": m.avg for name, m in losses_m.items()}
495+
if tb_writer is not None:
496+
for name, val in epoch_log.items():
497+
tb_writer.add_scalar(name, val, last_step)
498+
if args.wandb:
499+
assert wandb is not None, 'Please install wandb.'
500+
wandb.log({**epoch_log, "epoch": epoch}, step=last_step)
438501

439502

440503
def zero_shot_eval_all(task, data, epoch, args, tokenizer=None):

0 commit comments

Comments
 (0)