Skip to content

Commit 131779c

Browse files
authored
Fix DeepCompile profiling memory cleanup (deepspeedai#8106)
This PR reduces DeepCompile ZeRO-3 memory pressure in profiling and compiled backward by fixing three related issues: - Let the partitioner keep using its min-cut rematerialization policy for non-parameter activations, while still forcing ZeRO-3 parameter aliases/casts to be recomputed. - Compute selective-gather persistence capacity from profiled transient headroom instead of current allocator availability. - Avoid retaining temporary profiling outputs across warmup/measured profiling iterations, and clear gathered ZeRO-3 parameters when profiling exits through an exception. - Mark incomplete memory profiles explicitly, synchronize the completion status across ranks, and skip profile-dependent prefetch/selective-gather decisions when profiling data is incomplete. Signed-off-by: Masahiro Tanaka <[email protected]>
1 parent 32d009b commit 131779c

10 files changed

Lines changed: 501 additions & 28 deletions

File tree

deepspeed/compile/backend.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
except ImportError:
2222
pass
2323

24+
import deepspeed.comm as dist
2425
from deepspeed.accelerator import get_accelerator
2526

2627
from .fx import add_free_activations
@@ -107,7 +108,7 @@ def launch_compile_passes(global_steps: int):
107108
frames_partitioned.clear()
108109

109110

110-
def set_time_and_tensor_size(graph_id, graph: Graph, mem, bwd, profiling_results):
111+
def set_time_and_tensor_size(graph_id, graph: Graph, mem, bwd, profiling_results, mem_complete=True):
111112
node_time = []
112113
tensor_sizes = []
113114

@@ -121,11 +122,22 @@ def set_time_and_tensor_size(graph_id, graph: Graph, mem, bwd, profiling_results
121122
profiling_results[graph_id].bwd_time = node_time
122123
profiling_results[graph_id].bwd_tensor_sizes = tensor_sizes
123124
profiling_results[graph_id].bwd_mem = mem
125+
profiling_results[graph_id].bwd_mem_complete = mem_complete
124126
else:
125127
profiling_results[graph_id].fwd_graph = graph
126128
profiling_results[graph_id].fwd_time = node_time
127129
profiling_results[graph_id].fwd_tensor_sizes = tensor_sizes
128130
profiling_results[graph_id].fwd_mem = mem
131+
profiling_results[graph_id].fwd_mem_complete = mem_complete
132+
133+
134+
def _sync_memory_profile_complete(profile_complete: bool) -> bool:
135+
if not dist.is_initialized():
136+
return profile_complete
137+
138+
complete = torch.tensor([1 if profile_complete else 0], device=torch.device(get_accelerator().current_device()))
139+
dist.all_reduce(complete, dist.ReduceOp.MIN)
140+
return bool(complete.item())
129141

130142

131143
def evaluate_symint_from_shape_env(sym_int_v):
@@ -213,9 +225,13 @@ def run_opt_passes(opt_passes: List[Callable],
213225

214226
mem_prof = MemoryProfilingInterpreter(gm, debug_log=debug_log)
215227
mem_prof.run(*create_inputs_fn())
216-
mem = [(name, current_alloc, delta, peak) for name, current_alloc, delta, peak in mem_prof.mem_record]
228+
profile_complete = _sync_memory_profile_complete(mem_prof.profile_complete)
229+
if profile_complete:
230+
mem = [(name, current_alloc, delta, peak) for name, current_alloc, delta, peak in mem_prof.mem_record]
231+
else:
232+
mem = []
217233

218-
set_time_and_tensor_size(graph_id, gm.graph, mem, bwd, profiling_results)
234+
set_time_and_tensor_size(graph_id, gm.graph, mem, bwd, profiling_results, profile_complete)
219235

220236
with unset_fake_temporarily():
221237
get_accelerator().synchronize()

deepspeed/compile/partitioner.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,9 @@ def need_recompute(n: Node) -> bool:
7575
any([(isinstance(a, Node) and (a in ds_param_inputs or a in recomputed_nodes)) for a in node.args]):
7676
node.meta["recompute"] = CheckpointPolicy.MUST_RECOMPUTE
7777
recomputed_nodes.add(node)
78-
else:
79-
# If checkpointing is not enabled for this graph, assume all
80-
# activations required by the backward pass should be saved.
81-
node.meta.setdefault("recompute", CheckpointPolicy.MUST_SAVE)
78+
# Leave non-parameter activations to the default min-cut policy. Forcing
79+
# every other node to MUST_SAVE prevents safe activation rematerialization
80+
# and can make long-sequence compiled backward graphs OOM.
8281

8382

8483
def get_wrapped_partitioner(

deepspeed/compile/passes/prefetch.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ def schedule_prefetch(gm: GraphModule, graph_id: int, graph_order: List[Tuple[in
3939
create_inputs_fn, mem_budget: float, param_manager: DSGraphParamManager,
4040
bwd: bool) -> GraphModule:
4141

42-
profile_graph = profiling_results[graph_id].bwd_graph if bwd else profiling_results[graph_id].fwd_graph
43-
if is_profile_incomplete(profile_graph):
42+
profile = profiling_results[graph_id]
43+
profile_graph = profile.bwd_graph if bwd else profile.fwd_graph
44+
mem_complete = profile.bwd_mem_complete if bwd else profile.fwd_mem_complete
45+
if is_profile_incomplete(profile_graph) or not mem_complete:
4446
print_rank_0(f"schedule_prefetch graph_id={graph_id} incomplete profiling data; skipping prefetch")
4547
return gm
4648

deepspeed/compile/passes/selective_gather.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,22 @@ def _compute_persistence_budget(all_graph_mem_records: List[List[Tuple[str, int,
5454
"profiled_list_count": 0,
5555
}
5656

57-
# Persistent parameters add to live allocations that remain resident past an op boundary.
57+
# Persistent parameters stay live during transient allocations inside an op.
5858
peak_resident_alloc = max(record[1] for mem_records in non_empty_records for record in mem_records)
5959
transient_peak = max(record[3] for mem_records in non_empty_records for record in mem_records)
6060

6161
return {
6262
"usable_mem": usable_mem,
6363
"peak_resident_alloc": peak_resident_alloc,
6464
"transient_peak": transient_peak,
65-
"available_mem": max(0, usable_mem - peak_resident_alloc),
65+
"available_mem": max(0, usable_mem - transient_peak),
6666
"profiled_list_count": len(non_empty_records),
6767
}
6868

6969

7070
def _profile_result_incomplete(prof) -> bool:
71-
return is_profile_incomplete(prof.fwd_graph) or is_profile_incomplete(prof.bwd_graph)
71+
return (is_profile_incomplete(prof.fwd_graph) or is_profile_incomplete(prof.bwd_graph) or not prof.fwd_mem_complete
72+
or (prof.needs_backward and not prof.bwd_mem_complete))
7273

7374

7475
def selective_gather(gm: GraphModule, graph_id: int, graph_order: List[Tuple[int, bool]], profiling_results,
@@ -171,7 +172,8 @@ def selective_gather(gm: GraphModule, graph_id: int, graph_order: List[Tuple[int
171172
current_available_mem = vals_to_bcast[1].item()
172173

173174
budget = _compute_persistence_budget(all_graph_mem_records, total_mem, MEM_MARGIN)
174-
available_mem = int(current_available_mem * (1 - MEM_MARGIN))
175+
profiled_available_mem = budget["available_mem"]
176+
available_mem = profiled_available_mem
175177

176178
ds_id_to_param = {}
177179
for g_id, g_pm in param_manager.items():
@@ -185,7 +187,7 @@ def selective_gather(gm: GraphModule, graph_id: int, graph_order: List[Tuple[int
185187
f"selective_gather target_graph_id={target_graph_id} profiled_mem_lists={budget['profiled_list_count']} "
186188
f"total_mem={total_mem} usable_mem={budget['usable_mem']} peak_resident_alloc={budget['peak_resident_alloc']} "
187189
f"transient_peak={budget['transient_peak']} current_available_mem={current_available_mem} "
188-
f"usable_available_mem={available_mem} "
190+
f"profiled_transient_available_mem={profiled_available_mem} "
189191
f"persistent_count={len(persistent_ds_ids)} persistent_bytes={persistent_bytes} "
190192
f"candidate_count={len(ds_ids)} candidate_bytes={candidate_bytes}")
191193

@@ -198,7 +200,7 @@ def selective_gather(gm: GraphModule, graph_id: int, graph_order: List[Tuple[int
198200
return gm
199201

200202
if available_mem == 0:
201-
print_rank_0("selective_gather no currently available memory for new persistent params")
203+
print_rank_0("selective_gather no profiled headroom for new persistent params")
202204
return gm
203205

204206
persistent_mem = 0

deepspeed/compile/profilers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class ProfilingResult:
1616
needs_backward: bool = False
1717
fwd_mem: List[Tuple[str, int, int, int]] = field(default_factory=list) # name, current_alloc, delta, peak
1818
bwd_mem: List[Tuple[str, int, int, int]] = field(default_factory=list)
19+
fwd_mem_complete: bool = True
20+
bwd_mem_complete: bool = True
1921
fwd_time: List[Tuple[str, int, int]] = field(default_factory=list) # name, device_time, wall_time
2022
bwd_time: List[Tuple[str, int, int]] = field(default_factory=list)
2123
fwd_tensor_sizes: List[Tuple[str, int]] = field(default_factory=list) # name, size

deepspeed/compile/profilers/graph_profile.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,25 @@ def _backfill_missing_profile_metadata(graph: Graph, profile_complete: bool = Tr
9191
node.meta.setdefault(key, default)
9292

9393

94+
def _run_warmup_for_profile(call_fn, warmup):
95+
for _ in range(warmup):
96+
warmup_out = call_fn()
97+
del warmup_out
98+
99+
100+
def _run_repeatedly_for_profile(call_fn, iteration, start_events, end_events):
101+
out = None
102+
for i in range(iteration):
103+
start_events[i].record()
104+
out = call_fn()
105+
end_events[i].record()
106+
if i + 1 < iteration:
107+
del out
108+
out = None
109+
110+
return out
111+
112+
94113
def _get_mem_usage_out_of_torch():
95114

96115
adjust = 0
@@ -218,19 +237,18 @@ def rebuild_param_if_necessary(v):
218237
alloc_mem_start = get_accelerator().memory_allocated()
219238
max_mem_start = get_accelerator().max_memory_allocated()
220239

221-
if not run_only_once:
222-
for i in range(self.warmup):
223-
out = getattr(self, n.op)(n.target, args, kwargs)
240+
def run_target():
241+
return getattr(self, n.op)(n.target, args, kwargs)
242+
243+
warmup = 0 if run_only_once else self.warmup
244+
_run_warmup_for_profile(run_target, warmup)
224245

225246
if is_comm_op(n):
226247
assert self.distributed, f"Distributed environment is not initialized but comm operator {n.name} {n.target} is used."
227248
dist.barrier()
228249

229250
start = time.time()
230-
for i in range(iteration):
231-
start_events[i].record()
232-
out = getattr(self, n.op)(n.target, args, kwargs)
233-
end_events[i].record()
251+
out = _run_repeatedly_for_profile(run_target, iteration, start_events, end_events)
234252
accelerator.synchronize()
235253
walltime_sum = time.time() - start
236254

@@ -295,13 +313,15 @@ def __init__(self, gm: GraphModule, debug_log=False):
295313
self.device = torch.device(get_accelerator().current_device())
296314
self.mem_record = []
297315
self.last_alloc = get_accelerator().memory_allocated()
316+
self.profile_complete = True
298317

299318
self.node_counter = 0
300319
self.node_num = len(gm.graph.nodes)
301320
self.debug_log = debug_log
302321

303322
def run(self, *args) -> Any:
304323
return_val = None
324+
self.profile_complete = True
305325
try:
306326
assert _all_real_if_tensor(args), "Inputs must be real tensors"
307327
self.nz3.enable_profiling(True)
@@ -311,9 +331,14 @@ def run(self, *args) -> Any:
311331
with get_accelerator().random().fork_rng(devices=[self.device]):
312332
return_val = super().run(*args)
313333
except Exception as e:
334+
self.profile_complete = False
335+
self.mem_record.clear()
314336
print(f"MemoryProfiling error {e}")
315337
finally:
316-
self.nz3.enable_profiling(False)
338+
try:
339+
self.nz3.clear_all_gathered_params()
340+
finally:
341+
self.nz3.enable_profiling(False)
317342

318343
return return_val
319344

tests/unit/compile/test_list_schedule.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from torch.fx import Graph, GraphModule
1212

1313
import deepspeed.compile.util as compile_util
14+
from deepspeed.compile import backend as backend_mod
1415
from deepspeed.compile import inductor as inductor_mod
1516
from deepspeed.compile import list_schedule as schedule_mod
1617
from deepspeed.compile.passes import prefetch as prefetch_mod
@@ -66,6 +67,31 @@ def _placeholder(graph, name):
6667
return _with_meta(graph.placeholder(name))
6768

6869

70+
def test_sync_memory_profile_complete_noops_without_distributed(monkeypatch):
71+
monkeypatch.setattr(backend_mod.dist, "is_initialized", lambda: False)
72+
73+
def fail_all_reduce(*args, **kwargs):
74+
raise AssertionError("all_reduce should not run without distributed init")
75+
76+
monkeypatch.setattr(backend_mod.dist, "all_reduce", fail_all_reduce)
77+
78+
assert backend_mod._sync_memory_profile_complete(True)
79+
assert not backend_mod._sync_memory_profile_complete(False)
80+
81+
82+
def test_sync_memory_profile_complete_reduces_asymmetric_failure(monkeypatch):
83+
monkeypatch.setattr(backend_mod.dist, "is_initialized", lambda: True)
84+
monkeypatch.setattr(backend_mod, "get_accelerator", lambda: SimpleNamespace(current_device=lambda: "cpu"))
85+
86+
def mark_any_rank_failed(tensor, op):
87+
assert op == backend_mod.dist.ReduceOp.MIN
88+
tensor[0] = 0
89+
90+
monkeypatch.setattr(backend_mod.dist, "all_reduce", mark_any_rank_failed)
91+
92+
assert not backend_mod._sync_memory_profile_complete(True)
93+
94+
6995
def _allgather(graph, arg, ds_id, name, tensor_size=1, device_time=1):
7096
return _with_meta(
7197
graph.call_function(torch.ops.dc.allgather_param.default, (arg, 0, ds_id), {"dtype": torch.float16},
@@ -301,6 +327,42 @@ def available_memory(self):
301327
assert any("incomplete profiling data" in message for message in logs)
302328

303329

330+
def test_schedule_prefetch_skips_when_memory_profile_incomplete(monkeypatch):
331+
graph = Graph()
332+
333+
param = _placeholder(graph, "mem_incomplete_param")
334+
ag = _allgather(graph, param, 91, "mem_incomplete")
335+
wait = _wait(graph, ag, 91, "mem_incomplete")
336+
use = _neg(graph, wait, "mem_incomplete_use")
337+
release = _release(graph, use, 91, "mem_incomplete")
338+
339+
graph.output((release, ))
340+
graph.lint()
341+
342+
profiling_results = {
343+
0:
344+
ProfilingResult(fwd_graph=graph,
345+
bwd_graph=None,
346+
fwd_mem=[("profiled_before_abort", 0, 0, 0)],
347+
fwd_mem_complete=False)
348+
}
349+
gm = GraphModule(torch.nn.Module(), graph)
350+
logs = []
351+
352+
monkeypatch.setattr(prefetch_mod, "print_rank_0", lambda message: logs.append(message))
353+
354+
assert prefetch_mod.schedule_prefetch(gm,
355+
graph_id=0,
356+
graph_order=[(0, False)],
357+
profiling_results=profiling_results,
358+
create_inputs_fn=lambda: (),
359+
mem_budget=0,
360+
param_manager={},
361+
bwd=False) is gm
362+
assert gm.graph is graph
363+
assert any("incomplete profiling data" in message for message in logs)
364+
365+
304366
def test_graphsafe_rng_state_outputs_are_registered_no_reuse():
305367
graphsafe_run_with_rng_state = inductor_mod._get_graphsafe_run_with_rng_state()
306368
if graphsafe_run_with_rng_state is None:

0 commit comments

Comments
 (0)