Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
ac05221
add pytorch profiler
Feb 16, 2026
30fc75f
add profiler_train
Feb 16, 2026
1b834fb
add helper function for profiling
Feb 16, 2026
12f10fb
add profiler_logs to gitignore
Feb 16, 2026
e7ec914
merge branch
Feb 16, 2026
c5465f8
resolve merge conflict
Feb 16, 2026
6ef0d7b
change default to profiling
Feb 17, 2026
eea5923
revise path
Feb 17, 2026
718ff42
add run_profiler to the config
Mar 2, 2026
cb11636
create profiler_logs in shared path
Mar 2, 2026
fc2cae0
revise helper func.
Mar 2, 2026
de671c3
add ProfilingTrainer class
Mar 2, 2026
7b19e2d
resolve the bug
Mar 4, 2026
3cee5b0
Merge branch 'develop' into javad/dev/pytorch_profiler
javak87 Mar 18, 2026
95839bd
resolve conflict
Mar 18, 2026
a76317f
skipping profiling for ARM CPUs
Mar 18, 2026
251ef08
revise comments
Mar 18, 2026
90beda2
add profiling config
Mar 19, 2026
84d7320
add _check_profiling func
Mar 19, 2026
52bfc4a
add run_profile & profile for a seperate entry point
Mar 19, 2026
b49d8f8
update ProfilerTrainer class
Mar 19, 2026
fff61b1
add profile_parser for profile entry point
Mar 19, 2026
07e5bf5
replace rank zero with is_root()
Mar 19, 2026
87a9e12
remove self._log_terminal for profiling
Mar 19, 2026
9d9c903
revise profiling keys
Mar 19, 2026
d0015ea
revise profiling keys
Mar 19, 2026
856c2c3
Merge branch 'develop' into javad/dev/pytorch_profiler
javak87 May 4, 2026
ca4f9f9
rm entry point
May 6, 2026
764e829
disable profiling in config
May 6, 2026
6e98810
Merge branch 'develop' into javad/dev/pytorch_profiler
javak87 May 6, 2026
008aac2
structural improvements
grassesi May 7, 2026
253a8b3
fixes
grassesi May 7, 2026
cccecc3
fixes
grassesi May 7, 2026
c2633c3
add upper bound for in profiling
May 8, 2026
6b80c73
Fix linter errors.
grassesi May 8, 2026
2161f00
Merge branch 'develop' into javad/dev/pytorch_profiler
florianscheidl Jun 12, 2026
db0142d
Merge branch 'javad/dev/pytorch_profiler' of github.com:javak87/Weath…
florianscheidl Jun 12, 2026
ba33afa
Various refactoring
florianscheidl Jun 12, 2026
9cd0b8e
refactor trainer
florianscheidl Jun 12, 2026
30b1dfa
add model batch
florianscheidl Jun 12, 2026
9da813b
Basic profiling config
florianscheidl Jun 12, 2026
7f3a6a0
Add profiling trainer in run_train
florianscheidl Jun 12, 2026
4a4fb63
minimal config
florianscheidl Jun 12, 2026
4563399
specify config
florianscheidl Jun 12, 2026
adc8445
Fix naming
florianscheidl Jun 12, 2026
4cdf918
Simple profiling config
florianscheidl Jun 12, 2026
be7b480
skip save in profielr
florianscheidl Jun 12, 2026
cc8d601
Linter fixes
florianscheidl Jun 15, 2026
4a43c71
Merge branch 'flo/revisit-profiler-pr' of github.com:florianscheidl/W…
florianscheidl Jun 15, 2026
471e346
Refactor to avoid getattr
florianscheidl Jun 15, 2026
5988450
profiling path and defaults
florianscheidl Jun 16, 2026
5828300
run trainer with factory
florianscheidl Jun 16, 2026
28f7fcd
Clean up trainer
florianscheidl Jun 16, 2026
a82c7e7
Merge branch 'flo/revisit-profiler-pr' of github.com:florianscheidl/W…
florianscheidl Jun 16, 2026
22e3798
minimal config for profiling to run
florianscheidl Jun 16, 2026
e62c082
Fix _train_batch
florianscheidl Jun 16, 2026
41e0319
Refactor such that profiling logs no longer own dir
florianscheidl Jun 16, 2026
5cfb05e
Refactor trainer
florianscheidl Jun 16, 2026
1fd13ba
Formatting
florianscheidl Jun 16, 2026
ce47411
rename paths
florianscheidl Jun 16, 2026
402ae56
linting
florianscheidl Jun 16, 2026
211a5b6
Merge branch 'develop' into flo/revisit-profiler-pr
florianscheidl Jun 16, 2026
d75d028
Merge branch 'flo/revisit-profiler-pr' of github.com:florianscheidl/W…
florianscheidl Jun 16, 2026
ff004da
initialize variable in init
florianscheidl Jun 16, 2026
9fa5ca8
Merge branch 'develop' into flo/revisit-profiler-pr
florianscheidl Jun 26, 2026
2834cd4
Merge branch 'develop' into flo/revisit-profiler-pr
florianscheidl Jun 26, 2026
b04b67f
linting reformatting
florianscheidl Jun 26, 2026
db99917
Merge branch 'develop' into flo/revisit-profiler-pr
grassesi Jun 29, 2026
3a72a75
Merge branch 'develop' into flo/revisit-profiler-pr
grassesi Jul 1, 2026
2258a67
Merge branch 'develop' into flo/revisit-profiler-pr
florianscheidl Jul 29, 2026
4cde9b8
Remove performance configs
florianscheidl Aug 4, 2026
65a49f7
Refactor to move as much out of trainer
florianscheidl Aug 4, 2026
7971435
Move performance utils outside of trainer, implement as wrapper instead
florianscheidl Aug 4, 2026
671332b
Undo changes in default_config
florianscheidl Aug 4, 2026
76d18b8
Merge branch 'develop' into fs/refactor-profiler-pr
florianscheidl Aug 4, 2026
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
53 changes: 53 additions & 0 deletions config/config_performance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Overwrite config for measuring a training run, e.g.
# train --config config/config_performance.yml
# Both sections below are handled by the ProfilingTrainer
# (src/weathergen/train/profiling_trainer.py), which replaces the Trainer as soon as either
# of them asks for something. They are absent from default_config.yml: every key falls back
# to the default given in its comment (ProfilingConfig / PerformanceLoggingConfig in
# src/weathergen/utils/profiling.py), so an overwrite config only sets what it changes.
#
# As written, the run ends once the profiled stretch is done, and its chrome trace, memory
# timeline and memory snapshot are written to
# <shared_working_dir>/logs/<run_id>/profiling_traces.

