Skip to content

Commit 29109a7

Browse files
committed
[bugfix]: preserve compile across lazy module reloads
Register pipeline-level compilation as a lazy materialization transform so every freshly loaded component receives the same compile setup. Keep whole-module compilation behind the proxy, preserve release scheduling, and add regression coverage for conditional and whole-module compile paths.
1 parent 2d9f8ac commit 29109a7

4 files changed

Lines changed: 184 additions & 20 deletions

File tree

docs/inference/offloading.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ pipeline that is roughly `max(text encoder, DiT + VAE)` rather than
107107

108108
A freed component is read from disk again on the next generation, so a
109109
multi-prompt run pays one reload per component per request. For a large text
110-
encoder that is tens of seconds.
110+
encoder that is tens of seconds. If pipeline-level `torch.compile` is enabled,
111+
the compile setup is reapplied after each reload; PyTorch can reuse its graph
112+
and kernel caches when the component structure and input shapes are unchanged.
111113

112114
#### Usage Recommendation
113115

fastvideo/pipelines/composed_pipeline_base.py

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import os
1010
from abc import ABC, abstractmethod
1111
from collections.abc import Iterator
12+
from functools import partial
1213
from typing import Any, cast
1314

1415
import torch
@@ -201,49 +202,71 @@ def _compile_with_conditions(
201202
compiled_count += 1
202203
return compiled_count
203204

204-
def _maybe_compile_pipeline_module(
205-
self,
205+
@staticmethod
206+
def _compile_pipeline_module_instance(
206207
module_name: str,
208+
module: torch.nn.Module,
207209
fsdp_module_cls: type | None,
208210
compile_kwargs: dict[str, Any],
209-
) -> None:
210-
if module_name not in self.modules:
211-
return
212-
213-
entry = self.modules[module_name]
214-
# Materialize into a local. The dict keeps the proxy until we know a
215-
# compiled callable is actually going to replace it, so a component
216-
# that turns out to be FSDP-wrapped is neither compiled nor stripped of
217-
# its release hook.
218-
module = entry.materialize() if is_lazy_module(entry) else entry
211+
) -> Any:
212+
"""Apply pipeline-level compile setup to one loaded component."""
219213
if fsdp_module_cls is not None and isinstance(module, fsdp_module_cls):
220214
logger.info(
221215
"%s is already FSDP-wrapped; skipping torch.compile in pipeline",
222216
module_name.capitalize(),
223217
)
224-
return
218+
return module
225219

226220
prepare_for_compile = getattr(module, "prepare_for_compile", None)
227221
if callable(prepare_for_compile):
228222
logger.info("Running prepare_for_compile for %s", module_name)
229223
prepare_for_compile()
230224

231-
compiled_count = self._compile_with_conditions(module, compile_kwargs)
225+
compiled_count = ComposedPipelineBase._compile_with_conditions(module, compile_kwargs)
232226
if compiled_count > 0:
233227
logger.info(
234228
"Enabled torch.compile for %d submodules in %s via _compile_conditions with kwargs=%s",
235229
compiled_count,
236230
module_name,
237231
compile_kwargs,
238232
)
239-
return
233+
return module
240234

241235
# Backward-compatible fallback: compile full module if no condition matched.
242236
logger.info("Enabling torch.compile for %s with kwargs=%s", module_name, compile_kwargs)
237+
return torch.compile(module, **compile_kwargs)
238+
239+
def _maybe_compile_pipeline_module(
240+
self,
241+
module_name: str,
242+
fsdp_module_cls: type | None,
243+
compile_kwargs: dict[str, Any],
244+
) -> None:
245+
if module_name not in self.modules:
246+
return
247+
248+
entry = self.modules[module_name]
243249
if is_lazy_module(entry):
244-
logger.info("Whole-module torch.compile replaces the deferred %s, so it stays resident for the run",
245-
module_name)
246-
self.modules[module_name] = torch.compile(module, **compile_kwargs)
250+
# Compilation is part of materialization, not a one-time mutation
251+
# of the first loaded instance. The proxy remains in the module
252+
# map, so whole-module and conditional compile both survive every
253+
# release/reload cycle without making initialization eager.
254+
entry.set_materialize_transform(
255+
partial(
256+
ComposedPipelineBase._compile_pipeline_module_instance,
257+
module_name,
258+
fsdp_module_cls=fsdp_module_cls,
259+
compile_kwargs=dict(compile_kwargs),
260+
))
261+
logger.info("Configured torch.compile for every materialization of deferred %s", module_name)
262+
return
263+
264+
self.modules[module_name] = self._compile_pipeline_module_instance(
265+
module_name,
266+
entry,
267+
fsdp_module_cls,
268+
compile_kwargs,
269+
)
247270

248271
def post_init(self) -> None:
249272
assert self.fastvideo_args is not None, "fastvideo_args must be set"

fastvideo/pipelines/lazy_module.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ class LazyModule:
4343
early is a latency cost, never a correctness one.
4444
"""
4545

46-
__slots__ = ("_lazy_name", "_lazy_loader", "_lazy_module")
46+
__slots__ = ("_lazy_name", "_lazy_loader", "_lazy_materialize_transform", "_lazy_module")
4747

4848
def __init__(self, name: str, loader: Callable[[], Any]) -> None:
4949
object.__setattr__(self, "_lazy_name", name)
5050
object.__setattr__(self, "_lazy_loader", loader)
51+
object.__setattr__(self, "_lazy_materialize_transform", None)
5152
object.__setattr__(self, "_lazy_module", None)
5253

5354
@property
@@ -70,13 +71,40 @@ def materialize(self) -> Any:
7071
module = loader()
7172
if module is None:
7273
raise ValueError(f"Deferred loader for module {name} returned None")
74+
75+
transform = object.__getattribute__(self, "_lazy_materialize_transform")
76+
if transform is not None:
77+
module = transform(module)
78+
if module is None:
79+
raise ValueError(f"Materialize transform for module {name} returned None")
7380
object.__setattr__(self, "_lazy_module", module)
7481

7582
allocated = _cuda_allocated_gib()
7683
if allocated is not None:
7784
logger.info("Loaded deferred module %s, cuda allocated now %.2f GiB", name, allocated)
7885
return module
7986

87+
def set_materialize_transform(self, transform: Callable[[Any], Any]) -> None:
88+
"""Apply ``transform`` to this and every future loaded instance.
89+
90+
Registering a transform does not itself load a deferred component. If
91+
something has already materialized the component, transform that
92+
instance immediately so current and future instances have the same
93+
setup. A transform may return a wrapper, as ``torch.compile`` does.
94+
"""
95+
current_transform = object.__getattribute__(self, "_lazy_materialize_transform")
96+
if current_transform is not None:
97+
raise RuntimeError(f"Materialize transform for module {self.lazy_name} is already set")
98+
99+
module = object.__getattribute__(self, "_lazy_module")
100+
transformed = transform(module) if module is not None else None
101+
if module is not None and transformed is None:
102+
raise ValueError(f"Materialize transform for module {self.lazy_name} returned None")
103+
104+
object.__setattr__(self, "_lazy_materialize_transform", transform)
105+
if module is not None:
106+
object.__setattr__(self, "_lazy_module", transformed)
107+
80108
def release(self) -> bool:
81109
"""Drop the real component. Returns True if something was released."""
82110
module = object.__getattribute__(self, "_lazy_module")

fastvideo/tests/stages/test_lazy_module_load.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,30 @@ def test_release_then_reload_is_correct_not_broken():
154154
assert second.tag == "c"
155155

156156

157+
def test_materialize_transform_applies_to_every_loaded_instance_without_loading_eagerly():
158+
loader, calls = _counting_loader()
159+
transformed = []
160+
module = LazyModule("vae", loader)
161+
162+
def transform(component):
163+
transformed.append(component)
164+
component.tag = f"compiled-{component.tag}"
165+
return component
166+
167+
module.set_materialize_transform(transform)
168+
assert calls == []
169+
170+
first = module.materialize()
171+
assert first.tag == "compiled-c"
172+
assert module.release() is True
173+
174+
second = module.materialize()
175+
assert second.tag == "compiled-c"
176+
assert second is not first
177+
assert calls == ["c", "c"]
178+
assert transformed == [first, second]
179+
180+
157181
def test_release_without_materializing_is_a_noop():
158182
loader, calls = _counting_loader()
159183
module = LazyModule("text_encoder", loader)
@@ -204,6 +228,93 @@ def _lazy(name):
204228
return LazyModule(name, lambda: _Component(name))
205229

206230

231+
def test_pipeline_compile_is_reapplied_after_lazy_release(monkeypatch):
232+
loads = []
233+
compile_calls = []
234+
235+
class _CompileAwareComponent(torch.nn.Module):
236+
_compile_conditions = (lambda name, module: name == "block", )
237+
238+
def __init__(self):
239+
super().__init__()
240+
self.block = torch.nn.Linear(2, 2)
241+
self.prepare_calls = 0
242+
243+
def prepare_for_compile(self):
244+
self.prepare_calls += 1
245+
246+
def load_component():
247+
component = _CompileAwareComponent()
248+
loads.append(component)
249+
return component
250+
251+
def fake_compile(target, **kwargs):
252+
compile_calls.append((target, kwargs))
253+
return target
254+
255+
monkeypatch.setattr(torch, "compile", fake_compile)
256+
lazy = LazyModule("vae", load_component)
257+
pipeline = _FakePipeline({"vae": lazy}, [])
258+
259+
pipeline._maybe_compile_pipeline_module("vae", None, {"mode": "reduce-overhead"})
260+
assert loads == []
261+
262+
first = lazy.materialize()
263+
assert first.prepare_calls == 1
264+
assert lazy.release() is True
265+
266+
second = lazy.materialize()
267+
assert second is not first
268+
assert second.prepare_calls == 1
269+
assert loads == [first, second]
270+
assert len(compile_calls) == 2
271+
assert [kwargs for _, kwargs in compile_calls] == [
272+
{"mode": "reduce-overhead"},
273+
{"mode": "reduce-overhead"},
274+
]
275+
276+
277+
def test_whole_module_compile_keeps_lazy_proxy_and_recompiles_after_release(monkeypatch):
278+
loads = []
279+
compile_calls = []
280+
281+
class _WholeComponent(torch.nn.Module):
282+
283+
def forward(self, value):
284+
return value
285+
286+
class _Compiled:
287+
288+
def __init__(self, original):
289+
self.original = original
290+
291+
def load_component():
292+
component = _WholeComponent()
293+
loads.append(component)
294+
return component
295+
296+
def fake_compile(target, **kwargs):
297+
compile_calls.append((target, kwargs))
298+
return _Compiled(target)
299+
300+
monkeypatch.setattr(torch, "compile", fake_compile)
301+
lazy = LazyModule("text_encoder", load_component)
302+
pipeline = _FakePipeline({"text_encoder": lazy}, [])
303+
304+
pipeline._maybe_compile_pipeline_module("text_encoder", None, {"dynamic": True})
305+
assert pipeline.modules["text_encoder"] is lazy
306+
assert loads == []
307+
308+
first = lazy.materialize()
309+
assert first.original is loads[0]
310+
assert lazy.release() is True
311+
312+
second = lazy.materialize()
313+
assert second.original is loads[1]
314+
assert second is not first
315+
assert len(compile_calls) == 2
316+
317+
207318
def test_schedule_releases_after_the_last_stage_that_holds_a_module():
208319
text_encoder = _lazy("text_encoder")
209320
transformer = _lazy("transformer")

0 commit comments

Comments
 (0)