Skip to content

Commit e92bd91

Browse files
alexmalyshevmeta-codesync[bot]
authored andcommitted
Hoist torchbench run-contexts out of the timed loop
Summary: The `torchbench` benchmark called `model.invoke()` once per timed iteration. `BenchmarkModel.invoke()` wraps each call in `with nested(*self.run_contexts)`, which rebuilds an `ExitStack` plus fresh context-manager and closure objects (`enable_profiling_executor`, the `pick_grad` lambda) on every call. `perf` profiling of `pyhpc_equation_of_state` under the CinderX JIT showed this per-iteration churn as `contextlib`/`nested`/`ExitStack` frames, and because the short-lived functions are created and destroyed each iteration it also kept the JIT busy with `scheduleJitCompile`/`funcDestroyed` cleanup in steady state, overhead that only penalizes the JIT arm of the comparison. This change adds `resolve_step()`, which returns the model's `eval`/`train` step together with its `run_contexts`. `run()` now enters those contexts once in a single `ExitStack` around warmup and the timed loop, and calls the step directly per iteration. Staged-train keeps its own per-stage contexts, so it falls back to `invoke()`. Entering grad mode once around many iterations also better reflects a real serving loop. Reviewed By: mpage Differential Revision: D109523407 fbshipit-source-id: 2f6b61a9de2473793a539e717de320cc43c61b8d
1 parent 0e54ec1 commit e92bd91

1 file changed

Lines changed: 58 additions & 15 deletions

File tree

cinderx/benchmarks/torchbench.py

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111

1212
from __future__ import annotations
1313

14+
import contextlib
1415
import os
1516
import statistics
1617
import subprocess
1718
import sys
1819
import time
19-
from typing import Any
20+
from typing import Any, Callable
2021

2122
import cinderx.jit
2223
import click
@@ -25,6 +26,7 @@
2526
try:
2627
import torch
2728
from torchbenchmark import load_model_by_name
29+
from torchbenchmark.util.extra_args import is_staged_train_test
2830
except ImportError:
2931
print(
3032
"Error: torch / torchbenchmark not found. Install torch with:\n"
@@ -75,27 +77,68 @@ def build_model(name: str, test: str, batch_size: int | None) -> Any:
7577
)
7678

7779

78-
def run_iterations(model: Any, iterations: int) -> float:
80+
def resolve_step(model: Any) -> tuple[Callable[[], Any], list[Any]]:
81+
"""Return ``(step, contexts)`` where ``step()`` runs one model iteration.
82+
83+
The model's per-call ``run_contexts`` (grad mode + JIT profiling executor) are
84+
hoisted out of the timed loop and the model's ``eval``/``train`` step is called
85+
directly. ``BenchmarkModel.invoke`` otherwise rebuilds an ``ExitStack`` plus
86+
fresh context-manager and closure objects on every call; that churn adds
87+
eager-Python overhead and keeps CinderX recompiling/cleaning up short-lived
88+
functions in steady state, neither of which reflects a real serving loop that
89+
wraps grad mode once around many requests.
90+
91+
Staged train manages its own per-stage contexts, so it falls back to ``invoke()``
92+
with no externally-held contexts.
93+
"""
94+
is_train = model.test == "train"
95+
if (
96+
is_train
97+
and is_staged_train_test(model)
98+
and getattr(model, "train", None) is None
99+
):
100+
return model.invoke, []
101+
# Staged train is the only path that loops over ``num_batch``; for every other
102+
# path ``invoke()`` asserts a single batch per call. Calling ``eval``/``train``
103+
# directly once per timed iteration would silently undercount ``num_batch > 1``
104+
# configs, so keep invoke()'s fail-fast guard.
105+
assert model.num_batch == 1, (
106+
"Only staged_train_test supports multiple-batch testing at this time."
107+
)
108+
step = model.train if is_train else model.eval
109+
return step, list(getattr(model, "run_contexts", []))
110+
111+
112+
def run_iterations(step: Callable[[], Any], iterations: int) -> float:
79113
"""Run one timed sample and return elapsed seconds."""
80114
start = time.perf_counter()
81115
for _ in range(iterations):
82-
model.invoke()
116+
step()
83117
return time.perf_counter() - start
84118

85119

86120
def run(model: Any, iterations: int, warmup: int, repeat: int) -> list[float]:
87-
"""Warm up once, then collect repeated per-iteration timing samples."""
88-
print(f"Warmup ({warmup} iterations)...", file=sys.stderr)
89-
for _ in range(warmup):
90-
model.invoke()
91-
92-
print(f"Timed runs ({repeat} x {iterations} iterations)...", file=sys.stderr)
93-
samples_ms: list[float] = []
94-
for i in range(repeat):
95-
elapsed = run_iterations(model, iterations)
96-
mean_ms = elapsed / iterations * 1000
97-
samples_ms.append(mean_ms)
98-
print(f" Run {i + 1}/{repeat}: {mean_ms:.2f} ms/iter", file=sys.stderr)
121+
"""Warm up, then collect repeated per-iteration timing samples.
122+
123+
The model's run-contexts are entered once around the whole measurement rather
124+
than per iteration (see ``resolve_step``).
125+
"""
126+
step, contexts = resolve_step(model)
127+
with contextlib.ExitStack() as stack:
128+
for make_context in contexts:
129+
stack.enter_context(make_context())
130+
131+
print(f"Warmup ({warmup} iterations)...", file=sys.stderr)
132+
for _ in range(warmup):
133+
step()
134+
135+
print(f"Timed runs ({repeat} x {iterations} iterations)...", file=sys.stderr)
136+
samples_ms: list[float] = []
137+
for i in range(repeat):
138+
elapsed = run_iterations(step, iterations)
139+
mean_ms = elapsed / iterations * 1000
140+
samples_ms.append(mean_ms)
141+
print(f" Run {i + 1}/{repeat}: {mean_ms:.2f} ms/iter", file=sys.stderr)
99142

100143
return samples_ms
101144

0 commit comments

Comments
 (0)