Skip to content

Commit 579fb4e

Browse files
authored
Add torch_compile flag for training/inference (#28)
* Add torch_compile_mode for training networks Add an optional torch_compile_mode config flag ("default", "reduce-overhead", "max-autotune"; None disables). Networks to compile are collected in a compile_dict (derived from model_dict minus EMA, plus the teacher and the net's preprocessors) and compiled in place via nn.Module.compile by the trainer, after DDP/FSDP wrapping.
1 parent c40fbea commit 579fb4e

7 files changed

Lines changed: 281 additions & 15 deletions

File tree

fastgen/configs/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ class BaseModelConfig:
160160
# - however, it is required if the model has a discriminator or the net initializes unused modules (e.g., for logvar predictions)
161161
ddp_find_unused_parameters: bool = True
162162

163+
# torch.compile mode for the training networks ("default", "reduce-overhead", "max-autotune")
164+
# applied in apply_torch_compile. None disables torch.compile.
165+
torch_compile_mode: Optional[str] = None
166+
163167
# precision variables (choose from "float64", "float32", "bfloat16", or "float16")
164168
# (precision of the time steps is handled in the noise scheduler, defaulting to float64 for numerical stability)
165169

fastgen/methods/model.py

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424

2525

2626
class FastGenModel(torch.nn.Module):
27+
# Preprocessor sub-objects of ``net`` (handled specially for device/dtype placement
28+
# in on_train_begin and for torch.compile in apply_torch_compile). Single source of truth.
29+
_PREPROCESSOR_ATTRS = ("vae", "text_encoder", "image_encoder")
30+
2731
def __init__(self, config: BaseModelConfig):
2832
"""FastGenModel class for implementing training interface for all fastgen networks.
2933
@@ -264,6 +268,50 @@ def build_model(self):
264268
if hasattr(self.net, "init_preprocessors") and self.config.enable_preprocessors:
265269
self.net.init_preprocessors()
266270

271+
def apply_torch_compile(self):
272+
"""Compile the training networks in place with torch.compile.
273+
274+
Called by the trainer after DDP/FSDP wrapping (and after the networks
275+
have been moved to their device in ``on_train_begin``) so torch.compile
276+
composes with the distributed wrappers. No-op when
277+
``config.torch_compile_mode`` is None.
278+
279+
The modules compiled are those in model_dict (e.g. net, plus
280+
fake_score/discriminator for DMD2) minus the EMA networks, plus the
281+
teacher (if any, cf. fsdp_dict) and the net's preprocessors.
282+
"""
283+
mode = self.config.torch_compile_mode
284+
if mode is None:
285+
return
286+
287+
# model_dict contains the trainable networks (incl. EMA); EMA networks are
288+
# weight-averaged copies that aren't run during training, so drop them.
289+
# None entries arise when cleanup_unused_modules has already been called.
290+
modules = {name: net for name, net in self.model_dict.items() if name not in self.ema_dict and net is not None}
291+
# The teacher is not part of model_dict; add it when present (cf. fsdp_dict).
292+
if getattr(self, "teacher", None) is not None:
293+
modules["teacher"] = self.teacher
294+
# Preprocessors (VAE, text/image encoders) are often lightweight wrappers
295+
# (e.g. WanVideoEncoder, SDVAE) that are not nn.Modules themselves but hold
296+
# the actual nn.Module under an attribute; compile those submodules too.
297+
for name in self._PREPROCESSOR_ATTRS:
298+
obj = getattr(self.net, name, None)
299+
if obj is None:
300+
continue
301+
if isinstance(obj, torch.nn.Module):
302+
modules[name] = obj
303+
else:
304+
for attr, submodule in getattr(obj, "__dict__", {}).items():
305+
if isinstance(submodule, torch.nn.Module):
306+
modules[f"{name}.{attr}"] = submodule
307+
308+
for name, module in modules.items():
309+
if getattr(module, "_compiled_call_impl", None) is not None:
310+
logger.info(f"Skipping torch.compile for {name} (already compiled)")
311+
continue
312+
logger.info(f"Applying torch.compile (mode={mode}) to {name}")
313+
module.compile(mode=mode)
314+
267315
def on_train_begin(self, is_fsdp=False):
268316
self._is_fsdp = is_fsdp # Store for later use (e.g., to skip EMA during inference)
269317
ctx = dict(dtype=self.precision, device=self.device)
@@ -306,15 +354,11 @@ def on_train_begin(self, is_fsdp=False):
306354
# For networks that don't need gradients, we always manually handle casting and device management
307355
if hasattr(self.net, "init_preprocessors") and self.config.enable_preprocessors:
308356
logger.debug(f"Starting moving preprocessors to context: {ctx}.")
309-
if hasattr(self.net, "vae"):
310-
self.net.vae.to(**ctx)
311-
synchronize()
312-
if hasattr(self.net, "text_encoder"):
313-
self.net.text_encoder.to(**ctx)
314-
synchronize()
315-
if hasattr(self.net, "image_encoder"):
316-
self.net.image_encoder.to(**ctx)
317-
synchronize()
357+
for name in self._PREPROCESSOR_ATTRS:
358+
preprocessor = getattr(self.net, name, None)
359+
if preprocessor is not None:
360+
preprocessor.to(**ctx)
361+
synchronize()
318362
logger.debug(f"Completed moving preprocessors to context: {ctx}.")
319363

320364
synchronize()

fastgen/trainer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ def run(
118118
logger.info("FSDP wrapping completed")
119119
else:
120120
model_ddp = model
121+
122+
# Compile networks after DDP/FSDP wrapping so torch.compile composes
123+
# with the distributed wrappers (no-op if torch_compile_mode is None).
124+
model.apply_torch_compile()
125+
121126
self.callbacks.on_model_init_end(model_ddp)
122127
synchronize()
123128

scripts/inference/image_model_inference.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def main(args, config: BaseConfig):
127127
# Remove unused modules to free memory
128128
cleanup_unused_modules(model, args.do_teacher_sampling)
129129

130-
# Set up inference modules
130+
# Set up inference modules (also calls apply_torch_compile internally)
131131
teacher, student, vae = setup_inference_modules(
132132
model, config, args.do_teacher_sampling, args.do_student_sampling, model.precision
133133
)

scripts/inference/inference_utils.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,22 @@ def load_checkpoint(
124124

125125

126126
def cleanup_unused_modules(model: Any, do_teacher_sampling: bool) -> None:
127-
"""Remove unused modules to free memory.
127+
"""Free GPU memory held by modules that are not needed for inference.
128+
129+
Sets attributes to None rather than deleting them so that model_dict
130+
property accesses (which read e.g. self.fake_score) return None instead
131+
of raising AttributeError.
128132
129133
Args:
130134
model: Model to clean up
131135
do_teacher_sampling: Whether teacher sampling will be performed
132136
"""
133137
if hasattr(model, "fake_score"):
134-
del model.fake_score
138+
model.fake_score = None
135139
if hasattr(model, "discriminator"):
136-
del model.discriminator
140+
model.discriminator = None
137141
if (not do_teacher_sampling) and hasattr(model, "teacher"):
138-
del model.teacher
142+
model.teacher = None
139143

140144

141145
def setup_inference_modules(
@@ -177,6 +181,8 @@ def setup_inference_modules(
177181
vae = model.net.vae
178182
vae.to(device=model.device, dtype=precision)
179183

184+
model.apply_torch_compile() # no-op if torch_compile_mode is None; must run after init_preprocessors
185+
180186
return teacher, student, vae
181187

182188

scripts/inference/video_model_inference.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ def main(args, config: BaseConfig):
567567
# Remove unused modules
568568
cleanup_unused_modules(model, args.do_teacher_sampling)
569569

570-
# Get precision and set up inference modules
570+
# Get precision and set up inference modules (also calls apply_torch_compile internally)
571571
teacher, student, vae = setup_inference_modules(
572572
model, config, args.do_teacher_sampling, args.do_student_sampling, model.precision
573573
)

tests/test_torch_compile.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import gc
5+
import torch
6+
import pytest
7+
from fastgen.methods import DMD2Model
8+
from fastgen.configs.methods.config_sft import ModelConfig as SFTModelConfig
9+
from fastgen.configs.methods.config_dmd2 import ModelConfig as DMD2ModelConfig
10+
from fastgen.configs.config_utils import override_config_with_opts
11+
from fastgen.methods.fine_tuning.sft import SFTModel
12+
13+
14+
def _is_compiled(module):
15+
# nn.Module.compile() compiles the module in place: it stores the compiled
16+
# callable on `_compiled_call_impl` rather than replacing the module.
17+
return getattr(module, "_compiled_call_impl", None) is not None
18+
19+
20+
@pytest.fixture
21+
def sft_model_compiled():
22+
gc.collect()
23+
instance = SFTModelConfig()
24+
opts = ["-", "img_resolution=8", "channel_mult=[1]", "channel_mult_noise=1", "r_timestep=False"]
25+
instance.net = override_config_with_opts(instance.net, opts)
26+
instance.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
27+
instance.precision = "float32" if instance.device == torch.device("cpu") else "bfloat16"
28+
instance.pretrained_model_path = ""
29+
instance.input_shape = [3, 8, 8]
30+
instance.torch_compile_mode = "default"
31+
instance.cond_dropout_prob = 0.1
32+
instance.cond_keys_no_dropout = []
33+
instance.guidance_scale = None
34+
model = SFTModel(instance)
35+
# Mirror the trainer order: on_train_begin() moves parameters to device/dtype and
36+
# initialises preprocessors; apply_torch_compile() must come after so that
37+
# preprocessors exist and are on the right device when compiled.
38+
model.on_train_begin()
39+
model.apply_torch_compile()
40+
return model
41+
42+
43+
@pytest.fixture
44+
def sft_model_not_compiled():
45+
gc.collect()
46+
instance = SFTModelConfig()
47+
opts = ["-", "img_resolution=8", "channel_mult=[1]", "channel_mult_noise=1", "r_timestep=False"]
48+
instance.net = override_config_with_opts(instance.net, opts)
49+
instance.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
50+
instance.precision = "float32" if instance.device == torch.device("cpu") else "bfloat16"
51+
instance.pretrained_model_path = ""
52+
instance.input_shape = [3, 8, 8]
53+
instance.torch_compile_mode = None
54+
instance.cond_dropout_prob = 0.1
55+
instance.cond_keys_no_dropout = []
56+
instance.guidance_scale = None
57+
model = SFTModel(instance)
58+
model.on_train_begin()
59+
model.apply_torch_compile()
60+
return model
61+
62+
63+
@pytest.fixture
64+
def dmd2_model_compiled():
65+
gc.collect()
66+
instance = DMD2ModelConfig()
67+
opts = ["-", "img_resolution=8", "channel_mult=[1]", "channel_mult_noise=1"]
68+
instance.net = override_config_with_opts(instance.net, opts)
69+
opts_discriminator = ["-", "feature_indices=[0]", "all_res=[8]", "in_channels=128"]
70+
instance.discriminator = override_config_with_opts(instance.discriminator, opts_discriminator)
71+
instance.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
72+
instance.precision = "float32" if instance.device == torch.device("cpu") else "bfloat16"
73+
instance.pretrained_model_path = ""
74+
instance.student_update_freq = 2
75+
instance.input_shape = [3, 8, 8]
76+
instance.torch_compile_mode = "default"
77+
model = DMD2Model(instance)
78+
model.on_train_begin()
79+
model.apply_torch_compile()
80+
return model
81+
82+
83+
@pytest.fixture
84+
def dmd2_model_not_compiled():
85+
gc.collect()
86+
instance = DMD2ModelConfig()
87+
opts = ["-", "img_resolution=8", "channel_mult=[1]", "channel_mult_noise=1"]
88+
instance.net = override_config_with_opts(instance.net, opts)
89+
opts_discriminator = ["-", "feature_indices=[0]", "all_res=[8]", "in_channels=128"]
90+
instance.discriminator = override_config_with_opts(instance.discriminator, opts_discriminator)
91+
instance.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
92+
instance.precision = "float32" if instance.device == torch.device("cpu") else "bfloat16"
93+
instance.pretrained_model_path = ""
94+
instance.student_update_freq = 2
95+
instance.input_shape = [3, 8, 8]
96+
instance.torch_compile_mode = None
97+
model = DMD2Model(instance)
98+
model.on_train_begin()
99+
model.apply_torch_compile()
100+
return model
101+
102+
103+
def test_default_torch_compile_mode_is_none():
104+
from fastgen.configs.config import BaseModelConfig
105+
106+
config = BaseModelConfig()
107+
assert config.torch_compile_mode is None
108+
109+
110+
def test_sft_compile_enabled(sft_model_compiled):
111+
assert _is_compiled(sft_model_compiled.net)
112+
113+
114+
def test_sft_compile_disabled(sft_model_not_compiled):
115+
assert not _is_compiled(sft_model_not_compiled.net)
116+
117+
118+
def test_dmd2_compile_enabled(dmd2_model_compiled):
119+
# apply_torch_compile draws from model_dict (net, fake_score, discriminator) plus the teacher.
120+
assert _is_compiled(dmd2_model_compiled.net)
121+
assert _is_compiled(dmd2_model_compiled.teacher)
122+
assert _is_compiled(dmd2_model_compiled.fake_score)
123+
assert _is_compiled(dmd2_model_compiled.discriminator)
124+
125+
126+
def test_dmd2_compile_disabled(dmd2_model_not_compiled):
127+
assert not _is_compiled(dmd2_model_not_compiled.net)
128+
assert not _is_compiled(dmd2_model_not_compiled.teacher)
129+
assert not _is_compiled(dmd2_model_not_compiled.fake_score)
130+
assert not _is_compiled(dmd2_model_not_compiled.discriminator)
131+
132+
133+
def test_compile_excludes_ema(sft_model_not_compiled):
134+
# EMA networks live in model_dict but are weight-averaged copies that are not run
135+
# during training, so apply_torch_compile must not compile them.
136+
model = sft_model_not_compiled
137+
model.use_ema = ["ema"]
138+
model.ema = torch.nn.Linear(4, 4) # any nn.Module suffices for ema_dict/model_dict
139+
assert "ema" in model.ema_dict and "ema" in model.model_dict
140+
141+
model.config.torch_compile_mode = "default"
142+
model.apply_torch_compile()
143+
assert _is_compiled(model.net)
144+
assert not _is_compiled(model.ema)
145+
146+
147+
def test_compile_discovers_preprocessor_submodules(sft_model_not_compiled):
148+
# Preprocessor wrappers (VAE, text/image encoders) are not nn.Modules themselves but
149+
# hold the actual nn.Module under an attribute; apply_torch_compile must find and
150+
# compile those submodules.
151+
model = sft_model_not_compiled
152+
153+
class _DummyVAEWrapper: # mimics WanVideoEncoder/SDVAE (not an nn.Module)
154+
def __init__(self):
155+
self.vae = torch.nn.Linear(4, 4)
156+
self.scaling_factor = 0.18 # non-module attributes are ignored
157+
158+
model.net.vae = _DummyVAEWrapper()
159+
# An attribute that is itself an nn.Module is compiled directly under its own name.
160+
model.net.text_encoder = torch.nn.Linear(4, 4)
161+
162+
model.config.torch_compile_mode = "default"
163+
model.apply_torch_compile()
164+
assert _is_compiled(model.net.vae.vae)
165+
assert _is_compiled(model.net.text_encoder)
166+
167+
168+
def test_sft_compiled_train_step(sft_model_compiled):
169+
model = sft_model_compiled
170+
model.init_optimizers()
171+
172+
batch_size = 1
173+
labels = torch.nn.functional.one_hot(torch.randint(0, 10, (batch_size,)), num_classes=10).float()
174+
data = {
175+
"real": torch.randn(batch_size, 3, 8, 8).to(model.device, model.precision),
176+
"condition": labels.to(model.device, model.precision),
177+
"neg_condition": torch.zeros(batch_size, 10).to(model.device, model.precision),
178+
}
179+
180+
loss_map, _ = model.single_train_step(data, 0)
181+
assert "total_loss" in loss_map
182+
assert not torch.isnan(loss_map["total_loss"])
183+
loss_map["total_loss"].backward()
184+
185+
186+
def test_dmd2_compiled_train_step(dmd2_model_compiled):
187+
model = dmd2_model_compiled
188+
model.init_optimizers()
189+
190+
batch_size = 1
191+
labels = torch.nn.functional.one_hot(torch.randint(0, 10, (batch_size,)), num_classes=10)
192+
data = {
193+
"real": torch.randn(batch_size, 3, 8, 8).to(model.device, model.precision),
194+
"condition": labels.to(model.device, model.precision),
195+
"neg_condition": torch.zeros(batch_size, 10).to(model.device, model.precision),
196+
}
197+
198+
# Student update step
199+
loss_map, _ = model.single_train_step(data, 0)
200+
assert "total_loss" in loss_map
201+
assert not torch.isnan(loss_map["total_loss"])
202+
203+
# Fake score update step
204+
model.optimizers_zero_grad(1)
205+
loss_map, _ = model.single_train_step(data, 1)
206+
assert "total_loss" in loss_map
207+
assert not torch.isnan(loss_map["total_loss"])

0 commit comments

Comments
 (0)