# tracing a bounded stretch of training; expensive
profiling:

# collect traces; without this, nothing else in this section has any effect (default False)
enabled: True

# end the run once the profiled stretch is done, without validating or checkpointing, so
# that the traces cover the training step and nothing else. Leave at the default False to
# trace the beginning of an otherwise normal run.
stop_after_profiling: True

# the profiled stretch, in training steps: (wait_iteration + warmup_iteration +
# active_iteration) * repeat steps are run, the collectors below skip the wait and warmup
# ones, and a stop_after_profiling run ends after them. Each defaults to 1.
wait_iteration: 1
warmup_iteration: 1
active_iteration: 3
repeat: 1

# PyTorch profiler: chrome trace and memory timeline per cycle, root rank only
pytorch_profiler:
enabled: True # default False

# CUDA memory history snapshot, root rank only, recorded from the first active step on.
# View at https://pytorch.org/memory_viz . Independent of pytorch_profiler.
memory_snapshot:
enabled: True # default False

# annotate batches and model blocks with nvtx ranges, for nsys
# nvtx_annotate: False

# how the run itself performs: cheap metrics over the whole run, logged next to the training
# metrics on all ranks. Independent of profiling — a run with only these enabled trains
# exactly as it would without them, so they can be left on for a full-length run.
performance_logging:

# throughput metrics (performance.throughput.*)
throughput:
enabled: True # default False
# steps to skip before reporting, so that startup does not skew the numbers
# warmup_steps: 2
32 changes: 0 additions & 32 deletions config/config_performance_default.yml

This file was deleted.

52 changes: 0 additions & 52 deletions config/config_performance_jepa.yml

This file was deleted.

5 changes: 5 additions & 0 deletions packages/common/src/weathergen/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,11 @@ def get_path_run(config: Config) -> Path:
return _get_shared_wg_path() / "results" / get_run_id_from_config(config)


def get_path_profiling_traces(config: Config) -> Path:
"""Get the path for storing profiling traces."""
return _get_shared_wg_path() / "logs" / get_run_id_from_config(config) / "profiling_traces"


