Skip to content

Commit 16b626c

Browse files
Collect peak memory directly in integration tests
Add direct peak-memory collection for integration runs by having MetricsProcessor write a JSON summary at the end of training. The test runner now passes TORCHTITAN_PEAK_MEMORY_JSON into each launched training job, forces metrics logging every step, and reads the emitted summary file back for reporting. MetricsProcessor tracks the maximum reserved and active CUDA memory it observes across log and validation calls and writes a single summary on close from the metrics rank. This keeps the measurement path local to the training run, avoids depending on TensorBoard event parsing for memory collection, and preserves the integration-test UX via --collect_peak_memory. The graph-trainer integration entrypoint and 8-GPU workflow are wired to use the flag. [ghstack-poisoned]
1 parent 87b7abc commit 16b626c

4 files changed

Lines changed: 86 additions & 1 deletion

File tree

.github/workflows/integration_test_8gpu_graph_trainer.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ jobs:
6161
sudo mkdir -p "$RUNNER_TEMP/artifacts-to-be-uploaded"
6262
sudo chown -R $(id -u):$(id -g) "$RUNNER_TEMP/artifacts-to-be-uploaded"
6363
64-
python -m torchtitan.experiments.graph_trainer.tests.integration_tests --test_suite graph_trainer_default --gpu_arch_type cuda $RUNNER_TEMP/artifacts-to-be-uploaded --ngpu 8
64+
python -m torchtitan.experiments.graph_trainer.tests.integration_tests --test_suite graph_trainer_default --gpu_arch_type cuda $RUNNER_TEMP/artifacts-to-be-uploaded --ngpu 8 --collect_peak_memory
6565
6666
6767
# Run the numerics unit tests (dense models only; MoE tests run in the H100 workflow)

tests/integration_tests/run_tests.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
# LICENSE file in the root directory of this source tree.
66

77
import argparse
8+
import json
89
import os
910
import subprocess
1011
import time
12+
from pathlib import Path
1113

1214
from torchtitan.tools.logging import logger
1315

