-
Notifications
You must be signed in to change notification settings - Fork 71
Add torch_compile flag for training networks #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e9a3776
d66243f
4a9614f
cf2216f
06949f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,10 @@ | |
|
|
||
|
|
||
| class FastGenModel(torch.nn.Module): | ||
| # Preprocessor sub-objects of ``net`` (handled specially for device/dtype placement | ||
| # in on_train_begin and for torch.compile in apply_torch_compile). Single source of truth. | ||
| _PREPROCESSOR_ATTRS = ("vae", "text_encoder", "image_encoder") | ||
|
|
||
| def __init__(self, config: BaseModelConfig): | ||
| """FastGenModel class for implementing training interface for all fastgen networks. | ||
|
|
||
|
|
@@ -264,6 +268,50 @@ def build_model(self): | |
| if hasattr(self.net, "init_preprocessors") and self.config.enable_preprocessors: | ||
| self.net.init_preprocessors() | ||
|
|
||
| def apply_torch_compile(self): | ||
| """Compile the training networks in place with torch.compile. | ||
|
|
||
| Called by the trainer after DDP/FSDP wrapping (and after the networks | ||
| have been moved to their device in ``on_train_begin``) so torch.compile | ||
| composes with the distributed wrappers. No-op when | ||
| ``config.torch_compile_mode`` is None. | ||
|
|
||
| The modules compiled are those in model_dict (e.g. net, plus | ||
| fake_score/discriminator for DMD2) minus the EMA networks, plus the | ||
| teacher (if any, cf. fsdp_dict) and the net's preprocessors. | ||
| """ | ||
| mode = self.config.torch_compile_mode | ||
| if mode is None: | ||
| return | ||
|
|
||
| # model_dict contains the trainable networks (incl. EMA); EMA networks are | ||
| # weight-averaged copies that aren't run during training, so drop them. | ||
| # None entries arise when cleanup_unused_modules has already been called. | ||
| modules = {name: net for name, net in self.model_dict.items() if name not in self.ema_dict and net is not None} | ||
| # The teacher is not part of model_dict; add it when present (cf. fsdp_dict). | ||
| if getattr(self, "teacher", None) is not None: | ||
| modules["teacher"] = self.teacher | ||
| # Preprocessors (VAE, text/image encoders) are often lightweight wrappers | ||
| # (e.g. WanVideoEncoder, SDVAE) that are not nn.Modules themselves but hold | ||
| # the actual nn.Module under an attribute; compile those submodules too. | ||
| for name in self._PREPROCESSOR_ATTRS: | ||
| obj = getattr(self.net, name, None) | ||
| if obj is None: | ||
| continue | ||
| if isinstance(obj, torch.nn.Module): | ||
| modules[name] = obj | ||
| else: | ||
| for attr, submodule in getattr(obj, "__dict__", {}).items(): | ||
| if isinstance(submodule, torch.nn.Module): | ||
| modules[f"{name}.{attr}"] = submodule | ||
|
|
||
| for name, module in modules.items(): | ||
| if getattr(module, "_compiled_call_impl", None) is not None: | ||
| logger.info(f"Skipping torch.compile for {name} (already compiled)") | ||
| continue | ||
| logger.info(f"Applying torch.compile (mode={mode}) to {name}") | ||
| module.compile(mode=mode) | ||
|
Comment on lines
+271
to
+313
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @juliusberner thank you for pointing it out, i have fixed both comments |
||
|
|
||
| def on_train_begin(self, is_fsdp=False): | ||
| self._is_fsdp = is_fsdp # Store for later use (e.g., to skip EMA during inference) | ||
| ctx = dict(dtype=self.precision, device=self.device) | ||
|
|
@@ -306,15 +354,11 @@ def on_train_begin(self, is_fsdp=False): | |
| # For networks that don't need gradients, we always manually handle casting and device management | ||
| if hasattr(self.net, "init_preprocessors") and self.config.enable_preprocessors: | ||
| logger.debug(f"Starting moving preprocessors to context: {ctx}.") | ||
| if hasattr(self.net, "vae"): | ||
| self.net.vae.to(**ctx) | ||
| synchronize() | ||
| if hasattr(self.net, "text_encoder"): | ||
| self.net.text_encoder.to(**ctx) | ||
| synchronize() | ||
| if hasattr(self.net, "image_encoder"): | ||
| self.net.image_encoder.to(**ctx) | ||
| synchronize() | ||
| for name in self._PREPROCESSOR_ATTRS: | ||
| preprocessor = getattr(self.net, name, None) | ||
| if preprocessor is not None: | ||
| preprocessor.to(**ctx) | ||
| synchronize() | ||
| logger.debug(f"Completed moving preprocessors to context: {ctx}.") | ||
|
|
||
| synchronize() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import gc | ||
| import torch | ||
| import pytest | ||
| from fastgen.methods import DMD2Model | ||
| from fastgen.configs.methods.config_sft import ModelConfig as SFTModelConfig | ||
| from fastgen.configs.methods.config_dmd2 import ModelConfig as DMD2ModelConfig | ||
| from fastgen.configs.config_utils import override_config_with_opts | ||
| from fastgen.methods.fine_tuning.sft import SFTModel | ||
|
|
||
|
|
||
| def _is_compiled(module): | ||
| # nn.Module.compile() compiles the module in place: it stores the compiled | ||
| # callable on `_compiled_call_impl` rather than replacing the module. | ||
| return getattr(module, "_compiled_call_impl", None) is not None | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def sft_model_compiled(): | ||
| gc.collect() | ||
| instance = SFTModelConfig() | ||
| opts = ["-", "img_resolution=8", "channel_mult=[1]", "channel_mult_noise=1", "r_timestep=False"] | ||
| instance.net = override_config_with_opts(instance.net, opts) | ||
| instance.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | ||
| instance.precision = "float32" if instance.device == torch.device("cpu") else "bfloat16" | ||
| instance.pretrained_model_path = "" | ||
| instance.input_shape = [3, 8, 8] | ||
| instance.torch_compile_mode = "default" | ||
| instance.cond_dropout_prob = 0.1 | ||
| instance.cond_keys_no_dropout = [] | ||
| instance.guidance_scale = None | ||
| model = SFTModel(instance) | ||
| # Mirror the trainer order: on_train_begin() moves parameters to device/dtype and | ||
| # initialises preprocessors; apply_torch_compile() must come after so that | ||
| # preprocessors exist and are on the right device when compiled. | ||
| model.on_train_begin() | ||
| model.apply_torch_compile() | ||
| return model | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def sft_model_not_compiled(): | ||
| gc.collect() | ||
| instance = SFTModelConfig() | ||
| opts = ["-", "img_resolution=8", "channel_mult=[1]", "channel_mult_noise=1", "r_timestep=False"] | ||
| instance.net = override_config_with_opts(instance.net, opts) | ||
| instance.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | ||
| instance.precision = "float32" if instance.device == torch.device("cpu") else "bfloat16" | ||
| instance.pretrained_model_path = "" | ||
| instance.input_shape = [3, 8, 8] | ||
| instance.torch_compile_mode = None | ||
| instance.cond_dropout_prob = 0.1 | ||
| instance.cond_keys_no_dropout = [] | ||
| instance.guidance_scale = None | ||
| model = SFTModel(instance) | ||
| model.on_train_begin() | ||
| model.apply_torch_compile() | ||
| return model | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def dmd2_model_compiled(): | ||
| gc.collect() | ||
| instance = DMD2ModelConfig() | ||
| opts = ["-", "img_resolution=8", "channel_mult=[1]", "channel_mult_noise=1"] | ||
| instance.net = override_config_with_opts(instance.net, opts) | ||
| opts_discriminator = ["-", "feature_indices=[0]", "all_res=[8]", "in_channels=128"] | ||
| instance.discriminator = override_config_with_opts(instance.discriminator, opts_discriminator) | ||
| instance.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | ||
| instance.precision = "float32" if instance.device == torch.device("cpu") else "bfloat16" | ||
| instance.pretrained_model_path = "" | ||
| instance.student_update_freq = 2 | ||
| instance.input_shape = [3, 8, 8] | ||
| instance.torch_compile_mode = "default" | ||
| model = DMD2Model(instance) | ||
| model.on_train_begin() | ||
| model.apply_torch_compile() | ||
| return model | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def dmd2_model_not_compiled(): | ||
| gc.collect() | ||
| instance = DMD2ModelConfig() | ||
| opts = ["-", "img_resolution=8", "channel_mult=[1]", "channel_mult_noise=1"] | ||
| instance.net = override_config_with_opts(instance.net, opts) | ||
| opts_discriminator = ["-", "feature_indices=[0]", "all_res=[8]", "in_channels=128"] | ||
| instance.discriminator = override_config_with_opts(instance.discriminator, opts_discriminator) | ||
| instance.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | ||
| instance.precision = "float32" if instance.device == torch.device("cpu") else "bfloat16" | ||
| instance.pretrained_model_path = "" | ||
| instance.student_update_freq = 2 | ||
| instance.input_shape = [3, 8, 8] | ||
| instance.torch_compile_mode = None | ||
| model = DMD2Model(instance) | ||
| model.on_train_begin() | ||
| model.apply_torch_compile() | ||
| return model | ||
|
|
||
|
|
||
| def test_default_torch_compile_mode_is_none(): | ||
| from fastgen.configs.config import BaseModelConfig | ||
|
|
||
| config = BaseModelConfig() | ||
| assert config.torch_compile_mode is None | ||
|
|
||
|
|
||
| def test_sft_compile_enabled(sft_model_compiled): | ||
| assert _is_compiled(sft_model_compiled.net) | ||
|
|
||
|
|
||
| def test_sft_compile_disabled(sft_model_not_compiled): | ||
| assert not _is_compiled(sft_model_not_compiled.net) | ||
|
|
||
|
|
||
| def test_dmd2_compile_enabled(dmd2_model_compiled): | ||
| # apply_torch_compile draws from model_dict (net, fake_score, discriminator) plus the teacher. | ||
| assert _is_compiled(dmd2_model_compiled.net) | ||
| assert _is_compiled(dmd2_model_compiled.teacher) | ||
| assert _is_compiled(dmd2_model_compiled.fake_score) | ||
| assert _is_compiled(dmd2_model_compiled.discriminator) | ||
|
|
||
|
|
||
| def test_dmd2_compile_disabled(dmd2_model_not_compiled): | ||
| assert not _is_compiled(dmd2_model_not_compiled.net) | ||
| assert not _is_compiled(dmd2_model_not_compiled.teacher) | ||
| assert not _is_compiled(dmd2_model_not_compiled.fake_score) | ||
| assert not _is_compiled(dmd2_model_not_compiled.discriminator) | ||
|
|
||
|
|
||
| def test_compile_excludes_ema(sft_model_not_compiled): | ||
| # EMA networks live in model_dict but are weight-averaged copies that are not run | ||
| # during training, so apply_torch_compile must not compile them. | ||
| model = sft_model_not_compiled | ||
| model.use_ema = ["ema"] | ||
| model.ema = torch.nn.Linear(4, 4) # any nn.Module suffices for ema_dict/model_dict | ||
| assert "ema" in model.ema_dict and "ema" in model.model_dict | ||
|
|
||
| model.config.torch_compile_mode = "default" | ||
| model.apply_torch_compile() | ||
| assert _is_compiled(model.net) | ||
| assert not _is_compiled(model.ema) | ||
|
|
||
|
|
||
| def test_compile_discovers_preprocessor_submodules(sft_model_not_compiled): | ||
| # Preprocessor wrappers (VAE, text/image encoders) are not nn.Modules themselves but | ||
| # hold the actual nn.Module under an attribute; apply_torch_compile must find and | ||
| # compile those submodules. | ||
| model = sft_model_not_compiled | ||
|
|
||
| class _DummyVAEWrapper: # mimics WanVideoEncoder/SDVAE (not an nn.Module) | ||
| def __init__(self): | ||
| self.vae = torch.nn.Linear(4, 4) | ||
| self.scaling_factor = 0.18 # non-module attributes are ignored | ||
|
|
||
| model.net.vae = _DummyVAEWrapper() | ||
| # An attribute that is itself an nn.Module is compiled directly under its own name. | ||
| model.net.text_encoder = torch.nn.Linear(4, 4) | ||
|
|
||
| model.config.torch_compile_mode = "default" | ||
| model.apply_torch_compile() | ||
| assert _is_compiled(model.net.vae.vae) | ||
| assert _is_compiled(model.net.text_encoder) | ||
|
|
||
|
|
||
| def test_sft_compiled_train_step(sft_model_compiled): | ||
| model = sft_model_compiled | ||
| model.init_optimizers() | ||
|
|
||
| batch_size = 1 | ||
| labels = torch.nn.functional.one_hot(torch.randint(0, 10, (batch_size,)), num_classes=10).float() | ||
| data = { | ||
| "real": torch.randn(batch_size, 3, 8, 8).to(model.device, model.precision), | ||
| "condition": labels.to(model.device, model.precision), | ||
| "neg_condition": torch.zeros(batch_size, 10).to(model.device, model.precision), | ||
| } | ||
|
|
||
| loss_map, _ = model.single_train_step(data, 0) | ||
| assert "total_loss" in loss_map | ||
| assert not torch.isnan(loss_map["total_loss"]) | ||
| loss_map["total_loss"].backward() | ||
|
|
||
|
|
||
| def test_dmd2_compiled_train_step(dmd2_model_compiled): | ||
| model = dmd2_model_compiled | ||
| model.init_optimizers() | ||
|
|
||
| batch_size = 1 | ||
| labels = torch.nn.functional.one_hot(torch.randint(0, 10, (batch_size,)), num_classes=10) | ||
| data = { | ||
| "real": torch.randn(batch_size, 3, 8, 8).to(model.device, model.precision), | ||
| "condition": labels.to(model.device, model.precision), | ||
| "neg_condition": torch.zeros(batch_size, 10).to(model.device, model.precision), | ||
| } | ||
|
|
||
| # Student update step | ||
| loss_map, _ = model.single_train_step(data, 0) | ||
| assert "total_loss" in loss_map | ||
| assert not torch.isnan(loss_map["total_loss"]) | ||
|
|
||
| # Fake score update step | ||
| model.optimizers_zero_grad(1) | ||
| loss_map, _ = model.single_train_step(data, 1) | ||
| assert "total_loss" in loss_map | ||
| assert not torch.isnan(loss_map["total_loss"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
apply_torch_compilealways excludes every name inema_dict, but at inference timesetup_inference_modules(line 175 ofinference_utils.py) picks the EMA network as the student:student = getattr(model, model.use_ema[0]) if model.use_ema else model.net. That EMA network is the one actually called for student sampling, yet it is never compiled here.model.netgets compiled but is not invoked; the EMA network that is invoked stays uncompiled. Any user who setstorch_compile_modeexpecting compiled inference will silently get no speedup whenuse_emais non-empty (the default for most production configs).The property
inference_net(line 729) confirms the same selection: it returnsself.use_ema[0]when EMA is active. Since the compile exclusion is designed for training semantics (EMA weights are updated by averaging, not by a forward pass), it should not unconditionally apply to the inference path. One option is to additionally compile whichever networkinference_netresolves to when called fromsetup_inference_modules.