def get_path_model(config: Config | None = None, run_id: str | None = None) -> Path:
"""Get the current runs model_path for storing model checkpoints."""
if config or run_id:
Expand Down
16 changes: 14 additions & 2 deletions src/weathergen/run_train.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,24 @@

import weathergen.common.config as config
import weathergen.utils.cli as cli
from weathergen.common.config import Config
from weathergen.common.logger import init_loggers
from weathergen.train.profiling_trainer import ProfilingTrainer
from weathergen.train.trainer import Trainer
from weathergen.utils.profiling import PerformanceLoggingConfig, ProfilingConfig

logger = logging.getLogger(__name__)


def get_trainer(cf: Config) -> Trainer:
"""Select the trainer: the ProfilingTrainer if the run is measured, a plain one otherwise."""
if ProfilingConfig.from_config(cf).enabled or PerformanceLoggingConfig.from_config(cf).enabled:
logger.info("Profiling or performance logging enabled: running with ProfilingTrainer.")
return ProfilingTrainer(cf.train_logging)

return Trainer(cf.train_logging)


def train() -> None:
"""Entry point for calling the training code from the command line."""
main([cli.Stage.train] + sys.argv[1:])
Expand Down Expand Up @@ -144,7 +156,7 @@ def run_continue(args):
# track history of run to ensure traceability of results
cf.general.run_history += [(args.from_run_id, cf.general.istep)]

trainer = Trainer(cf.train_logging)
trainer = get_trainer(cf)

try:
trainer.run(cf, devices, args.from_run_id, args.mini_epoch)
Expand Down Expand Up @@ -185,7 +197,7 @@ def run_train(args):
if cf.with_flash_attention:
assert cf.with_mixed_precision

trainer = Trainer(cf.train_logging)
trainer = get_trainer(cf)

try:
trainer.run(cf, devices)
Expand Down
197 changes: 197 additions & 0 deletions src/weathergen/train/profiling_trainer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# (C) Copyright 2025 WeatherGenerator contributors.
#
# This software is licensed under the terms of the Apache Licence Version 2.0
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
#
# In applying this licence, ECMWF does not waive the privileges and immunities
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.

"""
Trainer variant that measures a training run instead of (only) performing it.

Selected by the `profiling` and `performance_logging` config sections, see
`weathergen.run_train.get_trainer`.
"""

import contextlib
import logging
from collections.abc import Iterator
from itertools import islice

import torch

from weathergen.common.config import Config
from weathergen.datasets.batch import ModelBatch
from weathergen.train.trainer import Trainer
from weathergen.train.utils import TRAIN
from weathergen.utils.distributed import is_root
from weathergen.utils.performance import ThroughputTracker, nvtx_range
from weathergen.utils.profiling import (
BatchTracker,
PerformanceLoggingConfig,
ProfilingConfig,
memory_snapshot_session,
pytorch_profiler_session,
wrap_module_forward_with_profiling,
)

logger = logging.getLogger(__name__)


class ProfilingTrainer(Trainer):
"""
Trainer that measures the training loop, configured by `profiling` and
`performance_logging`.

The training step itself is inherited unchanged from `Trainer`: only the iteration
seams (`mini_epochs`, `train_batches`) are overridden, so the measured code path and
the code path of a normal run cannot drift apart. Everything that measures a step hangs
off `train_batches`, which regains control once the step for the batch it yielded is
done — `Trainer` therefore knows about no measurement tool at all.

`profiling` traces the profiled stretch (`schedule.num_steps` training steps) on the
root rank, while the other ranks run the same steps untraced so that collectives stay
matched. The PyTorch profiler steps through the schedule; the memory snapshot records
from the first active step onwards. With `stop_after_profiling` (the default) the run
ends once the stretch is done, without validating or checkpointing, so that the traces
cover the training step and nothing else.

`performance_logging` builds the `BatchTracker`s (see `get_trackers`) that measure every
step of the whole run on every rank. It is cheap: a run with only this enabled trains
exactly as a plain `Trainer` run would, and just logs more.
"""

def __init__(self, train_logging: Config):
super().__init__(train_logging)

self.profiling_cfg = ProfilingConfig()
self.performance_cfg = PerformanceLoggingConfig()
self.trackers: list[BatchTracker] = []
self.profiling_done: bool = False

