Skip to content

Commit e9a3776

Browse files
committed
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 82a8fbf commit e9a3776

6 files changed

Lines changed: 270 additions & 9 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: 49 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,46 @@ 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+
modules = {name: net for name, net in self.model_dict.items() if name not in self.ema_dict}
290+
# The teacher is not part of model_dict; add it when present (cf. fsdp_dict).
291+
if getattr(self, "teacher", None) is not None:
292+
modules["teacher"] = self.teacher
293+
# Preprocessors (VAE, text/image encoders) are often lightweight wrappers
294+
# (e.g. WanVideoEncoder, SDVAE) that are not nn.Modules themselves but hold
295+
# the actual nn.Module under an attribute; compile those submodules too.
296+
for name in self._PREPROCESSOR_ATTRS:
297+
obj = getattr(self.net, name, None)
298+
if obj is None:
299+
continue
300+
if isinstance(obj, torch.nn.Module):
301+
modules[name] = obj
302+
else:
303+
for attr, submodule in getattr(obj, "__dict__", {}).items():
304+
if isinstance(submodule, torch.nn.Module):
305+
modules[f"{name}.{attr}"] = submodule
306+
307+
for name, module in modules.items():
308+
logger.info(f"Applying torch.compile (mode={mode}) to {name}")
309+
module.compile(mode=mode)
310+
267311
def on_train_begin(self, is_fsdp=False):
268312
self._is_fsdp = is_fsdp # Store for later use (e.g., to skip EMA during inference)
269313
ctx = dict(dtype=self.precision, device=self.device)
@@ -306,15 +350,11 @@ def on_train_begin(self, is_fsdp=False):
306350
# For networks that don't need gradients, we always manually handle casting and device management
307351
if hasattr(self.net, "init_preprocessors") and self.config.enable_preprocessors:
308352
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()
353+
for name in self._PREPROCESSOR_ATTRS:
354+
preprocessor = getattr(self.net, name, None)
355+
if preprocessor is not None:
356+
preprocessor.to(**ctx)
357+
synchronize()
318358
logger.debug(f"Completed moving preprocessors to context: {ctx}.")
319359

320360
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ def main(args, config: BaseConfig):
124124
logger.info(f"image_save_dir: {save_dir}")
125125
save_dir = Path(save_dir) / prompt_name
126126

127+
# Optionally compile the networks; no-op unless config.model.torch_compile_mode is set.
128+
# Must run before cleanup_unused_modules, which deletes modules that model_dict references.
129+
# torch.compile is lazy, so the device placement in setup_inference_modules is still applied.
130+
model.apply_torch_compile()
131+
127132
# Remove unused modules to free memory
128133
cleanup_unused_modules(model, args.do_teacher_sampling)
129134

scripts/inference/video_model_inference.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,11 @@ def main(args, config: BaseConfig):
564564
}
565565
save_dir = save_dir.parent / (save_dir.name + "_hq")
566566

567+
# Optionally compile the networks; no-op unless config.model.torch_compile_mode is set.
568+
# Must run before cleanup_unused_modules, which deletes modules that model_dict references.
569+
# torch.compile is lazy, so the device placement in setup_inference_modules is still applied.
570+
model.apply_torch_compile()
571+
567572
# Remove unused modules
568573
cleanup_unused_modules(model, args.do_teacher_sampling)
569574

