|
6 | 6 | import os |
7 | 7 | import time |
8 | 8 | from contextlib import nullcontext |
9 | | -from dataclasses import dataclass |
| 9 | +from dataclasses import dataclass, field |
10 | 10 | from typing import Any, Callable, Optional |
11 | 11 |
|
12 | 12 | import torch |
@@ -44,6 +44,8 @@ class TrainState: |
44 | 44 | global_step: int = 0 |
45 | 45 | samples_seen: int = 0 |
46 | 46 | 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) |
47 | 49 |
|
48 | 50 |
|
49 | 51 | def estimate_train_state_counters(epoch: int, data: dict, args) -> tuple[int, int]: |
@@ -93,6 +95,25 @@ def update(self, val, n=1): |
93 | 95 | self.avg = self.sum / self.count |
94 | 96 |
|
95 | 97 |
|
| 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 | + |
96 | 117 | def postprocess_clip_output(model_out): |
97 | 118 | return { |
98 | 119 | "image_features": model_out[0], |
@@ -340,6 +361,14 @@ def train_one_epoch(state: TrainState, data, args, tb_writer=None): |
340 | 361 | data_time_m = AverageMeter() |
341 | 362 | end = time.time() |
342 | 363 | 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 |
343 | 372 | for i, batch in enumerate(dataloader): |
344 | 373 | i_accum = i // args.accum_freq |
345 | 374 | step = num_batches_per_epoch * epoch + i_accum |
@@ -375,66 +404,100 @@ def train_one_epoch(state: TrainState, data, args, tb_writer=None): |
375 | 404 | num_samples += step_batch_size * args.world_size |
376 | 405 | state.global_step = step + 1 |
377 | 406 | 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 | + ) |
409 | 483 |
|
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() |
437 | 487 | # 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) |
438 | 501 |
|
439 | 502 |
|
440 | 503 | def zero_shot_eval_all(task, data, epoch, args, tokenizer=None): |
|
0 commit comments