neonforestmist commited on
Commit
a8c48b5
·
verified ·
1 Parent(s): 88a1a24

Add Modal training and inpainting model scaffold

Browse files
inpainting/README.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: diffusers
3
+ pipeline_tag: image-inpainting
4
+ base_model: neonforestmist/Clover-Image-Tiny
5
+ license: creativeml-openrail-m
6
+ tags:
7
+ - clover-image
8
+ - inpainting
9
+ - stable-diffusion
10
+ - coreml
11
+ - iphone
12
+ ---
13
+
14
+ # Clover Image Tiny Inpaint 🍀
15
+
16
+ An inpainting adaptation of Clover Image Tiny for 512×512 local generation and
17
+ on-device Core ML deployment. White mask pixels are regenerated; black pixels
18
+ are preserved.
19
+
20
+ The model uses a 9-channel U-Net input:
21
+
22
+ ```text
23
+ [noisy latent (4), mask (1), masked-image latent (4)]
24
+ ```
25
+
26
+ The base text encoder, VAE, scheduler, safety checker, and tokenizer remain
27
+ compatible with Clover Image Tiny. The inpainting export additionally includes
28
+ the VAE encoder needed to prepare the masked-image latent on iPhone.
29
+
30
+ ## Diffusers
31
+
32
+ ```python
33
+ from diffusers import AutoPipelineForInpainting
34
+ from diffusers.utils import load_image
35
+
36
+ pipe = AutoPipelineForInpainting.from_pretrained(
37
+ "neonforestmist/Clover-Image-Tiny-Inpaint",
38
+ torch_dtype="auto",
39
+ )
40
+ image = pipe(
41
+ prompt="a tiny glass greenhouse glowing in a moonlit garden",
42
+ image=load_image("input.png"),
43
+ mask_image=load_image("mask.png"),
44
+ num_inference_steps=30,
45
+ ).images[0]
46
+ image.save("clover-inpaint.png")
47
+ ```
48
+
49
+ ## Core ML and iPhone 15
50
+
51
+ The companion Core ML resource bundle is converted for iOS 18 with a
52
+ batch-one U-Net and chunked U-Net resources. The Swift runtime performs
53
+ classifier-free guidance as two serial passes to reduce peak memory. The
54
+ bundled `VAEEncoder.mlmodelc` creates the masked-image latent locally, so the
55
+ input image and mask do not leave the device.
56
+
57
+ Conversion and the native iOS integration live in the source Clover repo:
58
+
59
+ - [`coreml-tools/convert_inpaint.sh`](https://huggingface.co/neonforestmist/Clover-Image-Tiny/blob/main/coreml-tools/convert_inpaint.sh)
60
+ - [`Clover-iOS`](https://huggingface.co/neonforestmist/Clover-Image-Tiny/tree/main/Clover-iOS)
61
+ - [`training/README-INPAINTING.md`](https://huggingface.co/neonforestmist/Clover-Image-Tiny/blob/main/training/README-INPAINTING.md)
62
+
63
+ ## Training provenance
64
+
65
+ Training uses synthetic rectangle, ellipse, and brush masks over the pinned
66
+ Apache-2.0 `prithivMLmods/Caption3o-Opt` image-caption dataset. The full job is
67
+ launched by Modal under the `guccichungus69` workspace and stores its output in
68
+ the `clover-image-tiny-inpaint-output` Volume before Core ML conversion.
inpainting/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """Clover Image Tiny inpainting training and export helpers."""
2
+
3
+ __all__ = ["model", "masks"]
inpainting/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (241 Bytes). View file
 
inpainting/__pycache__/masks.cpython-311.pyc ADDED
Binary file (3.88 kB). View file
 
inpainting/__pycache__/model.cpython-311.pyc ADDED
Binary file (2.92 kB). View file
 
inpainting/__pycache__/train.cpython-311.pyc ADDED
Binary file (19.4 kB). View file
 
inpainting/config.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "base_model": "neonforestmist/Clover-Image-Tiny",
3
+ "base_revision": "63b0e9f6be9c00888ff464f342a9ef052bf76681",
4
+ "dataset": "prithivMLmods/Caption3o-Opt",
5
+ "dataset_revision": "17e893f785fcd3f5d6fc4a5d65a914b9f7b1ff5b",
6
+ "dataset_split": "train",
7
+ "image_column": "image",
8
+ "caption_column": "caption",
9
+ "resolution": 512,
10
+ "unet_in_channels": 9,
11
+ "mask_semantics": "white=regenerate, black=preserve",
12
+ "training": {
13
+ "steps": 4000,
14
+ "learning_rate": 0.00001,
15
+ "warmup_steps": 200,
16
+ "train_batch_size": 1,
17
+ "gradient_accumulation_steps": 4,
18
+ "mixed_precision": "fp16",
19
+ "gradient_checkpointing": true,
20
+ "mask_min_area": 0.12,
21
+ "mask_max_area": 0.55
22
+ },
23
+ "modal": {
24
+ "profile": "guccichungus69",
25
+ "gpu": "A10G",
26
+ "timeout_hours": 4,
27
+ "estimated_hourly_usd": 1.0
28
+ }
29
+ }
inpainting/masks.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Synthetic mask generation used for inpainting fine-tuning."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+
7
+ from PIL import Image, ImageChops, ImageDraw
8
+
9
+
10
+ def random_mask(
11
+ size: tuple[int, int],
12
+ rng: random.Random,
13
+ *,
14
+ min_area: float = 0.12,
15
+ max_area: float = 0.55,
16
+ ) -> Image.Image:
17
+ """Return a black/white L mask where white means "paint this area"."""
18
+
19
+ width, height = size
20
+ mask = Image.new("L", size, 0)
21
+ draw = ImageDraw.Draw(mask)
22
+ shape = rng.choice(("rectangle", "ellipse", "brush"))
23
+
24
+ target_area = rng.uniform(min_area, max_area) * width * height
25
+ if shape in {"rectangle", "ellipse"}:
26
+ aspect = rng.uniform(0.45, 2.2)
27
+ box_width = max(8, int((target_area * aspect) ** 0.5))
28
+ box_height = max(8, int((target_area / aspect) ** 0.5))
29
+ box_width = min(box_width, width)
30
+ box_height = min(box_height, height)
31
+ left = rng.randint(0, max(0, width - box_width))
32
+ top = rng.randint(0, max(0, height - box_height))
33
+ box = (left, top, left + box_width, top + box_height)
34
+ if shape == "rectangle":
35
+ draw.rectangle(box, fill=255)
36
+ else:
37
+ draw.ellipse(box, fill=255)
38
+ else:
39
+ # A few overlapping strokes cover irregular object-shaped regions while
40
+ # staying cheap enough to generate inside a DataLoader worker.
41
+ stroke_width = max(8, int(min(width, height) * rng.uniform(0.06, 0.18)))
42
+ points = [
43
+ (
44
+ rng.randint(0, width - 1),
45
+ rng.randint(0, height - 1),
46
+ )
47
+ for _ in range(rng.randint(2, 5))
48
+ ]
49
+ draw.line(points, fill=255, width=stroke_width, joint="curve")
50
+ radius = stroke_width // 2
51
+ for x, y in points:
52
+ draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=255)
53
+
54
+ return mask
55
+
56
+
57
+ def apply_mask(image: Image.Image, mask: Image.Image) -> Image.Image:
58
+ """Black out the pixels that the inpainting model must regenerate."""
59
+
60
+ image = image.convert("RGB")
61
+ keep = ImageChops.invert(mask.convert("L"))
62
+ return Image.composite(image, Image.new("RGB", image.size), keep)
inpainting/model.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model construction helpers for Clover Image Tiny inpainting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import torch
9
+ from diffusers import UNet2DConditionModel
10
+
11
+
12
+ def _load_kwargs(revision: str | None) -> dict[str, Any]:
13
+ return {"revision": revision} if revision else {}
14
+
15
+
16
+ def make_inpainting_unet(
17
+ base_model: str | Path,
18
+ *,
19
+ revision: str | None = None,
20
+ zero_initialize_conditioning: bool = True,
21
+ ) -> UNet2DConditionModel:
22
+ """Create a 9-channel U-Net from the 4-channel Clover checkpoint.
23
+
24
+ The first four input channels retain Clover's original weights. The extra
25
+ channels receive the binary inpainting mask and the masked-image latent.
26
+ Zero-initializing them preserves a stable text-to-image starting point while
27
+ the inpainting fine-tune learns how to use the new conditioning channels.
28
+ """
29
+
30
+ load_kwargs = _load_kwargs(revision)
31
+ base = UNet2DConditionModel.from_pretrained(
32
+ str(base_model),
33
+ subfolder="unet",
34
+ low_cpu_mem_usage=False,
35
+ **load_kwargs,
36
+ )
37
+ config = dict(base.config)
38
+ config["in_channels"] = 9
39
+ inpaint = UNet2DConditionModel.from_config(config)
40
+
41
+ state = base.state_dict()
42
+ conv_in_weight = state.pop("conv_in.weight")
43
+ inpaint.load_state_dict(state, strict=False)
44
+
45
+ with torch.no_grad():
46
+ if zero_initialize_conditioning:
47
+ inpaint.conv_in.weight.zero_()
48
+ inpaint.conv_in.weight[:, :4].copy_(conv_in_weight)
49
+ else:
50
+ # Keep the pretrained channels and leave the five new channels at
51
+ # the framework's default initialization.
52
+ inpaint.conv_in.weight[:, :4].copy_(conv_in_weight)
53
+ if "conv_in.bias" in base.state_dict():
54
+ inpaint.conv_in.bias.copy_(base.conv_in.bias)
55
+
56
+ del base
57
+ return inpaint
inpainting/train.py ADDED
@@ -0,0 +1,368 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Fine-tune Clover Image Tiny into a 9-channel inpainting pipeline.
3
+
4
+ The dataset only needs an image column and a text-caption column. Masks are
5
+ generated on the fly, so one image produces many distinct training examples.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import random
13
+ import shutil
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import torch
18
+ import torch.nn.functional as F
19
+ from accelerate import Accelerator
20
+ from datasets import load_dataset
21
+ from diffusers import (
22
+ AutoencoderKL,
23
+ DDPMScheduler,
24
+ StableDiffusionInpaintPipeline,
25
+ get_scheduler,
26
+ )
27
+ from PIL import Image
28
+ from transformers import CLIPTextModel, CLIPTokenizer
29
+ from torchvision import transforms
30
+
31
+ from inpainting.masks import apply_mask, random_mask
32
+ from inpainting.model import make_inpainting_unet
33
+
34
+
35
+ def parse_args() -> argparse.Namespace:
36
+ parser = argparse.ArgumentParser()
37
+ parser.add_argument("--pretrained_model_name_or_path", required=True)
38
+ parser.add_argument("--revision")
39
+ parser.add_argument("--dataset_name", required=True)
40
+ parser.add_argument("--dataset_config_name")
41
+ parser.add_argument("--dataset_revision")
42
+ parser.add_argument("--dataset_split", default="train")
43
+ parser.add_argument("--image_column", default="image")
44
+ parser.add_argument("--caption_column", default="caption")
45
+ parser.add_argument("--resolution", type=int, default=512)
46
+ parser.add_argument("--train_batch_size", type=int, default=1)
47
+ parser.add_argument("--max_train_steps", type=int, default=4000)
48
+ parser.add_argument("--learning_rate", type=float, default=1e-5)
49
+ parser.add_argument("--lr_scheduler", default="cosine")
50
+ parser.add_argument("--lr_warmup_steps", type=int, default=200)
51
+ parser.add_argument("--gradient_accumulation_steps", type=int, default=4)
52
+ parser.add_argument("--gradient_checkpointing", action="store_true")
53
+ parser.add_argument("--mixed_precision", choices=("no", "fp16", "bf16"), default="fp16")
54
+ parser.add_argument("--seed", type=int, default=20260810)
55
+ parser.add_argument("--mask_min_area", type=float, default=0.12)
56
+ parser.add_argument("--mask_max_area", type=float, default=0.55)
57
+ parser.add_argument("--max_train_samples", type=int)
58
+ parser.add_argument("--output_dir", type=Path, required=True)
59
+ parser.add_argument("--validation_prompt")
60
+ parser.add_argument("--push_to_hub", action="store_true")
61
+ parser.add_argument("--hub_model_id")
62
+ return parser.parse_args()
63
+
64
+
65
+ def model_kwargs(revision: str | None) -> dict[str, Any]:
66
+ return {"revision": revision} if revision else {}
67
+
68
+
69
+ def image_to_tensor(image: Image.Image, resolution: int) -> torch.Tensor:
70
+ transform = transforms.Compose(
71
+ [
72
+ transforms.Resize(resolution, interpolation=transforms.InterpolationMode.BILINEAR),
73
+ transforms.CenterCrop(resolution),
74
+ transforms.ToTensor(),
75
+ transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]),
76
+ ]
77
+ )
78
+ return transform(image.convert("RGB"))
79
+
80
+
81
+ def make_collate_fn(
82
+ *,
83
+ tokenizer: CLIPTokenizer,
84
+ resolution: int,
85
+ min_area: float,
86
+ max_area: float,
87
+ seed: int,
88
+ ):
89
+ worker_rng = random.Random(seed)
90
+
91
+ def collate(examples: list[dict[str, Any]]) -> dict[str, Any]:
92
+ images: list[torch.Tensor] = []
93
+ masked_images: list[torch.Tensor] = []
94
+ masks: list[torch.Tensor] = []
95
+ captions: list[str] = []
96
+
97
+ for example in examples:
98
+ image = example["image"]
99
+ if not isinstance(image, Image.Image):
100
+ image = Image.fromarray(image)
101
+ image = image.convert("RGB")
102
+ image = transforms.Resize(
103
+ resolution,
104
+ interpolation=transforms.InterpolationMode.BILINEAR,
105
+ )(image)
106
+ image = transforms.CenterCrop(resolution)(image)
107
+ mask = random_mask(
108
+ (resolution, resolution),
109
+ worker_rng,
110
+ min_area=min_area,
111
+ max_area=max_area,
112
+ )
113
+ masked = apply_mask(image, mask)
114
+
115
+ images.append(image_to_tensor(image, resolution))
116
+ masked_images.append(image_to_tensor(masked, resolution))
117
+ mask_tensor = torch.from_numpy(
118
+ __import__("numpy").array(mask, dtype="float32") / 255.0
119
+ ).unsqueeze(0)
120
+ masks.append(mask_tensor)
121
+ caption = example["caption"]
122
+ if isinstance(caption, list):
123
+ caption = caption[0] if caption else ""
124
+ captions.append(str(caption or ""))
125
+
126
+ tokenized = tokenizer(
127
+ captions,
128
+ max_length=tokenizer.model_max_length,
129
+ padding="max_length",
130
+ truncation=True,
131
+ return_tensors="pt",
132
+ )
133
+ return {
134
+ "pixel_values": torch.stack(images),
135
+ "masked_pixel_values": torch.stack(masked_images),
136
+ "mask": torch.stack(masks),
137
+ "input_ids": tokenized.input_ids,
138
+ }
139
+
140
+ return collate
141
+
142
+
143
+ def save_pipeline(
144
+ *,
145
+ base_model: str,
146
+ revision: str | None,
147
+ unet,
148
+ output_dir: Path,
149
+ ) -> None:
150
+ output_dir.mkdir(parents=True, exist_ok=True)
151
+ pipeline = StableDiffusionInpaintPipeline.from_pretrained(
152
+ base_model,
153
+ revision=revision,
154
+ unet=unet,
155
+ )
156
+ pipeline.save_pretrained(output_dir, safe_serialization=True)
157
+ (output_dir / "inpainting-config.json").write_text(
158
+ json.dumps(
159
+ {
160
+ "unet_in_channels": 9,
161
+ "mask_semantics": "white=regenerate, black=preserve",
162
+ "base_model": base_model,
163
+ "base_revision": revision,
164
+ },
165
+ indent=2,
166
+ )
167
+ + "\n"
168
+ )
169
+
170
+
171
+ def main() -> None:
172
+ args = parse_args()
173
+ if not 0.0 < args.mask_min_area < args.mask_max_area < 1.0:
174
+ raise ValueError("mask area bounds must satisfy 0 < min < max < 1")
175
+ if args.push_to_hub and not args.hub_model_id:
176
+ raise ValueError("--hub_model_id is required with --push_to_hub")
177
+
178
+ accelerator = Accelerator(
179
+ gradient_accumulation_steps=args.gradient_accumulation_steps,
180
+ mixed_precision=args.mixed_precision,
181
+ )
182
+ accelerator.init_trackers("clover-image-tiny-inpaint")
183
+ torch.manual_seed(args.seed)
184
+ random.seed(args.seed)
185
+
186
+ kwargs = model_kwargs(args.revision)
187
+ dataset = load_dataset(
188
+ args.dataset_name,
189
+ args.dataset_config_name,
190
+ split=args.dataset_split,
191
+ revision=args.dataset_revision,
192
+ )
193
+ if args.max_train_samples:
194
+ dataset = dataset.select(range(min(args.max_train_samples, len(dataset))))
195
+ if args.image_column not in dataset.column_names:
196
+ raise ValueError(f"Missing image column {args.image_column!r}: {dataset.column_names}")
197
+ if args.caption_column not in dataset.column_names:
198
+ raise ValueError(f"Missing caption column {args.caption_column!r}: {dataset.column_names}")
199
+ dataset = dataset.rename_columns(
200
+ {args.image_column: "image", args.caption_column: "caption"}
201
+ )
202
+
203
+ tokenizer = CLIPTokenizer.from_pretrained(
204
+ args.pretrained_model_name_or_path,
205
+ subfolder="tokenizer",
206
+ **kwargs,
207
+ )
208
+ text_encoder = CLIPTextModel.from_pretrained(
209
+ args.pretrained_model_name_or_path,
210
+ subfolder="text_encoder",
211
+ **kwargs,
212
+ )
213
+ vae = AutoencoderKL.from_pretrained(
214
+ args.pretrained_model_name_or_path,
215
+ subfolder="vae",
216
+ **kwargs,
217
+ )
218
+ unet = make_inpainting_unet(
219
+ args.pretrained_model_name_or_path,
220
+ revision=args.revision,
221
+ )
222
+ noise_scheduler = DDPMScheduler.from_pretrained(
223
+ args.pretrained_model_name_or_path,
224
+ subfolder="scheduler",
225
+ **kwargs,
226
+ )
227
+
228
+ if args.gradient_checkpointing:
229
+ unet.enable_gradient_checkpointing()
230
+ text_encoder.requires_grad_(False)
231
+ vae.requires_grad_(False)
232
+ text_encoder.eval()
233
+ vae.eval()
234
+
235
+ collate_fn = make_collate_fn(
236
+ tokenizer=tokenizer,
237
+ resolution=args.resolution,
238
+ min_area=args.mask_min_area,
239
+ max_area=args.mask_max_area,
240
+ seed=args.seed,
241
+ )
242
+ dataloader = torch.utils.data.DataLoader(
243
+ dataset,
244
+ shuffle=True,
245
+ collate_fn=collate_fn,
246
+ batch_size=args.train_batch_size,
247
+ num_workers=2,
248
+ pin_memory=True,
249
+ )
250
+ optimizer = torch.optim.AdamW(
251
+ unet.parameters(),
252
+ lr=args.learning_rate,
253
+ betas=(0.9, 0.999),
254
+ weight_decay=1e-2,
255
+ eps=1e-8,
256
+ )
257
+ lr_scheduler = get_scheduler(
258
+ args.lr_scheduler,
259
+ optimizer=optimizer,
260
+ num_warmup_steps=args.lr_warmup_steps * accelerator.num_processes,
261
+ num_training_steps=args.max_train_steps * accelerator.num_processes,
262
+ )
263
+
264
+ unet, optimizer, dataloader, lr_scheduler = accelerator.prepare(
265
+ unet, optimizer, dataloader, lr_scheduler
266
+ )
267
+ weight_dtype = torch.float32
268
+ if accelerator.mixed_precision == "fp16":
269
+ weight_dtype = torch.float16
270
+ elif accelerator.mixed_precision == "bf16":
271
+ weight_dtype = torch.bfloat16
272
+ vae.to(accelerator.device, dtype=weight_dtype)
273
+ text_encoder.to(accelerator.device, dtype=weight_dtype)
274
+
275
+ global_step = 0
276
+ data_iterator = iter(dataloader)
277
+ while global_step < args.max_train_steps:
278
+ try:
279
+ batch = next(data_iterator)
280
+ except StopIteration:
281
+ data_iterator = iter(dataloader)
282
+ batch = next(data_iterator)
283
+
284
+ with accelerator.accumulate(unet):
285
+ pixel_values = batch["pixel_values"].to(
286
+ accelerator.device, dtype=weight_dtype, non_blocking=True
287
+ )
288
+ masked_pixel_values = batch["masked_pixel_values"].to(
289
+ accelerator.device, dtype=weight_dtype, non_blocking=True
290
+ )
291
+ mask = batch["mask"].to(
292
+ accelerator.device, dtype=weight_dtype, non_blocking=True
293
+ )
294
+ with torch.no_grad():
295
+ latents = vae.encode(pixel_values).latent_dist.sample()
296
+ latents = latents * vae.config.scaling_factor
297
+ masked_latents = vae.encode(masked_pixel_values).latent_dist.sample()
298
+ masked_latents = masked_latents * vae.config.scaling_factor
299
+ encoder_hidden_states = text_encoder(batch["input_ids"].to(accelerator.device))[0]
300
+
301
+ noise = torch.randn_like(latents)
302
+ timesteps = torch.randint(
303
+ 0,
304
+ noise_scheduler.config.num_train_timesteps,
305
+ (latents.shape[0],),
306
+ device=latents.device,
307
+ ).long()
308
+ noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps)
309
+ mask = F.interpolate(mask, size=latents.shape[-2:], mode="nearest")
310
+ model_input = torch.cat([noisy_latents, mask, masked_latents], dim=1)
311
+ model_pred = unet(
312
+ model_input,
313
+ timesteps,
314
+ encoder_hidden_states=encoder_hidden_states,
315
+ ).sample
316
+
317
+ if noise_scheduler.config.prediction_type == "epsilon":
318
+ target = noise
319
+ elif noise_scheduler.config.prediction_type == "v_prediction":
320
+ target = noise_scheduler.get_velocity(latents, noise, timesteps)
321
+ else:
322
+ raise ValueError(
323
+ f"Unsupported prediction type: {noise_scheduler.config.prediction_type}"
324
+ )
325
+ loss = F.mse_loss(model_pred.float(), target.float(), reduction="mean")
326
+ accelerator.backward(loss)
327
+ if accelerator.sync_gradients:
328
+ accelerator.clip_grad_norm_(unet.parameters(), 1.0)
329
+ optimizer.step()
330
+ lr_scheduler.step()
331
+ optimizer.zero_grad(set_to_none=True)
332
+
333
+ if accelerator.sync_gradients:
334
+ global_step += 1
335
+ if accelerator.is_main_process and global_step % 50 == 0:
336
+ accelerator.print(
337
+ f"step={global_step}/{args.max_train_steps} "
338
+ f"loss={loss.detach().item():.4f} "
339
+ f"lr={lr_scheduler.get_last_lr()[0]:.3e}"
340
+ )
341
+ accelerator.log(
342
+ {"train_loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]},
343
+ step=global_step,
344
+ )
345
+
346
+ accelerator.wait_for_everyone()
347
+ if accelerator.is_main_process:
348
+ unwrapped = accelerator.unwrap_model(unet).cpu()
349
+ save_pipeline(
350
+ base_model=args.pretrained_model_name_or_path,
351
+ revision=args.revision,
352
+ unet=unwrapped,
353
+ output_dir=args.output_dir,
354
+ )
355
+ summary = vars(args).copy()
356
+ summary["output_dir"] = str(args.output_dir)
357
+ (args.output_dir / "training-summary.json").write_text(
358
+ json.dumps(summary, indent=2, default=str) + "\n"
359
+ )
360
+ if args.push_to_hub:
361
+ pipeline = StableDiffusionInpaintPipeline.from_pretrained(args.output_dir)
362
+ pipeline.push_to_hub(args.hub_model_id)
363
+ accelerator.print(f"Saved Clover Image Tiny Inpaint to {args.output_dir}")
364
+ accelerator.end_training()
365
+
366
+
367
+ if __name__ == "__main__":
368
+ main()