Skip to content

Commit 0c6dd5e

Browse files
Split pin_memory benches into per-experiment folders. (#1015)
Keep ZeRO-3 offload as model_tensor_offload (with --zero-stage) and add activation, H2D/D2H, and optional harnesses so one DSE tree covers the pinned-offload blog matrix. Root zero3_offload_bench.py still delegates to the ZeRO-3 path. Signed-off-by: tunji-ruwase_snow <tunji.ruwase@snowflake.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 43578c4 commit 0c6dd5e

15 files changed

Lines changed: 860 additions & 298 deletions

File tree

‎benchmarks/pin_memory/README.md‎

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,36 @@
1-
# ZeRO-3 CPU-Offload Pinned-Memory Benchmark
1+
# Pinned-memory experiments
22

3-
This directory contains an end-to-end benchmark for ZeRO-3 CPU offload that
4-
measures training step time with pinned vs unpinned host memory, plus an
5-
opt-in ablation of registered vs unregistered pinned memory.
3+
Harnesses for pin vs pageable host memory on CPU offload. Each subdirectory is
4+
one experiment. Shared subprocess/JSON helpers live in `common.py`.
65

7-
## Files in this Directory
6+
Native backends `mlock` host memory: raise `RLIMIT_MEMLOCK` (`ulimit -l`) or
7+
run as root for multi-GB models.
88

9-
- **zero3_offload_bench.py**: Benchmarking script; the model can be a real
10-
architecture fetched from the HuggingFace hub (random weights) or a
11-
synthetic MLP stack that needs no network access.
9+
## Layout
1210

13-
## What it Measures
11+
| Folder | Blog experiment | Default command |
12+
|--------|-----------------|-----------------|
13+
| [`model_tensor_offload/`](model_tensor_offload/) | ZeRO CPU param/optimizer offload (stage 3 default; `--zero-stage 1\|2` optional) | `python model_tensor_offload/bench.py --hidden 2048 --layers 12 --batch 4 --seq 128` |
14+
| [`activation_offload/`](activation_offload/) | Checkpoint hidden-state offload; `use_pin_memory` on/off, **async on** | `python activation_offload/bench.py --hidden 1024 --layers 8 --batch 1 --seq 2048` |
15+
| [`h2d_d2h/`](h2d_d2h/) | Supporting H2D/D2H GB/s (pageable, torch, native-unregistered, native-registered) | `python h2d_d2h/bench.py` |
16+
| [`grad_offload/`](grad_offload/) | Optional #8207-style grad offload (wraps model-tensor ZeRO-3) | `python grad_offload/bench.py --hidden 2048 --layers 12` |
17+
| [`cpu_pin/`](cpu_pin/) | Optional CPU-only native vs Torch pin | `python cpu_pin/bench.py` |
18+
| [`deepcompile_activation/`](deepcompile_activation/) | Optional `compile.offload_activation_pin_memory` | `python deepcompile_activation/bench.py` |
1419

15-
By default the script runs ZeRO-3 with `offload_optimizer` and `offload_param`
16-
(both CPU) in two arms and reports the step-time comparison:
20+
`zero3_offload_bench.py` at this directory root still runs **model-tensor ZeRO-3** (same flags as before).
1721

18-
| Arm | offload `pin_memory` | `DS_PIN_MEMORY_REGISTER_DEVICE` |
19-
|-----|----------------------|---------------------------------|
20-
| `unpinned` | `False` | (n/a) |
21-
| `pinned` | `True` (`DS_PIN_MEMORY_BACKEND=native`) | `1` |
22+
## Model-tensor arms
2223

23-
Works on any accelerator with native pin + `register_host_memory` support
24-
(CUDA and XPU are tested). Each arm runs in its own subprocess with a fresh
25-
rendezvous port so device state never leaks between arms.
24+
| Arm | `offload_*.pin_memory` | Backend |
25+
|-----|------------------------|---------|
26+
| unpinned | `false` | n/a |
27+
| pinned | `true` | `DS_PIN_MEMORY_BACKEND=native`, `DS_PIN_MEMORY_REGISTER_DEVICE=1` |
2628

27-
Power users can additionally ablate device registration of pinned buffers:
29+
`--ablate-register` adds `pinned-unregistered`. CUDA-oriented; skip on XPU if `register_host_memory` is missing (`h2d_d2h/bench.py --skip-native-register`).
2830

2931
```bash
30-
python zero3_offload_bench.py --ablate-register ...
32+
python model_tensor_offload/bench.py --model Qwen/Qwen2.5-7B --batch 4 --seq 512
33+
python model_tensor_offload/bench.py --zero-stage 2 --hidden 2048 --layers 12 --batch 4 --seq 128
3134
```
3235

33-
which adds a `pinned-unregistered` arm (`DS_PIN_MEMORY_REGISTER_DEVICE=0`).
34-
35-
## Usage
36-
37-
```bash
38-
# real model architecture (config fetched from the HF hub, random weights)
39-
python zero3_offload_bench.py --model Qwen/Qwen2.5-7B --batch 4 --seq 512
40-
41-
# synthetic MLP stack, no network needed
42-
python zero3_offload_bench.py --hidden 2048 --layers 12 --batch 4 --seq 128
43-
```
44-
45-
Results are printed as a table (avg/min step time, GPU peak memory) and as a
46-
JSON line (`DRIVERRESULT=...`) with per-arm details and the pinning speedup.
47-
48-
> **Note**: the native backend mlocks host memory; raise `RLIMIT_MEMLOCK`
49-
> (`ulimit -l`) or run as root for multi-GB models.
36+
Each arm is a subprocess with a fresh rendezvous port. Results: table plus `DRIVERRESULT=` JSON.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Activation / checkpoint hidden-state offload
2+
3+
Compares `use_pin_memory` True vs False on `CheckpointHiddenStatesOffload`
4+
with **async / side streams held on**. This is not the async-vs-blocking
5+
table from DeepSpeed #8282.
6+
7+
See the [parent README](../README.md).
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# DeepSpeed Team
3+
"""
4+
Activation / checkpoint hidden-state CPU offload: pin vs pageable with async on.
5+
6+
Holds use_streams=True. Compares use_pin_memory True vs False. Do not treat
7+
this as DeepSpeed #8282's async-vs-blocking table.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import argparse
13+
import os
14+
import sys
15+
import time
16+
17+
_PIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18+
if _PIN_ROOT not in sys.path:
19+
sys.path.insert(0, _PIN_ROOT)
20+
21+
from common import dist_env, patch_cpp_extension_drop_cxx17, print_arm_result, print_driver_result, run_arm_subprocess
22+
23+
24+
def parse_args():
25+
parser = argparse.ArgumentParser(description=__doc__)
26+
parser.add_argument("--hidden", type=int, default=1024)
27+
parser.add_argument("--layers", type=int, default=8)
28+
parser.add_argument("--batch", type=int, default=1)
29+
parser.add_argument("--seq", type=int, default=2048)
30+
parser.add_argument("--steps", type=int, default=4)
31+
parser.add_argument("--warmup", type=int, default=2)
32+
parser.add_argument("--pin", type=int, default=None, help="internal: use_pin_memory 0/1")
33+
return parser.parse_args()
34+
35+
36+
def run_arm(args):
37+
dist_env()
38+
patch_cpp_extension_drop_cxx17()
39+
40+
import torch
41+
from torch.utils.checkpoint import checkpoint
42+
43+
from deepspeed.accelerator import get_accelerator
44+
from deepspeed.runtime.activation_checkpointing.offload_activations import CheckpointHiddenStatesOffload
45+
46+
accelerator = get_accelerator()
47+
if not accelerator.is_available():
48+
raise RuntimeError(f"No {accelerator.device_name()} device is available")
49+
accelerator.set_device(0)
50+
device = accelerator.current_device_name()
51+
52+
class Block(torch.nn.Module):
53+
54+
def __init__(self, hidden):
55+
super().__init__()
56+
self.fc1 = torch.nn.Linear(hidden, 4 * hidden)
57+
self.fc2 = torch.nn.Linear(4 * hidden, hidden)
58+
59+
def forward(self, hidden_states):
60+
return self.fc2(torch.nn.functional.gelu(self.fc1(hidden_states)))
61+
62+
class Net(torch.nn.Module):
63+
64+
def __init__(self, hidden, layers):
65+
super().__init__()
66+
self.blocks = torch.nn.ModuleList([Block(hidden) for _ in range(layers)])
67+
68+
def forward(self, hidden_states, offload):
69+
x = hidden_states
70+
for block in self.blocks:
71+
offload.mark(x)
72+
x = x + checkpoint(block, x, use_reentrant=False)
73+
return x.sum()
74+
75+
model = Net(args.hidden, args.layers).to(device)
76+
opt = torch.optim.AdamW(model.parameters(), lr=1e-4)
77+
x = torch.randn(args.batch, args.seq, args.hidden, device=device, requires_grad=True)
78+
79+
# Async side stream stays on; pin vs pageable is the only axis.
80+
offload = CheckpointHiddenStatesOffload(use_pin_memory=bool(args.pin),
81+
use_streams=True,
82+
min_offload_bytes=0,
83+
keep_last_count=1)
84+
85+
step_times = []
86+
for step in range(args.warmup + args.steps):
87+
accelerator.synchronize()
88+
t0 = time.perf_counter()
89+
opt.zero_grad(set_to_none=True)
90+
with offload:
91+
loss = model(x, offload)
92+
loss.backward()
93+
opt.step()
94+
accelerator.synchronize()
95+
if step >= args.warmup:
96+
step_times.append(time.perf_counter() - t0)
97+
offload.reset()
98+
99+
def _peak():
100+
try:
101+
return round(torch.get_device_module(device).max_memory_allocated() / 1e9, 2)
102+
except Exception:
103+
return None
104+
105+
print_arm_result({
106+
"experiment": "activation_offload",
107+
"use_pin_memory": bool(args.pin),
108+
"use_streams": True,
109+
"device": accelerator.device_name(),
110+
"hidden": args.hidden,
111+
"layers": args.layers,
112+
"batch": args.batch,
113+
"seq": args.seq,
114+
"steps": len(step_times),
115+
"step_avg_s": sum(step_times) / len(step_times),
116+
"step_min_s": min(step_times),
117+
"gpu_peak_gb": _peak(),
118+
})
119+
120+
121+
def run_driver(args):
122+
script = os.path.abspath(__file__)
123+
base = [
124+
"--hidden",
125+
str(args.hidden),
126+
"--layers",
127+
str(args.layers),
128+
"--batch",
129+
str(args.batch),
130+
"--seq",
131+
str(args.seq),
132+
"--steps",
133+
str(args.steps),
134+
"--warmup",
135+
str(args.warmup),
136+
]
137+
results = {}
138+
for name, pin in (("pageable", 0), ("pinned", 1)):
139+
results[name] = run_arm_subprocess(script, base + ["--pin", str(pin)])
140+
141+
pageable = results["pageable"]
142+
pinned = results["pinned"]
143+
print("\n================ Activation offload (async on) ================")
144+
print(f"device: {pinned['device']} hidden: {pinned['hidden']} layers: {pinned['layers']} "
145+
f"batch: {pinned['batch']} seq: {pinned['seq']}")
146+
print(f"{'arm':<22}{'avg step (s)':>14}{'min step (s)':>14}{'GPU peak (GB)':>16}")
147+
for name in ("pageable", "pinned"):
148+
row = results[name]
149+
print(f"{name:<22}{row['step_avg_s']:>14.3f}{row['step_min_s']:>14.3f}"
150+
f"{(row['gpu_peak_gb'] or 0):>16.2f}")
151+
speedup = pageable["step_avg_s"] / pinned["step_avg_s"]
152+
print()
153+
print(f"pin vs pageable step-time ratio: {speedup:.2f}x (async held on)")
154+
print_driver_result({"pageable": pageable, "pinned": pinned, "speedup": speedup})
155+
156+
157+
if __name__ == "__main__":
158+
parsed = parse_args()
159+
if parsed.pin is None:
160+
run_driver(parsed)
161+
else:
162+
run_arm(parsed)

‎benchmarks/pin_memory/common.py‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# DeepSpeed Team
3+
"""Shared helpers for pin_memory experiment drivers (subprocess arms, JSON lines)."""
4+
5+
from __future__ import annotations
6+
7+
import json
8+
import os
9+
import socket
10+
import subprocess
11+
import sys
12+
13+
14+
def free_port():
15+
# A stale listener from an interrupted rank makes the next init hang in a
16+
# collective, so always rendezvous on a fresh ephemeral port.
17+
sock = socket.socket()
18+
sock.bind(("127.0.0.1", 0))
19+
port = sock.getsockname()[1]
20+
sock.close()
21+
return port
22+
23+
24+
def dist_env():
25+
os.environ.update(
26+
MASTER_ADDR="127.0.0.1",
27+
MASTER_PORT=str(free_port()),
28+
RANK="0",
29+
WORLD_SIZE="1",
30+
LOCAL_RANK="0",
31+
)
32+
33+
34+
def patch_cpp_extension_drop_cxx17():
35+
# torch-nightly requires C++20; SYCL toolchain flags may carry -std=c++17,
36+
# which (appearing last) downgrades the dialect and breaks torch headers.
37+
import torch.utils.cpp_extension as cpp_ext
38+
39+
orig_load = cpp_ext.load
40+
41+
def load_without_cxx17(*args, **kwargs):
42+
for key in ("extra_cflags", "extra_cxxflags"):
43+
if kwargs.get(key):
44+
kwargs[key] = [flag for flag in kwargs[key] if flag != "-std=c++17"]
45+
return orig_load(*args, **kwargs)
46+
47+
cpp_ext.load = load_without_cxx17
48+
49+
50+
def print_arm_result(result):
51+
print("ARMRESULT=" + json.dumps(result), flush=True)
52+
53+
54+
def print_driver_result(summary):
55+
print("DRIVERRESULT=" + json.dumps(summary), flush=True)
56+
57+
58+
def parse_arm_result(stdout):
59+
for line in stdout.splitlines():
60+
if line.startswith("ARMRESULT="):
61+
return json.loads(line[len("ARMRESULT="):])
62+
return None
63+
64+
65+
def run_arm_subprocess(script_path, extra_args, env=None):
66+
command = [sys.executable, os.path.abspath(script_path), *extra_args]
67+
print(f"[driver] {' '.join(command)}", flush=True)
68+
proc = subprocess.run(command, env=env or os.environ.copy(), capture_output=True, text=True)
69+
arm = parse_arm_result(proc.stdout)
70+
if arm is None:
71+
print(proc.stdout[-2000:])
72+
print(proc.stderr[-2000:], file=sys.stderr)
73+
raise RuntimeError(f"arm produced no ARMRESULT (rc={proc.returncode})")
74+
return arm
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# CPU-only pin
2+
3+
Native `mlock` vs Torch pin on a CPU host. Torch typically cannot pin without
4+
an accelerator-capable backend.
5+
6+
See the [parent README](../README.md).
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# DeepSpeed Team
3+
"""CPU-only: native pin vs Torch (Torch cannot pin without an accelerator)."""
4+
5+
from __future__ import annotations
6+
7+
import argparse
8+
import os
9+
import sys
10+
11+
_PIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12+
if _PIN_ROOT not in sys.path:
13+
sys.path.insert(0, _PIN_ROOT)
14+
15+
from common import print_arm_result, print_driver_result
16+
17+
18+
def parse_args():
19+
parser = argparse.ArgumentParser(description=__doc__)
20+
parser.add_argument("--numel", type=int, default=1024 * 1024)
21+
return parser.parse_args()
22+
23+
24+
def main():
25+
args = parse_args()
26+
import torch
27+
from deepspeed.accelerator import get_accelerator
28+
29+
accel = get_accelerator()
30+
os.environ["DS_PIN_MEMORY_BACKEND"] = "native"
31+
host = torch.empty(args.numel, dtype=torch.float32)
32+
native = accel.pin_memory(host.clone(), make_copy=False)
33+
native_ok = bool(accel.is_pinned(native))
34+
accel.unpin_memory(native)
35+
36+
os.environ["DS_PIN_MEMORY_BACKEND"] = "torch"
37+
torch_ok = None
38+
torch_error = None
39+
try:
40+
pinned = accel.pin_memory(torch.empty_like(host), make_copy=False)
41+
torch_ok = bool(accel.is_pinned(pinned))
42+
except Exception as exc:
43+
torch_error = type(exc).__name__ + ": " + str(exc)
44+
45+
result = {
46+
"experiment": "cpu_pin",
47+
"accelerator": accel.device_name(),
48+
"native_is_pinned": native_ok,
49+
"torch_is_pinned": torch_ok,
50+
"torch_error": torch_error,
51+
}
52+
print_arm_result(result)
53+
print_driver_result(result)
54+
if not native_ok:
55+
raise SystemExit("native pin failed on this host")
56+
57+
58+
if __name__ == "__main__":
59+
main()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# DeepCompile activation pin
2+
3+
Optional `compile.offload_activation_pin_memory` on/off. Requires DeepCompile.
4+
5+
See the [parent README](../README.md).

0 commit comments

Comments
 (0)