Skip to content

Commit 0b27f37

Browse files
committed
Harden DeepCompile compiler lifecycle cleanup
Signed-off-by: Masahiro Tanaka <mtanaka@anyscale.com>
1 parent 4c275f9 commit 0b27f37

8 files changed

Lines changed: 676 additions & 57 deletions

File tree

deepspeed/compile/backend.py

Lines changed: 110 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ def set_needs_backward(self, frame_id: int, needs_backward: bool):
5858
if frame_id in self.frames:
5959
self.frames[frame_id] = (self.frames[frame_id][0], needs_backward)
6060

61+
def remove_graph(self, graph_id: int, frame_id: int) -> bool:
62+
if frame_id in self.frames and self.frames[frame_id][0] == graph_id:
63+
del self.frames[frame_id]
64+
return True
65+
return False
66+
6167
def get_graph_order(self) -> List[Tuple[int, bool]]:
6268
assert all(isinstance(needs_backward, bool) for _, needs_backward in self.frames.values())
6369
return list(self.frames.values())
@@ -77,6 +83,66 @@ def clear(self):
7783
fwd_real_inputs = []
7884

7985

86+
def cleanup_compiled_backward_state(frame_id=None, owned_frames=None):
87+
"""Release process-global compiled-backward state after completion or failure."""
88+
if frame_id is None:
89+
if owned_frames is None:
90+
frames_needing_bwd.clear()
91+
else:
92+
frames_needing_bwd.difference_update(owned_frames)
93+
owned_frames.clear()
94+
else:
95+
frames_needing_bwd.discard(frame_id)
96+
if owned_frames is not None:
97+
owned_frames.discard(frame_id)
98+
if len(frames_needing_bwd) == 0:
99+
unpatch_compiled_func()
100+
101+
102+
def cleanup_backend_state(frame_id, graph_id, owned_frames=None):
103+
"""Release all process-global state owned by a failed graph attempt."""
104+
if graph_order_with_frame_id.remove_graph(graph_id, frame_id):
105+
frames_partitioned.discard(frame_id)
106+
profiling_results.pop(graph_id, None)
107+
param_manager.pop(graph_id, None)
108+
opt_pass_times[:] = [timing for timing in opt_pass_times if timing[2] != graph_id]
109+
cleanup_compiled_backward_state(frame_id, owned_frames)
110+
111+
112+
def _cleanup_compiled_backward_state_on_error(frame_id, graph_id, owned_frames=None):
113+
114+
def decorator(fn):
115+
116+
def wrapped(*args, **kwargs):
117+
try:
118+
return fn(*args, **kwargs)
119+
except Exception:
120+
cleanup_backend_state(frame_id, graph_id, owned_frames)
121+
raise
122+
123+
return wrapped
124+
125+
return decorator
126+
127+
128+
def _cleanup_compiled_backward_backend_state_on_error(owned_frames=None):
129+
130+
def decorator(fn):
131+
132+
def wrapped(gm, *args, **kwargs):
133+
frame_id = gm.meta["dynamo_compile_id"].frame_id
134+
graph_id = id(gm.graph)
135+
try:
136+
return fn(gm, *args, **kwargs)
137+
except Exception:
138+
cleanup_backend_state(frame_id, graph_id, owned_frames)
139+
raise
140+
141+
return wrapped
142+
143+
return decorator
144+
145+
80146
def register_compile_pass(name: str, opt_pass_fn, contract=None):
81147
from .passes.contract import register_pass_contract
82148
opt_passes[name] = opt_pass_fn
@@ -97,7 +163,8 @@ def init_schedule(schedule):
97163
remaining_schedule = deque(schedule)
98164

99165

100-
def launch_compile_passes(global_steps: int):
166+
def launch_compile_passes(global_steps: int, owned_frames=None):
167+
"""Advance the pass schedule and discard state owned by the previous compile cycle."""
101168
global next_pass_step, next_passes
102169

103170
if len(remaining_schedule) > 0 and global_steps == remaining_schedule[0][0]:
@@ -109,6 +176,8 @@ def launch_compile_passes(global_steps: int):
109176
graph_order_with_frame_id.clear()
110177
profiling_results.clear()
111178
param_manager.clear()
179+
fwd_real_inputs.clear()
180+
cleanup_compiled_backward_state(owned_frames=owned_frames)
112181
frames_partitioned.clear()
113182

114183

@@ -201,6 +270,25 @@ def set_example_values_to_symints(real_inputs, param_indices=None):
201270
return tuple(real_inputs_ret)
202271

203272

