Skip to content

Commit 8f8f693

Browse files
committed
Scope DeepCompile compiler lifecycle ownership
Signed-off-by: Masahiro Tanaka <mtanaka@anyscale.com>
1 parent 4c275f9 commit 8f8f693

8 files changed

Lines changed: 455 additions & 58 deletions

File tree

deepspeed/compile/backend.py

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,22 @@ def clear(self):
7777
fwd_real_inputs = []
7878

7979

80+
def cleanup_compiled_backward_state(frame_id=None, owned_frames=None):
81+
"""Release engine-owned process-global compiled-backward state."""
82+
if frame_id is None:
83+
if owned_frames is None:
84+
frames_needing_bwd.clear()
85+
else:
86+
frames_needing_bwd.difference_update(owned_frames)
87+
owned_frames.clear()
88+
else:
89+
frames_needing_bwd.discard(frame_id)
90+
if owned_frames is not None:
91+
owned_frames.discard(frame_id)
92+
if len(frames_needing_bwd) == 0:
93+
unpatch_compiled_func()
94+
95+
8096
def register_compile_pass(name: str, opt_pass_fn, contract=None):
8197
from .passes.contract import register_pass_contract
8298
opt_passes[name] = opt_pass_fn
@@ -97,7 +113,8 @@ def init_schedule(schedule):
97113
remaining_schedule = deque(schedule)
98114

99115

100-
def launch_compile_passes(global_steps: int):
116+
def launch_compile_passes(global_steps: int, owned_frames=None):
117+
"""Advance the pass schedule and discard state owned by the previous compile cycle."""
101118
global next_pass_step, next_passes
102119

103120
if len(remaining_schedule) > 0 and global_steps == remaining_schedule[0][0]:
@@ -109,6 +126,7 @@ def launch_compile_passes(global_steps: int):
109126
graph_order_with_frame_id.clear()
110127
profiling_results.clear()
111128
param_manager.clear()
129+
cleanup_compiled_backward_state(owned_frames=owned_frames)
112130
frames_partitioned.clear()
113131

114132

@@ -201,6 +219,21 @@ def set_example_values_to_symints(real_inputs, param_indices=None):
201219
return tuple(real_inputs_ret)
202220

203221