@@ -40,6 +42,8 @@ def run_single_test(
4042
output_dir: str,
4143
module: str | None = None,
4244
config: str | None = None,
45+
*,
46+
collect_peak_memory: bool = False,
4347
):
4448
# run_test supports sequence of tests.
4549
test_name = test_flavor.test_name
@@ -48,6 +52,7 @@ def run_single_test(
4852
all_ranks = ",".join(map(str, range(test_flavor.ngpu)))
4953

5054
for idx, override_arg in enumerate(test_flavor.override_args):
55+
peak_memory_path = Path(output_dir) / test_name / f"peak_memory_{idx}.json"
5156
cmd = ""
5257
if module is not None:
5358
cmd += f"MODULE={module} "
@@ -59,6 +64,9 @@ def run_single_test(
5964
cmd = f'TORCH_TRACE="{output_dir}/{test_name}/compile_trace" ' + cmd
6065

6166
cmd += " " + dump_folder_arg
67+
if collect_peak_memory:
68+
cmd = f'TORCHTITAN_PEAK_MEMORY_JSON="{peak_memory_path}" ' + cmd
69+
cmd += " --metrics.log_freq=1"
6270
if override_arg:
6371
cmd += " " + " ".join(override_arg)
6472
logger.info(
@@ -89,6 +97,19 @@ def run_single_test(
8997
f"Command: {cmd}\n"
9098
f"stderr: {result.stderr}\n"
9199
)
100+
if collect_peak_memory:
101+
if not peak_memory_path.exists():
102+
raise FileNotFoundError(
103+
f"Peak memory summary not found: {peak_memory_path}"
104+
)
105+
peak_memory = json.loads(peak_memory_path.read_text())
106+
logger.info(
107+
f"Peak memory for {test_name}[{idx}]: "
108+
f"reserved={peak_memory['max_reserved_gib']:.3f} GiB "
109+
f"at step {peak_memory['max_reserved_step']}, "
110+
f"active={peak_memory['max_active_gib']:.3f} GiB "
111+
f"at step {peak_memory['max_active_step']}"
112+
)
92113

93114

94115
def run_tests(
@@ -98,6 +119,7 @@ def run_tests(
98119
config=None,
99120
):
100121
"""Run all integration tests to test the core features of TorchTitan"""
122+
collect_peak_memory = getattr(args, "collect_peak_memory", False)
101123
exclude_set = set()
102124
if hasattr(args, "exclude") and args.exclude:
103125
exclude_set = {name.strip() for name in args.exclude.split(",")}
@@ -132,6 +154,7 @@ def run_tests(
132154
args.output_dir,
133155
module,
134156
config,
157+
collect_peak_memory=collect_peak_memory,
135158
)
136159
except Exception as e:
137160
logger.error(str(e))
@@ -202,6 +225,12 @@ def main():
202225
default=None,
203226
help="Comma-separated list of test names to skip",
204227
)
228+
parser.add_argument(
229+
"--collect_peak_memory",
230+
default=False,
231+
action="store_true",
232+
help="Collect peak reserved/active CUDA memory directly from the run.",
233+
)
205234
args = parser.parse_args()
206235

207236
if not os.path.exists(args.output_dir):

torchtitan/components/metrics.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7+
import json
78
import os
89
import time
910
from collections import namedtuple
@@ -353,6 +354,9 @@ def __init__(
353354
self.data_loading_times = []
354355
self.time_last_log = time.perf_counter()
355356
self.device_memory_monitor.reset_peak_stats()
357+
self.peak_memory_summary_path = os.getenv("TORCHTITAN_PEAK_MEMORY_JSON")
358+
self.pp_schedule = pp_schedule
359+
self.peak_memory_summary: dict[str, float | int] | None = None
356360

357361
self.has_quantization = has_quantization
358362

@@ -362,6 +366,48 @@ def __init__(
362366
self.lr_schedulers = None
363367
self.model_parts = None
364368

369+
def _should_write_peak_memory(self) -> bool:
370+
return self.peak_memory_summary_path is not None and (
371+
not torch.distributed.is_initialized()
372+
or torch.distributed.get_rank()
373+
== _get_metrics_rank(
374+
parallel_dims=self.parallel_dims, pp_schedule=self.pp_schedule
375+
)
376+
)
377+
378+
def _update_peak_memory_summary(
379+
self, device_mem_stats: DeviceMemStats, step: int
380+
) -> None:
381+
if not self._should_write_peak_memory():
382+
return
383+
384+
if self.peak_memory_summary is None:
385+
self.peak_memory_summary = {
386+
"max_reserved_gib": device_mem_stats.max_reserved_gib,
387+
"max_reserved_step": step,
388+
"max_active_gib": device_mem_stats.max_active_gib,
389+
"max_active_step": step,
390+
}
391+
return
392+
393+
if (
394+
device_mem_stats.max_reserved_gib
395+
> self.peak_memory_summary["max_reserved_gib"]
396+
):
397+
self.peak_memory_summary["max_reserved_gib"] = (
398+
device_mem_stats.max_reserved_gib
399+
)
400+
self.peak_memory_summary["max_reserved_step"] = step
401+
402+
if (
403+
device_mem_stats.max_active_gib
404+
> self.peak_memory_summary["max_active_gib"]
405+
):
406+
self.peak_memory_summary["max_active_gib"] = (
407+
device_mem_stats.max_active_gib
408+
)
409+
self.peak_memory_summary["max_active_step"] = step
410+
365411
def should_log(self, step: int) -> bool:
366412
return step == 1 or step % self.config.log_freq == 0
367413

@@ -496,6 +542,7 @@ def log(
496542
time_data_loading_pct = 100 * sum(self.data_loading_times) / time_delta
497543

498544
device_mem_stats = self.device_memory_monitor.get_peak_stats()
545+
self._update_peak_memory_summary(device_mem_stats, step)
499546

500547
metrics = {
501548
"loss_metrics/global_avg_loss": global_avg_loss,
@@ -545,6 +592,7 @@ def log_validation(
545592
time_delta = time.perf_counter() - self.time_last_log
546593

547594
device_mem_stats = self.device_memory_monitor.get_peak_stats()
595+
self._update_peak_memory_summary(device_mem_stats, step)
548596

549597
# tokens per second per device, abbreviated as tps
550598
tps = self.ntokens_since_last_log / (
@@ -579,4 +627,7 @@ def log_validation(
579627
self.device_memory_monitor.reset_peak_stats()
580628

581629
def close(self):
630+
if self._should_write_peak_memory() and self.peak_memory_summary is not None:
631+
with open(self.peak_memory_summary_path, "w") as f:
632+
json.dump(self.peak_memory_summary, f, indent=2)
582633
self.logger.close()

torchtitan/experiments/graph_trainer/tests/integration_tests.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,11 @@ def main():
547547
help="test to run, acceptable values: `test_name` in `build_test_list` (default: all)",
548548
)
549549
parser.add_argument("--ngpu", default=8, type=int)
550+
parser.add_argument(
551+
"--collect_peak_memory",
552+
action="store_true",
553+
help="Collect peak reserved/active CUDA memory directly from the run.",
554+
)
550555
args = parser.parse_args()
551556

552557
if not os.path.exists(args.output_dir):

0 commit comments

Comments
 (0)