`enable_model_cpu_offload` (#2285) · anagri/diffusers@2777264 · GitHub
Skip to content

Commit 2777264

Browse files
authored
enable_model_cpu_offload (huggingface#2285)
* enable_model_offload PoC It's surprisingly more involved than expected, see comments in the PR. * Rename final_offload_hook * Invoke the vae forward hook manually. * Completely remove decoder. * Style * apply_forward_hook decorator * Rename method. * Style * Copy enable_model_cpu_offload * Fix copies. * Remove comment. * Fix copies * Missing import * Fix doc-builder style. * Merge main and fix again. * Add docs * Fix docs. * Add a couple of tests. * style
1 parent 6eaebe8 commit 2777264

30 files changed

Lines changed: 595 additions & 32 deletions

‎docs/source/en/optimization/fp16.mdx‎

Lines changed: 57 additions & 1 deletion

‎src/diffusers/models/autoencoder_kl.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import torch.nn as nn
1919

2020
from ..configuration_utils import ConfigMixin, register_to_config
21-
from ..utils import BaseOutput
21+
from ..utils import BaseOutput, apply_forward_hook
2222
from .modeling_utils import ModelMixin
2323
from .vae import Decoder, DecoderOutput, DiagonalGaussianDistribution, Encoder
2424

@@ -109,6 +109,7 @@ def __init__(
109109
self.post_quant_conv = nn.Conv2d(latent_channels, latent_channels, 1)
110110
self.use_slicing = False
111111

112+
@apply_forward_hook
112113
def encode(self, x: torch.FloatTensor, return_dict: bool = True) -> AutoencoderKLOutput:
113114
h = self.encoder(x)
114115
moments = self.quant_conv(h)
@@ -144,6 +145,7 @@ def disable_slicing(self):
144145
"""
145146
self.use_slicing = False
146147

148+
@apply_forward_hook
147149
def decode(self, z: torch.FloatTensor, return_dict: bool = True) -> Union[DecoderOutput, torch.FloatTensor]:
148150
if self.use_slicing and z.shape[0] > 1:
149151
decoded_slices = [self._decode(z_slice).sample for z_slice in z.split(1)]

‎src/diffusers/pipelines/alt_diffusion/pipeline_alt_diffusion.py‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from packaging import version
2020
from transformers import CLIPFeatureExtractor, XLMRobertaTokenizer
2121

22-
from diffusers.utils import is_accelerate_available
22+
from diffusers.utils import is_accelerate_available, is_accelerate_version
2323

2424
from ...configuration_utils import FrozenDict
2525
from ...models import AutoencoderKL, UNet2DConditionModel
@@ -188,6 +188,8 @@ def enable_sequential_cpu_offload(self, gpu_id=0):
188188
Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet,
189189
text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a
190190
`torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called.
191+
Note that offloading happens on a submodule basis. Memory savings are higher than with
192+
`enable_model_cpu_offload`, but performance is lower.
191193
"""
192194
if is_accelerate_available():
193195
from accelerate import cpu_offload
@@ -202,14 +204,38 @@ def enable_sequential_cpu_offload(self, gpu_id=0):
202204
if self.safety_checker is not None:
203205
cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True)
204206

207+
def enable_model_cpu_offload(self, gpu_id=0):
208+
r"""
209+
Offloads all models to CPU using accelerate, reducing memory usage with a low impact on performance. Compared
210+
to `enable_sequential_cpu_offload`, this method moves one whole model at a time to the GPU when its `forward`
211+
method is called, and the model remains in GPU until the next model runs. Memory savings are lower than with
212+
`enable_sequential_cpu_offload`, but performance is much better due to the iterative execution of the `unet`.
213+
"""
214+
if is_accelerate_available() and is_accelerate_version(">=", "0.17.0.dev0"):
215+
from accelerate import cpu_offload_with_hook
216+
else:
217+
raise ImportError("`enable_model_offload` requires `accelerate v0.17.0` or higher.")
218+
219+
device = torch.device(f"cuda:{gpu_id}")
220+
221+
hook = None
222+
for cpu_offloaded_model in [self.text_encoder, self.unet, self.vae]:
223+
_, hook = cpu_offload_with_hook(cpu_offloaded_model, device, prev_module_hook=hook)
224+
225+
if self.safety_checker is not None:
226+
_, hook = cpu_offload_with_hook(self.safety_checker, device, prev_module_hook=hook)
227+
228+
# We'll offload the last model manually.
229+
self.final_offload_hook = hook
230+
205231
@property
206232
def _execution_device(self):
207233
r"""
208234
Returns the device on which the pipeline's models will be executed. After calling
209235
`pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module
210236
hooks.
211237
"""
212-
if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"):
238+
if not hasattr(self.unet, "_hf_hook"):
213239
return self.device
214240
for module in self.unet.modules():
215241
if (
@@ -651,6 +677,10 @@ def __call__(
651677
# 9. Run safety checker
652678
image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype)
653679

680+
# Offload last model to CPU
681+
if hasattr(self, "final_offload_hook") and self.final_offload_hook is not None:
682+
self.final_offload_hook.offload()
683+
654684
if not return_dict:
655685
return (image, has_nsfw_concept)
656686

‎src/diffusers/pipelines/alt_diffusion/pipeline_alt_diffusion_img2img.py‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from packaging import version
2222
from transformers import CLIPFeatureExtractor, XLMRobertaTokenizer
2323

24-
from diffusers.utils import is_accelerate_available
24+
from diffusers.utils import is_accelerate_available, is_accelerate_version
2525

2626
from ...configuration_utils import FrozenDict
2727
from ...models import AutoencoderKL, UNet2DConditionModel
@@ -210,6 +210,8 @@ def enable_sequential_cpu_offload(self, gpu_id=0):
210210
Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet,
211211
text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a
212212
`torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called.
213+
Note that offloading happens on a submodule basis. Memory savings are higher than with
214+
`enable_model_cpu_offload`, but performance is lower.
213215
"""
214216
if is_accelerate_available():
215217
from accelerate import cpu_offload
@@ -224,14 +226,38 @@ def enable_sequential_cpu_offload(self, gpu_id=0):
224226
if self.safety_checker is not None:
225227
cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True)
226228

229+
def enable_model_cpu_offload(self, gpu_id=0):
230+
r"""
231+
Offloads all models to CPU using accelerate, reducing memory usage with a low impact on performance. Compared
232+
to `enable_sequential_cpu_offload`, this method moves one whole model at a time to the GPU when its `forward`
233+
method is called, and the model remains in GPU until the next model runs. Memory savings are lower than with
234+
`enable_sequential_cpu_offload`, but performance is much better due to the iterative execution of the `unet`.
235+
"""
236+
if is_accelerate_available() and is_accelerate_version(">=", "0.17.0.dev0"):
237+
from accelerate import cpu_offload_with_hook
238+
else:
239+
raise ImportError("`enable_model_offload` requires `accelerate v0.17.0` or higher.")
240+
241+
device = torch.device(f"cuda:{gpu_id}")
242+
243+
hook = None
244+
for cpu_offloaded_model in [self.text_encoder, self.unet, self.vae]:
245+
_, hook = cpu_offload_with_hook(cpu_offloaded_model, device, prev_module_hook=hook)
246+
247+
if self.safety_checker is not None:
248+
_, hook = cpu_offload_with_hook(self.safety_checker, device, prev_module_hook=hook)
249+
250+
# We'll offload the last model manually.
251+
self.final_offload_hook = hook
252+
227253
@property
228254
def _execution_device(self):
229255
r"""
230256
Returns the device on which the pipeline's models will be executed. After calling
231257
`pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module
232258
hooks.
233259
"""
234-
if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"):
260+
if not hasattr(self.unet, "_hf_hook"):
235261
return self.device
236262
for module in self.unet.modules():
237263
if (
@@ -684,6 +710,10 @@ def __call__(
684710
if output_type == "pil":
685711
image = self.numpy_to_pil(image)
686712

713+
# Offload last model to CPU
714+
if hasattr(self, "final_offload_hook") and self.final_offload_hook is not None:
715+
self.final_offload_hook.offload()
716+
687717
if not return_dict:
688718
return (image, has_nsfw_concept)
689719

‎src/diffusers/pipelines/paint_by_example/pipeline_paint_by_example.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def _execution_device(self):
215215
`pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module
216216
hooks.
217217
"""
218-
if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"):
218+
if not hasattr(self.unet, "_hf_hook"):
219219
return self.device
220220
for module in self.unet.modules():
221221
if (

‎src/diffusers/pipelines/stable_diffusion/pipeline_cycle_diffusion.py‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from packaging import version
2222
from transformers import CLIPFeatureExtractor, CLIPTextModel, CLIPTokenizer
2323

24-
from diffusers.utils import is_accelerate_available
24+
from diffusers.utils import is_accelerate_available, is_accelerate_version
2525

2626
from ...configuration_utils import FrozenDict
2727
from ...models import AutoencoderKL, UNet2DConditionModel
@@ -227,6 +227,8 @@ def enable_sequential_cpu_offload(self, gpu_id=0):
227227
Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet,
228228
text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a
229229
`torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called.
230+
Note that offloading happens on a submodule basis. Memory savings are higher than with
231+
`enable_model_cpu_offload`, but performance is lower.
230232
"""
231233
if is_accelerate_available():
232234
from accelerate import cpu_offload
@@ -241,6 +243,31 @@ def enable_sequential_cpu_offload(self, gpu_id=0):
241243
if self.safety_checker is not None:
242244
cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True)
243245

246+
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline.enable_model_cpu_offload
247+
def enable_model_cpu_offload(self, gpu_id=0):
248+
r"""
249+
Offloads all models to CPU using accelerate, reducing memory usage with a low impact on performance. Compared
250+
to `enable_sequential_cpu_offload`, this method moves one whole model at a time to the GPU when its `forward`
251+
method is called, and the model remains in GPU until the next model runs. Memory savings are lower than with
252+
`enable_sequential_cpu_offload`, but performance is much better due to the iterative execution of the `unet`.
253+
"""
254+
if is_accelerate_available() and is_accelerate_version(">=", "0.17.0.dev0"):
255+
from accelerate import cpu_offload_with_hook
256+
else:
257+
raise ImportError("`enable_model_offload` requires `accelerate v0.17.0` or higher.")
258+
259+
device = torch.device(f"cuda:{gpu_id}")
260+
261+
hook = None
262+
for cpu_offloaded_model in [self.text_encoder, self.unet, self.vae]:
263+
_, hook = cpu_offload_with_hook(cpu_offloaded_model, device, prev_module_hook=hook)
264+
265+
if self.safety_checker is not None:
266+
_, hook = cpu_offload_with_hook(self.safety_checker, device, prev_module_hook=hook)
267+
268+
# We'll offload the last model manually.
269+
self.final_offload_hook = hook
270+
244271
@property
245272
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.StableDiffusionPipeline._execution_device
246273
def _execution_device(self):
@@ -249,7 +276,7 @@ def _execution_device(self):
249276
`pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module
250277
hooks.
251278
"""
252-
if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"):
279+
if not hasattr(self.unet, "_hf_hook"):
253280
return self.device
254281
for module in self.unet.modules():
255282
if (

‎src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@
2222
from ...configuration_utils import FrozenDict
2323
from ...models import AutoencoderKL, UNet2DConditionModel
2424
from ...schedulers import KarrasDiffusionSchedulers
25-
from ...utils import deprecate, is_accelerate_available, logging, randn_tensor, replace_example_docstring
25+
from ...utils import (
26+
deprecate,
27+
is_accelerate_available,
28+
is_accelerate_version,
29+
logging,
30+
randn_tensor,
31+
replace_example_docstring,
32+
)
2633
from ..pipeline_utils import DiffusionPipeline
2734
from . import StableDiffusionPipelineOutput
2835
from .safety_checker import StableDiffusionSafetyChecker
@@ -184,6 +191,8 @@ def enable_sequential_cpu_offload(self, gpu_id=0):
184191
Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet,
185192
text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a
186193
`torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called.
194+
Note that offloading happens on a submodule basis. Memory savings are higher than with
195+
`enable_model_cpu_offload`, but performance is lower.
187196
"""
188197
if is_accelerate_available():
189198
from accelerate import cpu_offload
@@ -198,14 +207,38 @@ def enable_sequential_cpu_offload(self, gpu_id=0):
198207
if self.safety_checker is not None:
199208
cpu_offload(self.safety_checker, execution_device=device, offload_buffers=True)
200209

210+
def enable_model_cpu_offload(self, gpu_id=0):
211+
r"""
212+
Offloads all models to CPU using accelerate, reducing memory usage with a low impact on performance. Compared
213+
to `enable_sequential_cpu_offload`, this method moves one whole model at a time to the GPU when its `forward`
214+
method is called, and the model remains in GPU until the next model runs. Memory savings are lower than with
215+
`enable_sequential_cpu_offload`, but performance is much better due to the iterative execution of the `unet`.
216+
"""
217+
if is_accelerate_available() and is_accelerate_version(">=", "0.17.0.dev0"):
218+
from accelerate import cpu_offload_with_hook
219+
else:
220+
raise ImportError("`enable_model_offload` requires `accelerate v0.17.0` or higher.")
221+
222+
device = torch.device(f"cuda:{gpu_id}")
223+
224+
hook = None
225+
for cpu_offloaded_model in [self.text_encoder, self.unet, self.vae]:
226+
_, hook = cpu_offload_with_hook(cpu_offloaded_model, device, prev_module_hook=hook)
227+
228+
if self.safety_checker is not None:
229+
_, hook = cpu_offload_with_hook(self.safety_checker, device, prev_module_hook=hook)
230+
231+
# We'll offload the last model manually.
232+
self.final_offload_hook = hook
233+
201234
@property
202235
def _execution_device(self):
203236
r"""
204237
Returns the device on which the pipeline's models will be executed. After calling
205238
`pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module
206239
hooks.
207240
"""
208-
if self.device != torch.device("meta") or not hasattr(self.unet, "_hf_hook"):
241+
if not hasattr(self.unet, "_hf_hook"):
209242
return self.device
210243
for module in self.unet.modules():
211244
if (
@@ -647,6 +680,10 @@ def __call__(
647680
# 9. Run safety checker
648681
image, has_nsfw_concept = self.run_safety_checker(image, device, prompt_embeds.dtype)
649682

683+
# Offload last model to CPU
684+
if hasattr(self, "final_offload_hook") and self.final_offload_hook is not None:
685+
self.final_offload_hook.offload()
686+
650687
if not return_dict:
651688
return (image, has_nsfw_concept)
652689

‎src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_depth2img.py‎

Lines changed: 1 addition & 1 deletion

0 commit comments

Comments
 (0)