222+
def _get_fw_real_inputs(local_real_inputs, input_storage: InputStorage, graph_id: int, debug_log: bool = False):
223+
"""Resolve graph-local real inputs from the one-shot queue or persistent storage."""
224+
if local_real_inputs:
225+
return local_real_inputs.popleft()
226+
227+
if input_storage.has_data():
228+
if debug_log:
229+
log_rank0(f"Retrieving real inputs from storage for graph_id={graph_id}", enable=True)
230+
return input_storage.get()
231+
232+
raise RuntimeError(f"No real inputs available for graph_id {graph_id}. "
233+
f"Local queue size: {len(local_real_inputs)}, "
234+
f"storage has data: {input_storage.has_data()}")
235+
236+
204237
def run_opt_passes(opt_passes: List[Callable],
205238
gm: GraphModule,
206239
graph_id: int,
@@ -243,14 +276,18 @@ def run_opt_passes(opt_passes: List[Callable],
243276
get_accelerator().empty_cache()
244277

245278

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

248281
register_custom_ops()
249282

250283
# Extract values from compile_config
251284
debug_log = compile_config.debug_log
252285
free_activation = compile_config.free_activation and not is_backend_inductor(backend)
253286

287+
if owned_frames is None:
288+
owned_frames = set()
289+
owner_token = object()
290+
254291
def backend_fn(gm: GraphModule, real_inputs):
255292
graph_id = id(gm.graph)
256293

@@ -263,6 +300,7 @@ def backend_fn(gm: GraphModule, real_inputs):
263300
# This check cannot be placed here because autograd creates the fw/bw compiler callables before graph
264301
# partitioning. It is thus postponed to the point where the fw compiler is called.
265302
frame_id = gm.meta["dynamo_compile_id"].frame_id
303+
frame_key = (owner_token, frame_id)
266304
graph_order_with_frame_id.add_graph(graph_id, frame_id)
267305

268306
z3_partition = any(hasattr(v, "ds_id") for v in real_inputs)
@@ -275,17 +313,15 @@ def backend_fn(gm: GraphModule, real_inputs):
275313
param_indices = [(i, input_val.param_id, input_val.shape) for i, input_val in enumerate(real_inputs)
276314
if isinstance(input_val, torch.nn.Parameter)]
277315

278-
global fwd_real_inputs
279-
280316
# Create an InputStorage instance for this specific graph
281317
# It will be captured by the make_fw_graph closure, eliminating the need for graph ID management
282318
input_storage = InputStorage(keep_int_input_tensors=compile_config.keep_int_input_tensors,
283319
keep_all_input_tensors=compile_config.keep_all_input_tensors)
284320

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

291327
global profiling_results
@@ -304,20 +340,10 @@ def make_fw_graph(gm, sample_inputs):
304340
if needs_backward:
305341
if len(frames_needing_bwd) == 0:
306342
patch_compiled_func()
307-
frames_needing_bwd.add(frame_id)
308-
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()}")
343+
frames_needing_bwd.add(frame_key)
344+
owned_frames.add(frame_key)
320345

346+
real_inputs = _get_fw_real_inputs(local_fwd_real_inputs, input_storage, graph_id, debug_log=debug_log)
321347
real_inputs = set_example_values_to_symints(real_inputs)
322348

323349
param_manager[graph_id] = DSGraphParamManager(gm.graph, real_inputs, param_indices)
@@ -385,9 +411,7 @@ def make_bw_graph(gm, sample_inputs):
385411
add_free_activations(graph_id, gm.graph,
386412
get_activation_node_names(gm.graph, param_nodes_bw, non_param_input_names))
387413

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

392416
log_rank0(
393417
f"Bwd end {graph_index} graph_id={graph_id} alloc_mem={get_accelerator().memory_allocated()} graph={gm.graph}",
@@ -415,10 +439,15 @@ def compiler_fn(gm, sample_inputs):
415439
partition_fn=partition_fn)
416440
return torch._dynamo.optimize(**compile_kwargs)(aot_mod)
417441
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)
442+
restore_aotautograd = patch_create_aot_dispatcher_function(graph_id, z3_partition, make_fw_graph,
443+
make_bw_graph, real_inputs, param_indices,
444+
param_manager, frame_id, frames_partitioned)
445+
try:
446+
return torch._inductor.compile(gm, real_inputs)
447+
finally:
448+
# AotAutograd.__init__ is process-global; never leak this
449+
# graph-specific compiler wiring into a later compilation.
450+
restore_aotautograd()
422451

423452
raise ValueError(f"Unsupported backend {backend}")
424453

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. Replace the currently installed
153+
# DeepCompile patch before taking ownership for this graph.
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)

deepspeed/compile/init_z3.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
# DeepSpeed Team
55

6+
from functools import partial
7+
from threading import Lock
8+
69
import torch
710

811
from deepspeed import comm as dist
@@ -19,6 +22,54 @@
1922
WARMUP = 5
2023