273+
def _get_fw_real_inputs(local_real_inputs, input_storage: InputStorage, graph_id: int, debug_log: bool = False):
274+
"""Resolve real inputs from the one-shot queue, persistent storage, then legacy state."""
275+
if local_real_inputs:
276+
return local_real_inputs.popleft()
277+
278+
if input_storage.has_data():
279+
if debug_log:
280+
log_rank0(f"Retrieving real inputs from storage for graph_id={graph_id}", enable=True)
281+
return input_storage.get()
282+
283+
if fwd_real_inputs:
284+
return fwd_real_inputs.pop(0)
285+
286+
raise RuntimeError(f"No real inputs available for graph_id {graph_id}. "
287+
f"Local queue size: {len(local_real_inputs)}, "
288+
f"global queue size: {len(fwd_real_inputs)}, "
289+
f"storage has data: {input_storage.has_data()}")
290+
291+
204292
def run_opt_passes(opt_passes: List[Callable],
205293
gm: GraphModule,
206294
graph_id: int,
@@ -243,14 +331,18 @@ def run_opt_passes(opt_passes: List[Callable],
243331
get_accelerator().empty_cache()
244332

245333

246-
def make_backend(backend, compile_config, compile_kwargs={}):
334+
def make_backend(backend, compile_config, compile_kwargs={}, owned_frames=None):
247335

248336
register_custom_ops()
249337

250338
# Extract values from compile_config
251339
debug_log = compile_config.debug_log
252340
free_activation = compile_config.free_activation and not is_backend_inductor(backend)
253341

342+
if owned_frames is None:
343+
owned_frames = set()
344+
345+
@_cleanup_compiled_backward_backend_state_on_error(owned_frames)
254346
def backend_fn(gm: GraphModule, real_inputs):
255347
graph_id = id(gm.graph)
256348

@@ -275,24 +367,23 @@ def backend_fn(gm: GraphModule, real_inputs):
275367
param_indices = [(i, input_val.param_id, input_val.shape) for i, input_val in enumerate(real_inputs)
276368
if isinstance(input_val, torch.nn.Parameter)]
277369

278-
global fwd_real_inputs
279-
280370
# Create an InputStorage instance for this specific graph
281371
# It will be captured by the make_fw_graph closure, eliminating the need for graph ID management
282372
input_storage = InputStorage(keep_int_input_tensors=compile_config.keep_int_input_tensors,
283373
keep_all_input_tensors=compile_config.keep_all_input_tensors)
284374

285-
# Store in both list (for backward compatibility) and storage (for persistence)
375+
# Store in a closure-local queue and storage (for persistence).
286376
# The input_storage keeps tensor metadata to handle cases where
287377
# backend_fn is called once but make_fw_graph is called multiple times
288-
fwd_real_inputs.append(real_inputs)
378+
local_fwd_real_inputs = deque([real_inputs])
289379
input_storage.put(real_inputs)
290380

291381
global profiling_results
292382
if graph_id not in profiling_results:
293383
profiling_results[graph_id] = ProfilingResult()
294384
profiling_results[graph_id].param_indices = param_indices
295385

386+
@_cleanup_compiled_backward_state_on_error(frame_id, graph_id, owned_frames)
296387
def make_fw_graph(gm, sample_inputs):
297388
time_start = time.time()
298389
graph_index = len(graph_order_with_frame_id) - 1
@@ -305,19 +396,9 @@ def make_fw_graph(gm, sample_inputs):
305396
if len(frames_needing_bwd) == 0:
306397
patch_compiled_func()
307398
frames_needing_bwd.add(frame_id)
399+
owned_frames.add(frame_id)
308400

309-
# Try to get real_inputs from the list first, then from storage
310-
if fwd_real_inputs:
311-
real_inputs = fwd_real_inputs.pop(0)
312-
elif input_storage.has_data():
313-
# Note: input_storage is captured from the enclosing backend_fn scope
314-
# Materialize tensors from storage when list is empty
315-
log_rank0(f"Retrieving real inputs from storage for graph_id={graph_id}", enable=debug_log)
316-
real_inputs = input_storage.get()
317-
else:
318-
raise RuntimeError(f"No real inputs available for graph_id {graph_id}. "
319-
f"List size: {len(fwd_real_inputs)}, Storage has data: {input_storage.has_data()}")
320-
401+
real_inputs = _get_fw_real_inputs(local_fwd_real_inputs, input_storage, graph_id, debug_log=debug_log)
321402
real_inputs = set_example_values_to_symints(real_inputs)
322403

323404
param_manager[graph_id] = DSGraphParamManager(gm.graph, real_inputs, param_indices)
@@ -342,6 +423,7 @@ def make_fw_graph(gm, sample_inputs):
342423

343424
return gm.graph
344425

426+
@_cleanup_compiled_backward_state_on_error(frame_id, graph_id, owned_frames)
345427
def make_bw_graph(gm, sample_inputs):
346428
time_start = time.time()
347429

@@ -385,9 +467,7 @@ def make_bw_graph(gm, sample_inputs):
385467
add_free_activations(graph_id, gm.graph,
386468
get_activation_node_names(gm.graph, param_nodes_bw, non_param_input_names))
387469

388-
frames_needing_bwd.remove(frame_id)
389-
if len(frames_needing_bwd) == 0:
390-
unpatch_compiled_func()
470+
cleanup_compiled_backward_state(frame_id, owned_frames)
391471

392472
log_rank0(
393473
f"Bwd end {graph_index} graph_id={graph_id} alloc_mem={get_accelerator().memory_allocated()} graph={gm.graph}",
@@ -415,10 +495,15 @@ def compiler_fn(gm, sample_inputs):
415495
partition_fn=partition_fn)
416496
return torch._dynamo.optimize(**compile_kwargs)(aot_mod)
417497
elif backend == "inductor":
418-
patch_create_aot_dispatcher_function(graph_id, z3_partition, make_fw_graph, make_bw_graph, real_inputs,
419-
param_indices, param_manager, frame_id, frames_partitioned)
420-
421-
return torch._inductor.compile(gm, real_inputs)
498+
restore_aotautograd = patch_create_aot_dispatcher_function(graph_id, z3_partition, make_fw_graph,
499+
make_bw_graph, real_inputs, param_indices,
500+
param_manager, frame_id, frames_partitioned)
501+
try:
502+
return torch._inductor.compile(gm, real_inputs)
503+
finally:
504+
# AotAutograd.__init__ is process-global; never leak this
505+
# graph-specific compiler wiring into a later compilation.
506+
restore_aotautograd()
422507

423508
raise ValueError(f"Unsupported backend {backend}")
424509

deepspeed/compile/inductor.py

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -144,36 +144,45 @@ def _patch_deepcompile_aot_kwargs(kwargs: dict, *, graph_id: int, z3_partition:
144144

145145
def patch_create_aot_dispatcher_function(graph_id: int, z3_partition: bool, make_fw_graph, make_bw_graph, real_inputs,
146146
param_indices, param_manager, frame_id: int, frames_partitioned: Set[int]):
147+
"""Temporarily install graph-specific AOT compilers and return an idempotent restore callback."""
147148

148149
from torch._dynamo.backends.common import AotAutograd
149150
import functools
150151

151-
def patch_aotautograd():
152-
# Unpatch if it was already patched
153-
if hasattr(AotAutograd, "__original_init"):
154-
AotAutograd.__init__ = AotAutograd.__original_init
155-
156-
original_init = AotAutograd.__init__
157-
158-
@functools.wraps(original_init)
159-
def patched_init(self, **kwargs):
160-
_patch_deepcompile_aot_kwargs(kwargs,
161-
graph_id=graph_id,
162-
z3_partition=z3_partition,
163-
make_fw_graph=make_fw_graph,
164-
make_bw_graph=make_bw_graph,
165-
real_inputs=real_inputs,
166-
param_indices=param_indices,
167-
param_manager=param_manager,
168-
frame_id=frame_id,
169-
frames_partitioned=frames_partitioned)
170-
171-
original_init(self, **kwargs)
172-
173-
AotAutograd.__original_init = original_init
174-
AotAutograd.__init__ = patched_init
175-
176-
patch_aotautograd()
152+
# The constructor patch is process-global. Recover first if a previous
153+
# compile failed before reaching its restoration callback.
154+
if hasattr(AotAutograd, "__original_init"):
155+
AotAutograd.__init__ = AotAutograd.__original_init
156+
delattr(AotAutograd, "__original_init")
157+
158+
original_init = AotAutograd.__init__
159+
160+
@functools.wraps(original_init)
161+
def patched_init(self, **kwargs):
162+
_patch_deepcompile_aot_kwargs(kwargs,
163+
graph_id=graph_id,
164+
z3_partition=z3_partition,
165+
make_fw_graph=make_fw_graph,
166+
make_bw_graph=make_bw_graph,
167+
real_inputs=real_inputs,
168+
param_indices=param_indices,
169+
param_manager=param_manager,
170+
frame_id=frame_id,
171+
frames_partitioned=frames_partitioned)
172+
173+
original_init(self, **kwargs)
174+
175+
AotAutograd.__original_init = original_init
176+
AotAutograd.__init__ = patched_init
177+
178+
def restore_aotautograd():
179+
"""Restore only this invocation's patch without clobbering a newer owner."""
180+
if AotAutograd.__init__ is patched_init:
181+
AotAutograd.__init__ = original_init
182+
if getattr(AotAutograd, "__original_init", None) is original_init:
183+
delattr(AotAutograd, "__original_init")
184+
185+
return restore_aotautograd
177186

178187

179188
def register_custom_ops():

deepspeed/compile/init_z1.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# DeepSpeed Team
55

66
import copy
7+
from functools import partial
78

89
import torch
910

@@ -185,5 +186,9 @@ def release_grad_buffer(group_idx=None):
185186

186187
init_schedule(schedule)
187188

188-
engine.launch_compile_passes = launch_compile_passes
189-
return make_backend(backend, compile_config, compile_kwargs=compile_kwargs)
189+
engine._deepcompile_owned_frames = set()
190+
engine.launch_compile_passes = partial(launch_compile_passes, owned_frames=engine._deepcompile_owned_frames)
191+
return make_backend(backend,
192+
compile_config,
193+
compile_kwargs=compile_kwargs,
194+
owned_frames=engine._deepcompile_owned_frames)

0 commit comments

Comments
 (0)