def init(self, cf: Config, devices: list) -> None:
super().init(cf, devices)

self.profiling_cfg = ProfilingConfig.from_config(self.cf)
self.performance_cfg = PerformanceLoggingConfig.from_config(self.cf)
logger.info(f"Profiling run: {self.profiling_cfg}, {self.performance_cfg}")

self.trackers = self.get_trackers()
if self.profiling_cfg.nvtx_annotate:
self.training_loop_annotation_context = nvtx_range

def get_trackers(self) -> list[BatchTracker]:
"""
Build the per-step measurement tools the `performance_logging` config asks for.

This is where a new tracking tool is added: implement `BatchTracker` and append it
here. The trainer only ever calls `step` on them, once per training step and on
every rank, so a tracker is free to sync across ranks.
"""
trackers: list[BatchTracker] = []

if self.performance_cfg.throughput:
trackers.append(
ThroughputTracker(
device=torch.device(self.devices[0]),
warmup_steps=self.performance_cfg.throughput_warmup_steps,
batch_size_per_gpu=self.batch_size_per_gpu,
)
)

return trackers

@property
def stops_after_profiling(self) -> bool:
"""Whether the run exists only to be profiled, and ends once it is."""
return self.profiling_cfg.enabled and self.profiling_cfg.stop_after_profiling

def mini_epochs(self, mini_epoch_base: int) -> Iterator[int]:
"""Run a single mini_epoch when the run only exists to be profiled."""
if not self.stops_after_profiling:
yield from super().mini_epochs(mini_epoch_base)
return

yield mini_epoch_base

def train_batches(self, dataset_iter: Iterator) -> Iterator[tuple[int, ModelBatch]]:
"""Measure every training step, and trace the profiled stretch of them."""
yield from self._tracked(self._profiled(dataset_iter))

def _tracked(
self, batches: Iterator[tuple[int, ModelBatch]]
) -> Iterator[tuple[int, ModelBatch]]:
"""
Step the trackers once per training step, on every rank.

Control returns here after `train()` has finished the step for the batch that was
yielded, which is what lets the measurement live outside the training step.
"""
if not self.trackers:
yield from batches
return

for bidx, batch in batches:
istep = self.cf.general.istep # train() increments it as part of the step
yield bidx, batch
for tracker in self.trackers:
tracker.step(
batch,
istep,
log_fn=lambda m, istep=istep: self.train_logger.log_metrics(
TRAIN, m, step=istep
),
)

def _profiled(self, dataset_iter: Iterator) -> Iterator[tuple[int, ModelBatch]]:
"""Trace the profiled stretch, then continue (or stop) as configured."""
if self.profiling_done or not self.profiling_cfg.enabled:
# the stretch is profiled once per run, not once per mini_epoch
yield from super().train_batches(dataset_iter)
return

self.profiling_done = True
schedule = self.profiling_cfg.schedule

if is_root() and self.profiling_cfg.pytorch_profiler:
# the model only exists once run() has built it, hence not in init()
wrap_module_forward_with_profiling(self.model, prefix="model")

with contextlib.ExitStack() as stack:
prof = None
if self.profiling_cfg.pytorch_profiler:
prof = stack.enter_context(pytorch_profiler_session(self.cf, schedule))

for bidx, batch in enumerate(islice(dataset_iter, schedule.num_steps)):
if bidx == schedule.steps_before_active and self.profiling_cfg.memory_snapshot:
# skip the wait and warmup steps, as the profiler does
stack.enter_context(memory_snapshot_session(self.cf))

yield bidx, batch
if prof is not None:
prof.step()

# keep the other ranks in step with the root rank writing its traces
if torch.distributed.is_initialized():
torch.distributed.barrier()

if self.stops_after_profiling:
logger.info(f"Profiled {schedule.num_steps} training steps, ending the run.")
return

yield from enumerate(dataset_iter, start=schedule.num_steps)

def validate(self, mini_epoch, mode_cfg, batch_size) -> None:
"""Skipped while the run only exists to be profiled."""
if self.stops_after_profiling:
return

super().validate(mini_epoch, mode_cfg, batch_size)

def save_model(self, mini_epoch: int, name=None) -> None:
"""Skipped while the run trains too few steps for its checkpoints to be useful."""
if self.stops_after_profiling:
return

super().save_model(mini_epoch, name)
Loading
Loading