tests/test_torch_compile.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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+
# Compilation is applied by the trainer after DDP/FSDP wrapping; emulate that here.
36+
model.apply_torch_compile()
37+
return model
38+
39+
40+
@pytest.fixture
41+
def sft_model_not_compiled():
42+
gc.collect()
43+
instance = SFTModelConfig()
44+
opts = ["-", "img_resolution=8", "channel_mult=[1]", "channel_mult_noise=1", "r_timestep=False"]
45+
instance.net = override_config_with_opts(instance.net, opts)
46+
instance.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
47+
instance.precision = "float32" if instance.device == torch.device("cpu") else "bfloat16"
48+
instance.pretrained_model_path = ""
49+
instance.input_shape = [3, 8, 8]
50+
instance.torch_compile_mode = None
51+
instance.cond_dropout_prob = 0.1
52+
instance.cond_keys_no_dropout = []
53+
instance.guidance_scale = None
54+
model = SFTModel(instance)
55+
model.apply_torch_compile()
56+
return model
57+
58+
59+
@pytest.fixture
60+
def dmd2_model_compiled():
61+
gc.collect()
62+
instance = DMD2ModelConfig()
63+
opts = ["-", "img_resolution=8", "channel_mult=[1]", "channel_mult_noise=1"]
64+
instance.net = override_config_with_opts(instance.net, opts)
65+
opts_discriminator = ["-", "feature_indices=[0]", "all_res=[8]", "in_channels=128"]
66+
instance.discriminator = override_config_with_opts(instance.discriminator, opts_discriminator)
67+
instance.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
68+
instance.precision = "float32" if instance.device == torch.device("cpu") else "bfloat16"
69+
instance.pretrained_model_path = ""
70+
instance.student_update_freq = 2
71+
instance.input_shape = [3, 8, 8]
72+
instance.torch_compile_mode = "default"
73+
model = DMD2Model(instance)
74+
model.apply_torch_compile()
75+
return model
76+
77+
78+
@pytest.fixture
79+
def dmd2_model_not_compiled():
80+
gc.collect()
81+
instance = DMD2ModelConfig()
82+
opts = ["-", "img_resolution=8", "channel_mult=[1]", "channel_mult_noise=1"]
83+
instance.net = override_config_with_opts(instance.net, opts)
84+
opts_discriminator = ["-", "feature_indices=[0]", "all_res=[8]", "in_channels=128"]
85+
instance.discriminator = override_config_with_opts(instance.discriminator, opts_discriminator)
86+
instance.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
87+
instance.precision = "float32" if instance.device == torch.device("cpu") else "bfloat16"
88+
instance.pretrained_model_path = ""
89+
instance.student_update_freq = 2
90+
instance.input_shape = [3, 8, 8]
91+
instance.torch_compile_mode = None
92+
model = DMD2Model(instance)
93+
model.apply_torch_compile()
94+
return model
95+
96+
97+
def test_default_torch_compile_mode_is_none():
98+
from fastgen.configs.config import BaseModelConfig
99+
100+
config = BaseModelConfig()
101+
assert config.torch_compile_mode is None
102+
103+
104+
def test_sft_compile_enabled(sft_model_compiled):
105+
assert _is_compiled(sft_model_compiled.net)
106+
107+
108+
def test_sft_compile_disabled(sft_model_not_compiled):
109+
assert not _is_compiled(sft_model_not_compiled.net)
110+
111+
112+
def test_dmd2_compile_enabled(dmd2_model_compiled):
113+
# apply_torch_compile draws from model_dict (net, fake_score, discriminator) plus the teacher.
114+
assert _is_compiled(dmd2_model_compiled.net)
115+
assert _is_compiled(dmd2_model_compiled.teacher)
116+
assert _is_compiled(dmd2_model_compiled.fake_score)
117+
assert _is_compiled(dmd2_model_compiled.discriminator)
118+
119+
120+
def test_dmd2_compile_disabled(dmd2_model_not_compiled):
121+
assert not _is_compiled(dmd2_model_not_compiled.net)
122+
assert not _is_compiled(dmd2_model_not_compiled.teacher)
123+
assert not _is_compiled(dmd2_model_not_compiled.fake_score)
124+
125+
126+
def test_compile_excludes_ema(sft_model_not_compiled):
127+
# EMA networks live in model_dict but are weight-averaged copies that are not run
128+
# during training, so apply_torch_compile must not compile them.
129+
model = sft_model_not_compiled
130+
model.use_ema = ["ema"]
131+
model.ema = torch.nn.Linear(4, 4) # any nn.Module suffices for ema_dict/model_dict
132+
assert "ema" in model.ema_dict and "ema" in model.model_dict
133+
134+
model.config.torch_compile_mode = "default"
135+
model.apply_torch_compile()
136+
assert _is_compiled(model.net)
137+
assert not _is_compiled(model.ema)
138+
139+
140+
def test_compile_discovers_preprocessor_submodules(sft_model_not_compiled):
141+
# Preprocessor wrappers (VAE, text/image encoders) are not nn.Modules themselves but
142+
# hold the actual nn.Module under an attribute; apply_torch_compile must find and
143+
# compile those submodules.
144+
model = sft_model_not_compiled
145+
146+
class _DummyVAEWrapper: # mimics WanVideoEncoder/SDVAE (not an nn.Module)
147+
def __init__(self):
148+
self.vae = torch.nn.Linear(4, 4)
149+
self.scaling_factor = 0.18 # non-module attributes are ignored
150+
151+
model.net.vae = _DummyVAEWrapper()
152+
# An attribute that is itself an nn.Module is compiled directly under its own name.
153+
model.net.text_encoder = torch.nn.Linear(4, 4)
154+
155+
model.config.torch_compile_mode = "default"
156+
model.apply_torch_compile()
157+
assert _is_compiled(model.net.vae.vae)
158+
assert _is_compiled(model.net.text_encoder)
159+
160+
161+
def test_sft_compiled_train_step(sft_model_compiled):
162+
model = sft_model_compiled
163+
model.on_train_begin()
164+
model.init_optimizers()
165+
166+
batch_size = 1
167+
labels = torch.nn.functional.one_hot(torch.randint(0, 10, (batch_size,)), num_classes=10).float()
168+
data = {
169+
"real": torch.randn(batch_size, 3, 8, 8).to(model.device, model.precision),
170+
"condition": labels.to(model.device, model.precision),
171+
"neg_condition": torch.zeros(batch_size, 10).to(model.device, model.precision),
172+
}
173+
174+
loss_map, outputs = model.single_train_step(data, 0)
175+
assert "total_loss" in loss_map
176+
assert not torch.isnan(loss_map["total_loss"])
177+
loss_map["total_loss"].backward()
178+
179+
180+
def test_dmd2_compiled_train_step(dmd2_model_compiled):
181+
model = dmd2_model_compiled
182+
model.on_train_begin()
183+
model.init_optimizers()
184+
185+
batch_size = 1
186+
labels = torch.nn.functional.one_hot(torch.randint(0, 10, (batch_size,)), num_classes=10)
187+
data = {
188+
"real": torch.randn(batch_size, 3, 8, 8).to(model.device, model.precision),
189+
"condition": labels.to(model.device, model.precision),
190+
"neg_condition": torch.zeros(batch_size, 10).to(model.device, model.precision),
191+
}
192+
193+
# Student update step
194+
loss_map, outputs = model.single_train_step(data, 0)
195+
assert "total_loss" in loss_map
196+
assert not torch.isnan(loss_map["total_loss"])
197+
198+
# Fake score update step
199+
model.optimizers_zero_grad(1)
200+
loss_map, outputs = model.single_train_step(data, 1)
201+
assert "total_loss" in loss_map
202+
assert not torch.isnan(loss_map["total_loss"])

0 commit comments

Comments
 (0)