Repository navigation
Expand file tree
/
Copy pathmps_runtime.py
More file actions
167 lines (139 loc) · 7.18 KB
/
Copy pathmps_runtime.py
File metadata and controls
167 lines (139 loc) · 7.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
"""Apple-silicon (MPS) loading that keeps one large model on the GPU at a time.
The Qwen text encoder (~17 GB) and the DiT (~10 GB) do not fit in a 32 GB Mac's
GPU working set together. The text encoder is loaded from disk only to encode a
new prompt and is released before sampling; the DiT then comes back. Prompt
embeddings are cached, so a new seed, size or step count skips the text encoder.
"""
from __future__ import annotations
import gc
import importlib.util
import os
import sys
from collections import OrderedDict
from pathlib import Path
import torch
from diffusers import AutoencoderKLFlux2
from huggingface_hub import snapshot_download
from transformers import AutoModel, AutoTokenizer
DEVICE = "mps"
CACHED_PROMPTS = 8
# MOSS_MPS_DIT_DTYPE=float16 runs the DiT in fp16, which M1/M2 GPUs compute natively.
DIT_DTYPE = getattr(torch, os.environ.get("MOSS_MPS_DIT_DTYPE", "bfloat16"), None)
if DIT_DTYPE not in (torch.float16, torch.bfloat16):
raise ValueError("MOSS_MPS_DIT_DTYPE must be float16 or bfloat16.")
# MPS has no float64. The DiT computes its timestep sinusoid in float64, so
# that one calculation runs on the CPU with the same precision.
_FLOAT64_TIMESTEP = (
" freq = 10000 ** (-torch.arange(half, device=times.device, dtype=torch.float64) / half)\n"
" phase = times.double()[:, None] * freq\n"
" embedding = torch.cat((phase.cos(), phase.sin()), -1).float()\n"
)
_CPU_TIMESTEP = (
" freq = 10000 ** (-torch.arange(half, dtype=torch.float64) / half)\n"
" phase = times.cpu().double()[:, None] * freq\n"
" embedding = torch.cat((phase.cos(), phase.sin()), -1).float().to(times.device)\n"
)
def _wait_without_gil(original):
# torch.mps.empty_cache() and synchronize() wait on the GPU while holding the GIL.
# After a non_blocking copy from a CPU tensor whose storage belongs to a Python object
# (safetensors, frombuffer), Metal's completion handler needs the GIL to release it, so
# the two deadlock; Diffusers does exactly this when loading with device_map.
# Copying a value back to the CPU waits with the GIL released and drains them first.
def wait(*args, **kwargs):
torch.zeros(1, device=DEVICE).cpu()
return original(*args, **kwargs)
return wait
if torch.backends.mps.is_available():
torch.mps.empty_cache = _wait_without_gil(torch.mps.empty_cache)
torch.mps.synchronize = _wait_without_gil(torch.mps.synchronize)
def is_available():
return not torch.cuda.is_available() and torch.backends.mps.is_available()
def empty_cache():
gc.collect()
if torch.backends.mps.is_available():
torch.mps.empty_cache()
def _bundle_root(model, local_files_only):
path = Path(model)
if path.is_dir():
return path
return Path(snapshot_download(model, local_files_only=local_files_only))
def _import_pipeline_module(root):
path = root / "pipeline_moss_image.py"
source = path.read_text()
if _FLOAT64_TIMESTEP not in source:
raise ValueError(f"{path} changed; update the float64 patch in mps_runtime.py.")
name = "pipeline_moss_image_mps"
module = importlib.util.module_from_spec(importlib.util.spec_from_loader(name, loader=None, origin=str(path)))
sys.modules[name] = module
exec(compile(source.replace(_FLOAT64_TIMESTEP, _CPU_TIMESTEP), str(path), "exec"), module.__dict__)
return module
def _staged_pipeline_class(module):
class StagedMOSSImagePipeline(module.MOSSImagePipeline):
"""Swaps the text encoder and the DiT on the GPU instead of holding both."""
@property
def _execution_device(self):
return torch.device(DEVICE)
def _release(self, name):
if getattr(self, name) is not None:
setattr(self, name, None)
empty_cache()
def use_text_encoder(self):
if self.text_encoder is None:
self._release("transformer")
self.text_encoder = AutoModel.from_pretrained(
self.root / "text_encoder", dtype=torch.bfloat16, device_map=DEVICE
).eval()
def use_transformer(self):
if self.transformer is None:
self._release("text_encoder")
self.transformer = module.MOSSImageTransformer.from_pretrained(
self.root / "transformer", torch_dtype=DIT_DTYPE, device_map=DEVICE
).eval()
def encode_prompt(self, prompt, max_sequence_length=4096):
key = ((prompt,) if isinstance(prompt, str) else tuple(prompt), max_sequence_length)
if key not in self.embeddings:
self.use_text_encoder()
# An autograd graph would keep the released text encoder alive in the cache.
with torch.no_grad():
self.embeddings[key] = super().encode_prompt(prompt, max_sequence_length)
while len(self.embeddings) > CACHED_PROMPTS:
self.embeddings.popitem(last=False)
self.embeddings.move_to_end(key)
return self.embeddings[key]
def __call__(self, prompt=None, *, negative_prompt="", guidance_scale=3.0, max_sequence_length=4096,
prompt_embeds=None, prompt_attention_mask=None,
negative_prompt_embeds=None, negative_prompt_attention_mask=None, **kwargs):
# Encode both prompts before the DiT returns to the GPU.
if prompt is not None and prompt_embeds is None:
prompt_embeds, prompt_attention_mask = self.encode_prompt(prompt, max_sequence_length)
prompt = None
if guidance_scale != 1.0 and negative_prompt_embeds is None and prompt_embeds is not None:
batch = prompt_embeds.shape[0]
negatives = [negative_prompt] * batch if isinstance(negative_prompt, str) else negative_prompt
negative_prompt_embeds, negative_prompt_attention_mask = self.encode_prompt(
negatives, max_sequence_length
)
self.use_transformer()
return super().__call__(
prompt, negative_prompt=negative_prompt, guidance_scale=guidance_scale,
max_sequence_length=max_sequence_length,
prompt_embeds=prompt_embeds, prompt_attention_mask=prompt_attention_mask,
negative_prompt_embeds=negative_prompt_embeds,
negative_prompt_attention_mask=negative_prompt_attention_mask, **kwargs,
)
return StagedMOSSImagePipeline
def load_pipeline(model, local_files_only=False):
root = _bundle_root(model, local_files_only)
module = _import_pipeline_module(root)
pipe = _staged_pipeline_class(module)(
transformer=None,
scheduler=module.MOSSImageScheduler.from_pretrained(root / "scheduler"),
tokenizer=AutoTokenizer.from_pretrained(root / "tokenizer"),
vae=AutoencoderKLFlux2.from_pretrained(root / "vae", torch_dtype=torch.float32).to(DEVICE).eval(),
)
pipe.root = root
# The pipeline class is defined here; provenance records the bundle source it patches and runs.
pipe.pipeline_file = root / "pipeline_moss_image.py"
pipe.embeddings = OrderedDict()
pipe.use_transformer()
return pipe