2124
_MISSING = object()
25+
_DYNAMO_CONFIG_NAMES = ("force_parameter_static_shapes", "force_nn_module_property_static_shapes")
26+
_DYNAMO_CONFIG_OWNERS = {}
27+
_DYNAMO_CONFIG_LOCK = Lock()
28+
29+
30+
def _allow_dynamo_dynamic_parameter_shapes_for_z3(compile_kwargs):
31+
"""Acquire process-wide ZeRO-3 Dynamo config ownership and return its release callback."""
32+
dynamo = getattr(torch, "_dynamo", None)
33+
if dynamo is None:
34+
try:
35+
import torch._dynamo as dynamo
36+
except ImportError:
37+
return None
38+
39+
dynamo_config = getattr(dynamo, "config", None)
40+
if dynamo_config is None:
41+
return None
42+
43+
owner_token = object()
44+
config_key = id(dynamo_config)
45+
with _DYNAMO_CONFIG_LOCK:
46+
state = _DYNAMO_CONFIG_OWNERS.get(config_key)
47+
if state is None or state["config"] is not dynamo_config:
48+
previous_values = {
49+
config_name: getattr(dynamo_config, config_name)
50+
for config_name in _DYNAMO_CONFIG_NAMES if hasattr(dynamo_config, config_name)
51+
}
52+
if not previous_values:
53+
return None
54+
state = {"config": dynamo_config, "previous_values": previous_values, "owner_tokens": set()}
55+
_DYNAMO_CONFIG_OWNERS[config_key] = state
56+
state["owner_tokens"].add(owner_token)
57+
for config_name in state["previous_values"]:
58+
setattr(dynamo_config, config_name, False)
59+
60+
def restore():
61+
with _DYNAMO_CONFIG_LOCK:
62+
state = _DYNAMO_CONFIG_OWNERS.get(config_key)
63+
if state is None or state["config"] is not dynamo_config or owner_token not in state["owner_tokens"]:
64+
return
65+
state["owner_tokens"].remove(owner_token)
66+
if state["owner_tokens"]:
67+
return
68+
for config_name, previous_value in state["previous_values"].items():
69+
setattr(dynamo_config, config_name, previous_value)
70+
del _DYNAMO_CONFIG_OWNERS[config_key]
71+
72+
return restore
2273

2374

2475
def _resolve_expected_grad_dtype(param):
@@ -102,9 +153,21 @@ def set_grad_buffer(_is_gradient_accumulation_boundary):
102153
if move_opt_states in passes or move_opt_states_sync in passes:
103154
init_offload_opt_states(optimizer, dc)
104155

105-
engine.launch_compile_passes = launch_compile_passes
156+
engine._deepcompile_owned_frames = set()
157+
engine.launch_compile_passes = partial(launch_compile_passes, owned_frames=engine._deepcompile_owned_frames)
106158

107159
patch_fake_tensor()
108160
torch._inductor.config.size_asserts = False
109161

110-
return make_backend(backend, compile_config, compile_kwargs=compile_kwargs)
162+
previous_restore = getattr(engine, "_deepcompile_dynamo_config_restore", None)
163+
if previous_restore is not None:
164+
previous_restore()
165+
del engine._deepcompile_dynamo_config_restore
166+
restore_dynamo_config = _allow_dynamo_dynamic_parameter_shapes_for_z3(compile_kwargs)
167+
if restore_dynamo_config is not None:
168+
engine._deepcompile_dynamo_config_restore = restore_dynamo_config
169+
170+
return make_backend(backend,
171+
compile_config,
172+
compile_kwargs=compile_kwargs,
173+
owned_frames=engine._deepcompile_owned_frames)

deepspeed/compile/patch_compiled_func.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,21 @@ class PatchedFunction(torch.autograd.Function, metaclass=FunctionMeta):
8282

8383

8484
def unpatch_compiled_func():
85+
"""Restore torch.autograd.Function and discard inputs captured for this compile cycle."""
8586
global enabled_patched_func
8687
enabled_patched_func = False
8788

8889
global original_grad_fn
89-
torch.autograd.Function = original_grad_fn
90+
if original_grad_fn is not None:
91+
torch.autograd.Function = original_grad_fn
92+
original_grad_fn = None
93+
clear_backward_inputs()
9094

9195

9296
def get_backward_inputs():
9397
return backward_inputs
98+
99+
100+
def clear_backward_inputs():
101+
"""Drop captured real backward inputs before the next graph compilation."""
102+
backward_inputs.clear()

0 commit comments

Comments
 (0)