ezhoureal commited on
Commit
9dbf60a
·
verified ·
1 Parent(s): b3052c4

Remove obsolete workspace code folder

Browse files
code/README.md DELETED
@@ -1,169 +0,0 @@
1
- # FLUX.1-dev LoRA Training Workspace
2
-
3
- This repo is a minimal local workspace for preparing a custom image dataset, launching a `FLUX.1-dev` LoRA run, and testing the resulting adapter.
4
-
5
- It uses a tracked local copy of Hugging Face's `diffusers` Flux LoRA trainer, patched for lower memory use, while keeping the project-specific logic here in this repo:
6
-
7
- - dataset normalization into a local `imagefolder` dataset
8
- - prompt and caption templating
9
- - reproducible training settings in `configs/flux_lora.toml`
10
- - a simple inference command for post-train validation
11
-
12
- ## Project layout
13
-
14
- - [configs/flux_lora.toml](/home/zireael/lora/configs/flux_lora.toml)
15
- - [src/lora/cli.py](/home/zireael/lora/src/lora/cli.py)
16
- - [scripts/train_dreambooth_lora_flux_lowmem.py](/home/zireael/lora/scripts/train_dreambooth_lora_flux_lowmem.py)
17
- - [dataset](/home/zireael/lora/dataset/)
18
-
19
- ## Why this setup
20
-
21
- Your dataset has two kinds of examples:
22
-
23
- - numbered source/output pairs like `1.jpg -> 1-output.png`
24
- - standalone keyword images that do not have matching source inputs
25
-
26
- `prepare-dataset` now preserves that split. Paired records include both the target image and its matching source image, while standalone keyword images are prepared as target-only samples.
27
-
28
- ## 1. Prerequisites
29
-
30
- You need:
31
-
32
- - Python 3.11+
33
- - a CUDA-capable GPU
34
- - a Hugging Face account
35
- - accepted access to `black-forest-labs/FLUX.1-dev`
36
-
37
- Install and bootstrap:
38
-
39
- ```bash
40
- uv sync
41
- uv pip install -e .
42
- source .venv/bin/activate
43
- lora install
44
- ```
45
-
46
- Then authenticate and configure Accelerate:
47
-
48
- ```bash
49
- hf auth login
50
- accelerate config default
51
- ```
52
-
53
- ## 2. Prepare the dataset
54
-
55
- This will:
56
-
57
- - convert all supported images in `dataset/` to RGB PNGs
58
- - write target images into `training_data/flux_aura_style/train/`
59
- - write paired source inputs into `training_data/flux_aura_style/conditioning/`
60
- - create `training_data/flux_aura_style/train/metadata.jsonl`
61
-
62
- Run:
63
-
64
- ```bash
65
- lora prepare-dataset
66
- ```
67
-
68
- The prompt template now defaults to:
69
-
70
- ```text
71
- A strong colorful light is illuminating {subject}'s silhouette. Medium shot. The image is rendered in a smooth gradient of luminous light, giving a radiant and ethereal appearance. Subtle contour lighting highlights delicate outlines, adding depth and a refined, high-end cinematic glow
72
- ```
73
-
74
- For numbered pairs, `{subject}` comes from `paired_subject` in [configs/flux_lora.toml](/home/zireael/lora/configs/flux_lora.toml). For standalone images, `{subject}` is derived from the filename stem.
75
-
76
- ## 3. Share or fetch the dataset
77
-
78
- Uploading the prepared dataset to Hugging Face is recommended if other users should reproduce the run. Upload `training_data/flux_aura_style`, not the raw `dataset/` folder, unless you intentionally want to publish the original source images too.
79
-
80
- Before publishing, make sure every image is safe to redistribute and choose `--private` if the dataset should only be available to collaborators.
81
-
82
- ```bash
83
- hf auth login
84
- hf repo create ezhoureal/flux-aura-style --type dataset --private
85
- hf upload ezhoureal/flux-aura-style training_data/flux_aura_style .
86
- ```
87
-
88
- Other users can fetch it with:
89
-
90
- ```bash
91
- hf download ezhoureal/flux-aura-style \
92
- --repo-type dataset \
93
- --local-dir training_data/flux_aura_style
94
- ```
95
-
96
- Then they can train without running `lora prepare-dataset`:
97
-
98
- ```bash
99
- uv sync
100
- uv run lora train
101
- ```
102
-
103
- The prepared dataset must keep this layout:
104
-
105
- ```text
106
- training_data/flux_aura_style/
107
- train/
108
- metadata.jsonl
109
- pair-001.png
110
- label-001.png
111
- conditioning/
112
- pair-001.png
113
- ```
114
-
115
- ## 4. Train the LoRA
116
-
117
- Start training with:
118
-
119
- ```bash
120
- lora train
121
- ```
122
-
123
- The launcher will:
124
-
125
- - run the tracked `scripts/train_dreambooth_lora_flux_lowmem.py` trainer
126
- - precompute all caption embeddings with CLIP/T5 before loading the FLUX transformer onto the GPU
127
- - free CLIP/T5 before transformer LoRA training starts
128
-
129
- The default config is intentionally conservative for a small dataset:
130
-
131
- - rank `8`
132
- - resolution `512`
133
- - `adamw` with learning rate `1e-4`
134
- - train batch size `2` with gradient accumulation `2`
135
- - dataloader workers `4`
136
- - in-training validation off, so the trainer does not reload the full inference pipeline while training
137
-
138
- If your GPU does not fit that profile, fall back in this order:
139
-
140
- - enable gradient checkpointing
141
- - reduce `train_batch_size` to `1`
142
- - increase `gradient_accumulation_steps` to keep the same effective batch size
143
-
144
- ## 5. Test inference
145
-
146
- Once training finishes:
147
-
148
- ```bash
149
- lora infer
150
- ```
151
-
152
- Or pass a custom prompt:
153
-
154
- ```bash
155
- lora infer "a side-profile portrait of a man in the style of zrlprfl, pastel haze, cinematic silhouette"
156
- ```
157
-
158
- The sample image is written to:
159
-
160
- ```text
161
- samples/flux-lora-test.png
162
- ```
163
-
164
- ## 6. Notes on Flux training
165
-
166
- - `FLUX.1-dev` is gated on Hugging Face, so you must accept the model terms before downloads work.
167
- - The official DreamBooth Flux trainer is text-to-image. It trains from the target image and `prompt` column; paired source images are preserved in `conditioning/` and referenced by metadata for future image-conditioned workflows, but this trainer does not consume them.
168
- - Flux LoRA training is memory-heavy. This workspace defaults to 512px, rank 8, gradient checkpointing, latent caching, and cached prompt embeddings to fit a 32 GB GPU more reliably.
169
- - If your GPU still runs out of memory, reduce `resolution`, reduce `rank`, or increase `gradient_accumulation_steps`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/configs/flux_lora.toml DELETED
@@ -1,46 +0,0 @@
1
- [dataset]
2
- source_dir = "dataset"
3
- prepared_dir = "training_data/flux_aura_style"
4
-
5
- [prompts]
6
- paired_subject = "man"
7
- instance_prompt = "A strong colorful light is illuminating man's silhouette. Medium shot. The image is rendered in a smooth gradient of luminous light, giving a radiant and ethereal appearance. Subtle contour lighting highlights delicate outlines, adding depth and a refined, high-end cinematic glow"
8
- prompt_template = "A strong colorful light is illuminating {subject}'s silhouette. Medium shot. The image is rendered in a smooth gradient of luminous light, giving a radiant and ethereal appearance. Subtle contour lighting highlights delicate outlines, adding depth and a refined, high-end cinematic glow"
9
-
10
- [training]
11
- model_name = "black-forest-labs/FLUX.1-dev"
12
- diffusers_ref = "a1c7df48013b1911231f34769ada4865a3acae20"
13
- training_script = "scripts/train_dreambooth_lora_flux_lowmem.py"
14
- output_dir = "outputs"
15
- mixed_precision = "bf16"
16
- resolution = 512
17
- train_batch_size = 2
18
- gradient_accumulation_steps = 2
19
- gradient_checkpointing = true
20
- optimizer = "adamw"
21
- learning_rate = 1e-4
22
- lr_scheduler = "constant"
23
- lr_warmup_steps = 0
24
- max_train_steps = 1000
25
- rank = 8
26
- lora_alpha = 8
27
- validation_prompt = ""
28
- validation_epochs = 100
29
- num_validation_images = 0
30
- seed = 42
31
- report_to = "tensorboard"
32
- repeats = 10
33
- max_sequence_length = 256
34
- cache_latents = true
35
- dataloader_num_workers = 4
36
- use_8bit_adam = false
37
- push_to_hub = false
38
-
39
- [inference]
40
- weight_name = "pytorch_lora_weights_v1.safetensors"
41
- prompt = "A strong colorful light is illuminating man's silhouette. Medium shot. The image is rendered in a smooth gradient of luminous light (pink, peach, lavendar), giving a radiant and ethereal appearance. Subtle contour lighting highlights delicate outlines, adding depth and a refined, high-end cinematic glow"
42
- output_path = "samples/flux-lora-test.png"
43
- height = 1024
44
- width = 1024
45
- guidance_scale = 3.5
46
- num_inference_steps = 28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/main.py DELETED
@@ -1,10 +0,0 @@
1
- from pathlib import Path
2
- import sys
3
-
4
- sys.path.insert(0, str(Path(__file__).resolve().parent / "src"))
5
-
6
- from lora.cli import main
7
-
8
-
9
- if __name__ == "__main__":
10
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
code/pyproject.toml DELETED
@@ -1,62 +0,0 @@
1
- [project]
2
- name = "lora"
3
- version = "0.1.0"
4
- description = "Local tooling for preparing and training a FLUX.1-dev LoRA."
5
- readme = "README.md"
6
- requires-python = ">=3.11"
7
- dependencies = [
8
- "accelerate>=0.31.0",
9
- "datasets>=3.0.0",
10
- "diffusers @ git+https://github.com/huggingface/diffusers.git",
11
- "fal-client>=0.7.0",
12
- "ftfy",
13
- "hf_transfer",
14
- "huggingface-hub>=0.31.0",
15
- "jinja2",
16
- "peft>=0.11.1",
17
- "pillow>=10.4.0",
18
- "torch==2.11.0+cu128",
19
- "torchvision==0.26.0+cu128",
20
- "transformers>=4.41.2",
21
- "sentencepiece",
22
- "tensorboard",
23
- "bitsandbytes>=0.49.2",
24
- "protobuf>=7.35.0",
25
- "requests>=2.34.2",
26
- ]
27
-
28
- [dependency-groups]
29
- dev = [
30
- "pyright>=1.1.400",
31
- "pytest>=8.0.0",
32
- "ruff>=0.11.0",
33
- ]
34
-
35
- [project.scripts]
36
- lora = "lora.cli:main"
37
-
38
- [build-system]
39
- requires = ["setuptools>=68"]
40
- build-backend = "setuptools.build_meta"
41
-
42
- [[tool.uv.index]]
43
- url = "https://pypi.tuna.tsinghua.edu.cn/simple"
44
- default = true
45
-
46
- [[tool.uv.index]]
47
- name = "pytorch-cu128"
48
- url = "https://download.pytorch.org/whl/cu128"
49
- explicit = true
50
-
51
- [tool.uv.sources]
52
- torch = { index = "pytorch-cu128" }
53
- torchvision = { index = "pytorch-cu128" }
54
-
55
- [tool.setuptools]
56
- package-dir = {"" = "src"}
57
-
58
- [tool.setuptools.packages.find]
59
- where = ["src"]
60
-
61
- [tool.ruff]
62
- line-length = 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/scripts/generate_realistic_label_pairs.py DELETED
@@ -1,324 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import argparse
5
- import base64
6
- import json
7
- import os
8
- import re
9
- import shutil
10
- import sys
11
- import time
12
- import urllib.error
13
- import urllib.request
14
- from dataclasses import dataclass
15
- from pathlib import Path
16
- from typing import Any
17
-
18
-
19
- REPO_ROOT = Path(__file__).resolve().parents[1]
20
- DEFAULT_TRAIN_DIR = REPO_ROOT / "training_data" / "flux_aura_style" / "train"
21
- DEFAULT_CONDITIONING_DIR = REPO_ROOT / "training_data" / "flux_aura_style" / "conditioning"
22
- DEFAULT_API_URL = "https://ark.cn-beijing.volces.com/api/v3/images/generations"
23
- DEFAULT_GENERATION_PROMPT = (
24
- "Use the reference image only for composition, subject identity, pose, silhouette, and camera "
25
- "framing. Generate a realistic natural photo of the same subject before the aura lighting style "
26
- "was applied. Remove colorful glow, gradients, ethereal haze, rim-light effects, painterly "
27
- "stylization, and text. Keep the result clean, plausible, detailed, and photorealistic."
28
- )
29
-
30
-
31
- @dataclass(frozen=True)
32
- class LabelRecord:
33
- label_path: Path
34
- prompt: str
35
-
36
-
37
- def parse_args() -> argparse.Namespace:
38
- parser = argparse.ArgumentParser(
39
- description=(
40
- "Generate realistic conditioning images for label-*.png targets and append them as "
41
- "new paired training records."
42
- )
43
- )
44
- parser.add_argument(
45
- "--api-key",
46
- default=os.environ.get("ARK_API_KEY"),
47
- help="Volcengine Ark API key. Defaults to ARK_API_KEY.",
48
- )
49
- parser.add_argument(
50
- "--model",
51
- default=os.environ.get("ARK_IMAGE_MODEL", "doubao-seedream-5-0-260128"),
52
- help="Ark image generation model or endpoint ID. Defaults to ARK_IMAGE_MODEL.",
53
- )
54
- parser.add_argument("--api-url", default=DEFAULT_API_URL, help="Image generation API URL.")
55
- parser.add_argument(
56
- "--train-dir",
57
- type=Path,
58
- default=DEFAULT_TRAIN_DIR,
59
- help="Directory containing label-*.png and metadata.jsonl.",
60
- )
61
- parser.add_argument(
62
- "--conditioning-dir",
63
- type=Path,
64
- default=DEFAULT_CONDITIONING_DIR,
65
- help="Directory where generated realistic pair conditioning images are written.",
66
- )
67
- parser.add_argument(
68
- "--prompt",
69
- default=DEFAULT_GENERATION_PROMPT,
70
- help="Prompt used to generate the realistic conditioning image from each label reference.",
71
- )
72
- parser.add_argument(
73
- "--size",
74
- default="2048x2048",
75
- help="Requested output size. Seedream 5.0 lite accepts values such as 2048x2048.",
76
- )
77
- parser.add_argument(
78
- "--limit",
79
- type=int,
80
- default=None,
81
- help="Maximum number of labels to process.",
82
- )
83
- parser.add_argument(
84
- "--start-label",
85
- type=int,
86
- default=None,
87
- help="First label number to process, inclusive.",
88
- )
89
- parser.add_argument(
90
- "--end-label",
91
- type=int,
92
- default=None,
93
- help="Last label number to process, inclusive.",
94
- )
95
- parser.add_argument(
96
- "--request-timeout",
97
- type=int,
98
- default=300,
99
- help="Per-request timeout in seconds.",
100
- )
101
- parser.add_argument(
102
- "--sleep",
103
- type=float,
104
- default=0.0,
105
- help="Seconds to sleep between successful API calls.",
106
- )
107
- parser.add_argument(
108
- "--retries",
109
- type=int,
110
- default=2,
111
- help="Number of retries after a failed API call.",
112
- )
113
- parser.add_argument(
114
- "--dry-run",
115
- action="store_true",
116
- help="Print the planned label to pair mapping without calling the API or writing files.",
117
- )
118
- return parser.parse_args()
119
-
120
-
121
- def label_number(path: Path) -> int:
122
- match = re.fullmatch(r"label-(\d+)\.png", path.name)
123
- if not match:
124
- raise ValueError(f"Not a label file: {path}")
125
- return int(match.group(1))
126
-
127
-
128
- def pair_number(path: Path) -> int:
129
- match = re.fullmatch(r"pair-(\d+)\.png", path.name)
130
- if not match:
131
- raise ValueError(f"Not a pair file: {path}")
132
- return int(match.group(1))
133
-
134
-
135
- def read_metadata(metadata_path: Path) -> list[dict[str, Any]]:
136
- with metadata_path.open("r", encoding="utf-8") as fh:
137
- return [json.loads(line) for line in fh if line.strip()]
138
-
139
-
140
- def collect_labels(train_dir: Path, metadata_rows: list[dict[str, Any]]) -> list[LabelRecord]:
141
- prompts_by_name = {
142
- row["file_name"]: row.get("prompt", "")
143
- for row in metadata_rows
144
- if row.get("kind") == "label" and isinstance(row.get("file_name"), str)
145
- }
146
- records = []
147
- for label_path in sorted(train_dir.glob("label-*.png"), key=label_number):
148
- prompt = prompts_by_name.get(label_path.name)
149
- if prompt is None:
150
- print(f"Skipping {label_path.name}: no label metadata row found.", file=sys.stderr)
151
- continue
152
- records.append(LabelRecord(label_path=label_path, prompt=prompt))
153
- return records
154
-
155
-
156
- def filter_labels(
157
- records: list[LabelRecord],
158
- start_label: int | None,
159
- end_label: int | None,
160
- limit: int | None,
161
- ) -> list[LabelRecord]:
162
- filtered = []
163
- for record in records:
164
- number = label_number(record.label_path)
165
- if start_label is not None and number < start_label:
166
- continue
167
- if end_label is not None and number > end_label:
168
- continue
169
- filtered.append(record)
170
- if limit is not None:
171
- filtered = filtered[:limit]
172
- return filtered
173
-
174
-
175
- def next_pair_numbers(train_dir: Path, count: int) -> list[int]:
176
- existing = [pair_number(path) for path in train_dir.glob("pair-*.png")]
177
- start = max(existing, default=0) + 1
178
- return list(range(start, start + count))
179
-
180
-
181
- def image_as_data_url(path: Path) -> str:
182
- encoded = base64.b64encode(path.read_bytes()).decode("ascii")
183
- return f"data:image/png;base64,{encoded}"
184
-
185
-
186
- def strip_data_url_prefix(value: str) -> str:
187
- if "," in value and value.lower().startswith("data:"):
188
- return value.split(",", 1)[1]
189
- return value
190
-
191
-
192
- def ark_generate_image(args: argparse.Namespace, label_path: Path) -> bytes:
193
- payload = {
194
- "model": args.model,
195
- "prompt": args.prompt,
196
- "image": image_as_data_url(label_path),
197
- "size": args.size,
198
- "sequential_image_generation": "disabled",
199
- "response_format": "b64_json",
200
- "output_format": "png",
201
- "watermark": False,
202
- }
203
- body = json.dumps(payload).encode("utf-8")
204
- request = urllib.request.Request(
205
- args.api_url,
206
- data=body,
207
- headers={
208
- "Authorization": f"Bearer {args.api_key}",
209
- "Content-Type": "application/json",
210
- },
211
- method="POST",
212
- )
213
-
214
- last_error: Exception | None = None
215
- for attempt in range(args.retries + 1):
216
- try:
217
- with urllib.request.urlopen(request, timeout=args.request_timeout) as response:
218
- result = json.loads(response.read().decode("utf-8"))
219
- data = result.get("data")
220
- if not data:
221
- raise RuntimeError(f"API response did not contain data: {result}")
222
- b64_json = data[0].get("b64_json")
223
- if not b64_json:
224
- raise RuntimeError(f"API response did not contain b64_json: {result}")
225
- return base64.b64decode(strip_data_url_prefix(b64_json))
226
- except urllib.error.HTTPError as exc:
227
- detail = exc.read().decode("utf-8", errors="replace")
228
- last_error = RuntimeError(f"HTTP {exc.code}: {detail}")
229
- except (urllib.error.URLError, TimeoutError, RuntimeError, json.JSONDecodeError) as exc:
230
- last_error = exc
231
-
232
- if attempt < args.retries:
233
- time.sleep(2**attempt)
234
-
235
- assert last_error is not None
236
- raise last_error
237
-
238
-
239
- def append_metadata(metadata_path: Path, row: dict[str, Any]) -> None:
240
- with metadata_path.open("a", encoding="utf-8") as fh:
241
- fh.write(json.dumps(row, ensure_ascii=False) + "\n")
242
-
243
-
244
- def main() -> int:
245
- args = parse_args()
246
- train_dir = args.train_dir.resolve()
247
- conditioning_dir = args.conditioning_dir.resolve()
248
- metadata_path = train_dir / "metadata.jsonl"
249
-
250
- if not train_dir.exists():
251
- print(f"Train directory does not exist: {train_dir}", file=sys.stderr)
252
- return 1
253
- if not metadata_path.exists():
254
- print(f"Metadata file does not exist: {metadata_path}", file=sys.stderr)
255
- return 1
256
- if not args.dry_run and not args.api_key:
257
- print("Missing API key. Set ARK_API_KEY or pass --api-key.", file=sys.stderr)
258
- return 1
259
-
260
- rows = read_metadata(metadata_path)
261
- labels = filter_labels(
262
- collect_labels(train_dir, rows),
263
- start_label=args.start_label,
264
- end_label=args.end_label,
265
- limit=args.limit,
266
- )
267
- if not labels:
268
- print("No label images selected.", file=sys.stderr)
269
- return 1
270
-
271
- pair_numbers = next_pair_numbers(train_dir, len(labels))
272
- plan = list(zip(labels, pair_numbers, strict=True))
273
-
274
- print(
275
- json.dumps(
276
- {
277
- "selected_labels": len(labels),
278
- "first_pair": f"pair-{pair_numbers[0]:03d}.png",
279
- "last_pair": f"pair-{pair_numbers[-1]:03d}.png",
280
- "model": args.model,
281
- "size": args.size,
282
- "dry_run": args.dry_run,
283
- },
284
- indent=2,
285
- )
286
- )
287
-
288
- if args.dry_run:
289
- for label, number in plan:
290
- print(f"{label.label_path.name} -> pair-{number:03d}.png")
291
- return 0
292
-
293
- conditioning_dir.mkdir(parents=True, exist_ok=True)
294
- for index, (label, number) in enumerate(plan, start=1):
295
- pair_name = f"pair-{number:03d}.png"
296
- pair_target = train_dir / pair_name
297
- conditioning_target = conditioning_dir / pair_name
298
- if pair_target.exists() or conditioning_target.exists():
299
- print(f"Refusing to overwrite existing files for {pair_name}", file=sys.stderr)
300
- return 1
301
-
302
- print(f"[{index}/{len(plan)}] Generating realistic conditioning for {label.label_path.name}")
303
- image_bytes = ark_generate_image(args, label.label_path)
304
- conditioning_target.write_bytes(image_bytes)
305
- shutil.copy2(label.label_path, pair_target)
306
- append_metadata(
307
- metadata_path,
308
- {
309
- "file_name": pair_name,
310
- "prompt": label.prompt,
311
- "kind": "paired",
312
- "conditioning_path": f"../conditioning/{pair_name}",
313
- "source_label": label.label_path.name,
314
- },
315
- )
316
- if args.sleep:
317
- time.sleep(args.sleep)
318
-
319
- print(f"Generated {len(plan)} realistic label pairs.")
320
- return 0
321
-
322
-
323
- if __name__ == "__main__":
324
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/scripts/run_flux2_edit_lora_fal.py DELETED
@@ -1,432 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import argparse
5
- import json
6
- import os
7
- import sys
8
- import time
9
- import urllib.parse
10
- import urllib.request
11
- from pathlib import Path
12
- from typing import Any
13
-
14
-
15
- REPO_ROOT = Path(__file__).resolve().parents[1]
16
- DEFAULT_TRAINING_OUTPUT_DIR = REPO_ROOT / "outputs" / "fal_flux2_edit_lora"
17
- DEFAULT_TRAINING_RESULT = DEFAULT_TRAINING_OUTPUT_DIR / "fal-training-result.json"
18
- DEFAULT_OUTPUT_DIR = REPO_ROOT / "outputs" / "fal_flux2_edit_inference"
19
- DEFAULT_ENDPOINT = "fal-ai/flux-2/lora/edit"
20
- DEFAULT_PROMPT = (
21
- "Transform this photorealistic image into the trained radiant aura style: smooth colorful "
22
- "gradients, ethereal haze, subtle contour lighting, and a refined cinematic glow. Preserve the "
23
- "subject identity, composition, pose, silhouette, camera framing, and important details."
24
- )
25
-
26
-
27
- def parse_args() -> argparse.Namespace:
28
- parser = argparse.ArgumentParser(
29
- description=(
30
- "Apply the trained FLUX.2 edit LoRA on fal to a photorealistic input image and download "
31
- "the stylized result."
32
- )
33
- )
34
- parser.add_argument(
35
- "input_image",
36
- type=str,
37
- help="Photorealistic input image path or URL.",
38
- )
39
- parser.add_argument(
40
- "--prompt",
41
- default=DEFAULT_PROMPT,
42
- help="Edit instruction sent to FLUX.2 LoRA Edit.",
43
- )
44
- parser.add_argument(
45
- "--lora",
46
- default=None,
47
- help=(
48
- "LoRA weights URL, Hugging Face repo ID, or local safetensors path. If omitted, the "
49
- "script reads --training-result or uses a local safetensors file from the training "
50
- "output directory."
51
- ),
52
- )
53
- parser.add_argument(
54
- "--training-result",
55
- type=Path,
56
- default=DEFAULT_TRAINING_RESULT,
57
- help="fal training result JSON written by scripts/train_flux2_edit_lora_fal.py.",
58
- )
59
- parser.add_argument(
60
- "--lora-scale",
61
- type=float,
62
- default=1.0,
63
- help="LoRA strength passed as the LoRAInput scale.",
64
- )
65
- parser.add_argument(
66
- "--output-dir",
67
- type=Path,
68
- default=DEFAULT_OUTPUT_DIR,
69
- help="Directory for downloaded images and the inference result JSON.",
70
- )
71
- parser.add_argument(
72
- "--output-path",
73
- type=Path,
74
- default=None,
75
- help="Optional path for the first downloaded output image.",
76
- )
77
- parser.add_argument(
78
- "--endpoint",
79
- default=DEFAULT_ENDPOINT,
80
- help="fal endpoint id.",
81
- )
82
- parser.add_argument(
83
- "--guidance-scale",
84
- type=float,
85
- default=2.5,
86
- help="Prompt adherence. fal default is 2.5.",
87
- )
88
- parser.add_argument(
89
- "--num-inference-steps",
90
- type=int,
91
- default=28,
92
- help="Number of inference steps. fal accepts 4 to 50.",
93
- )
94
- parser.add_argument(
95
- "--image-size",
96
- default=None,
97
- help='Optional output size as WIDTHxHEIGHT, for example "1024x1024". If omitted, fal chooses.',
98
- )
99
- parser.add_argument(
100
- "--num-images",
101
- type=int,
102
- default=1,
103
- help="Number of images to generate. fal accepts 1 to 4.",
104
- )
105
- parser.add_argument(
106
- "--seed",
107
- type=int,
108
- default=None,
109
- help="Optional seed for reproducible generations.",
110
- )
111
- parser.add_argument(
112
- "--acceleration",
113
- choices=("none", "regular", "high"),
114
- default="regular",
115
- help="fal acceleration level.",
116
- )
117
- parser.add_argument(
118
- "--output-format",
119
- choices=("jpeg", "png", "webp"),
120
- default="png",
121
- help="Output image format.",
122
- )
123
- parser.add_argument(
124
- "--enable-prompt-expansion",
125
- action="store_true",
126
- help="Ask fal to expand the prompt before generation.",
127
- )
128
- parser.add_argument(
129
- "--disable-safety-checker",
130
- action="store_true",
131
- help="Disable fal safety checker.",
132
- )
133
- parser.add_argument(
134
- "--dry-run",
135
- action="store_true",
136
- help="Print the resolved request arguments without calling fal.",
137
- )
138
- return parser.parse_args()
139
-
140
-
141
- def is_url(value: str) -> bool:
142
- parsed = urllib.parse.urlparse(value)
143
- return parsed.scheme in {"http", "https"}
144
-
145
-
146
- def parse_image_size(value: str | None) -> dict[str, int] | None:
147
- if value is None:
148
- return None
149
- try:
150
- width_text, height_text = value.lower().split("x", 1)
151
- width = int(width_text)
152
- height = int(height_text)
153
- except ValueError as exc:
154
- raise ValueError('--image-size must look like "1024x1024".') from exc
155
-
156
- if not 512 <= width <= 2048 or not 512 <= height <= 2048:
157
- raise ValueError("--image-size width and height must be between 512 and 2048 pixels.")
158
- return {"width": width, "height": height}
159
-
160
-
161
- def on_queue_update(update: object) -> None:
162
- try:
163
- import fal_client
164
- except ImportError:
165
- return
166
-
167
- if isinstance(update, fal_client.InProgress) and update.logs:
168
- for log in update.logs:
169
- message = log.get("message")
170
- if message:
171
- print(message, flush=True)
172
-
173
-
174
- def load_training_result(path: Path) -> dict[str, Any] | None:
175
- if not path.exists():
176
- return None
177
- data = json.loads(path.read_text(encoding="utf-8"))
178
- result = data.get("result", data)
179
- if not isinstance(result, dict):
180
- raise ValueError(f"Training result JSON has no object result: {path}")
181
- return result
182
-
183
-
184
- def lora_from_training_result(path: Path) -> str | None:
185
- result = load_training_result(path)
186
- if result is None:
187
- return None
188
-
189
- for key in ("diffusers_lora_file", "lora_file", "lora"):
190
- value = result.get(key)
191
- if isinstance(value, dict) and isinstance(value.get("url"), str):
192
- return value["url"]
193
- if isinstance(value, str):
194
- return value
195
-
196
- loras = result.get("loras")
197
- if isinstance(loras, list):
198
- for value in loras:
199
- if isinstance(value, dict) and isinstance(value.get("url"), str):
200
- return value["url"]
201
- if isinstance(value, str):
202
- return value
203
- return None
204
-
205
-
206
- def latest_local_lora(training_output_dir: Path) -> Path | None:
207
- candidates = sorted(
208
- training_output_dir.glob("*.safetensors"),
209
- key=lambda path: path.stat().st_mtime,
210
- reverse=True,
211
- )
212
- return candidates[0] if candidates else None
213
-
214
-
215
- def resolve_lora(args: argparse.Namespace) -> str:
216
- if args.lora:
217
- return args.lora
218
-
219
- lora = lora_from_training_result(args.training_result.resolve())
220
- if lora:
221
- return lora
222
-
223
- local_lora = latest_local_lora(args.training_result.resolve().parent)
224
- if local_lora is not None:
225
- return str(local_lora)
226
-
227
- raise FileNotFoundError(
228
- "Could not find LoRA weights. Pass --lora, or run training without --no-download so "
229
- f"{args.training_result} contains a diffusers_lora_file URL."
230
- )
231
-
232
-
233
- def upload_input_image(value: str, *, dry_run: bool) -> str:
234
- if is_url(value):
235
- return value
236
-
237
- path = Path(value).expanduser().resolve()
238
- if not path.exists():
239
- raise FileNotFoundError(f"Input image does not exist: {path}")
240
- if dry_run:
241
- return str(path)
242
-
243
- import fal_client
244
-
245
- uploaded_url = fal_client.upload_file(path)
246
- print(f"Uploaded {path.name}: {uploaded_url}", flush=True)
247
- return uploaded_url
248
-
249
-
250
- def upload_lora_if_local(value: str, *, dry_run: bool) -> str:
251
- if is_url(value):
252
- return value
253
-
254
- path = Path(value).expanduser().resolve()
255
- if not path.exists():
256
- # The API also accepts Hugging Face repo IDs, which look like "owner/repo".
257
- return value
258
- if dry_run:
259
- return str(path)
260
-
261
- import fal_client
262
-
263
- uploaded_url = fal_client.upload_file(path)
264
- print(f"Uploaded {path.name}: {uploaded_url}", flush=True)
265
- return uploaded_url
266
-
267
-
268
- def output_file_name(image: dict[str, Any], index: int, output_format: str) -> str:
269
- file_name = image.get("file_name")
270
- if isinstance(file_name, str) and file_name:
271
- return file_name
272
-
273
- url = image.get("url")
274
- if isinstance(url, str):
275
- parsed_name = Path(urllib.parse.urlparse(url).path).name
276
- if parsed_name:
277
- return parsed_name
278
-
279
- suffix = "jpg" if output_format == "jpeg" else output_format
280
- return f"stylized-{index + 1:02d}.{suffix}"
281
-
282
-
283
- def download_images(
284
- images: list[dict[str, Any]],
285
- output_dir: Path,
286
- output_path: Path | None,
287
- output_format: str,
288
- ) -> list[Path]:
289
- output_dir.mkdir(parents=True, exist_ok=True)
290
- downloaded = []
291
-
292
- for index, image in enumerate(images):
293
- url = image.get("url")
294
- if not isinstance(url, str) or not url:
295
- continue
296
-
297
- if index == 0 and output_path is not None:
298
- destination = output_path.expanduser().resolve()
299
- destination.parent.mkdir(parents=True, exist_ok=True)
300
- else:
301
- destination = output_dir / output_file_name(image, index, output_format)
302
-
303
- urllib.request.urlretrieve(url, destination)
304
- downloaded.append(destination)
305
-
306
- return downloaded
307
-
308
-
309
- def build_arguments(
310
- args: argparse.Namespace,
311
- image_url: str,
312
- lora_url: str,
313
- ) -> dict[str, Any]:
314
- request_arguments: dict[str, Any] = {
315
- "prompt": args.prompt,
316
- "guidance_scale": args.guidance_scale,
317
- "num_inference_steps": args.num_inference_steps,
318
- "num_images": args.num_images,
319
- "acceleration": args.acceleration,
320
- "enable_prompt_expansion": args.enable_prompt_expansion,
321
- "enable_safety_checker": not args.disable_safety_checker,
322
- "output_format": args.output_format,
323
- "image_urls": [image_url],
324
- "loras": [{"path": lora_url, "scale": args.lora_scale}],
325
- }
326
-
327
- image_size = parse_image_size(args.image_size)
328
- if image_size is not None:
329
- request_arguments["image_size"] = image_size
330
- if args.seed is not None:
331
- request_arguments["seed"] = args.seed
332
-
333
- return request_arguments
334
-
335
-
336
- def run_with_fal(endpoint: str, request_arguments: dict[str, Any]) -> tuple[dict[str, Any], str | None]:
337
- import fal_client
338
-
339
- result = fal_client.subscribe(
340
- endpoint,
341
- arguments=request_arguments,
342
- with_logs=True,
343
- on_queue_update=on_queue_update,
344
- )
345
- if hasattr(result, "data"):
346
- return dict(result.data), getattr(result, "request_id", None)
347
- return dict(result), None
348
-
349
-
350
- def main() -> int:
351
- args = parse_args()
352
-
353
- if args.num_images < 1 or args.num_images > 4:
354
- print("--num-images must be between 1 and 4.", file=sys.stderr)
355
- return 1
356
- if args.num_inference_steps < 4 or args.num_inference_steps > 50:
357
- print("--num-inference-steps must be between 4 and 50.", file=sys.stderr)
358
- return 1
359
- if args.guidance_scale < 0 or args.guidance_scale > 20:
360
- print("--guidance-scale must be between 0 and 20.", file=sys.stderr)
361
- return 1
362
- if not args.dry_run and not os.environ.get("FAL_KEY"):
363
- print("Missing fal key. Set FAL_KEY before running inference.", file=sys.stderr)
364
- return 1
365
-
366
- try:
367
- input_image_url = upload_input_image(args.input_image, dry_run=args.dry_run)
368
- lora_url = upload_lora_if_local(resolve_lora(args), dry_run=args.dry_run)
369
- request_arguments = build_arguments(args, input_image_url, lora_url)
370
- except (FileNotFoundError, ValueError) as exc:
371
- print(str(exc), file=sys.stderr)
372
- return 1
373
-
374
- print(
375
- json.dumps(
376
- {
377
- "endpoint": args.endpoint,
378
- "input_image": input_image_url,
379
- "lora": lora_url,
380
- "lora_scale": args.lora_scale,
381
- "output_dir": str(args.output_dir.resolve()),
382
- "dry_run": args.dry_run,
383
- },
384
- indent=2,
385
- )
386
- )
387
-
388
- if args.dry_run:
389
- print(json.dumps(request_arguments, indent=2))
390
- return 0
391
-
392
- output_dir = args.output_dir.resolve()
393
- output_dir.mkdir(parents=True, exist_ok=True)
394
- started_at = time.time()
395
- result, request_id = run_with_fal(args.endpoint, request_arguments)
396
- elapsed = time.time() - started_at
397
-
398
- images = result.get("images", [])
399
- if not isinstance(images, list):
400
- print("fal response did not contain an images list.", file=sys.stderr)
401
- return 1
402
-
403
- downloaded = download_images(
404
- [image for image in images if isinstance(image, dict)],
405
- output_dir,
406
- args.output_path,
407
- args.output_format,
408
- )
409
- result_path = output_dir / "fal-inference-result.json"
410
- result_path.write_text(
411
- json.dumps(
412
- {
413
- "request_id": request_id,
414
- "elapsed_seconds": elapsed,
415
- "arguments": request_arguments,
416
- "result": result,
417
- "downloaded_images": [str(path) for path in downloaded],
418
- },
419
- indent=2,
420
- ),
421
- encoding="utf-8",
422
- )
423
-
424
- print(f"Wrote result JSON: {result_path}")
425
- for path in downloaded:
426
- print(f"Downloaded image: {path}")
427
-
428
- return 0
429
-
430
-
431
- if __name__ == "__main__":
432
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/scripts/run_flux2_edit_lora_local.py DELETED
@@ -1,479 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import argparse
5
- import importlib.util
6
- import json
7
- import re
8
- import sys
9
- import time
10
- from pathlib import Path
11
- from typing import Any
12
-
13
- import torch
14
- from safetensors.torch import load_file, save_file
15
-
16
-
17
- REPO_ROOT = Path(__file__).resolve().parents[1]
18
- DEFAULT_LORA = REPO_ROOT / "fal_flux2_edit_lora" / "pytorch_lora_weights.safetensors"
19
- DEFAULT_OUTPUT_DIR = REPO_ROOT / "outputs" / "local_flux2_edit_inference"
20
- DEFAULT_MODEL = "diffusers/FLUX.2-dev-bnb-4bit"
21
- DEFAULT_PROMPT = (
22
- "Transform this photorealistic image into the trained radiant aura style: smooth colorful "
23
- "gradients, ethereal haze, subtle contour lighting, and a refined cinematic glow. Preserve the "
24
- "subject identity, composition, pose, silhouette, camera framing, and important details."
25
- )
26
- SUPPORTED_IMAGE_SUFFIXES = {".avif", ".bmp", ".jpeg", ".jpg", ".png", ".webp"}
27
-
28
-
29
- def parse_args() -> argparse.Namespace:
30
- parser = argparse.ArgumentParser(
31
- description="Run local FLUX.2 image editing with the fal-trained LoRA, or validate it offline."
32
- )
33
- parser.add_argument(
34
- "input_dir",
35
- nargs="?",
36
- type=Path,
37
- help="Directory containing photorealistic input images.",
38
- )
39
- parser.add_argument("--prompt", default=DEFAULT_PROMPT, help="Edit prompt.")
40
- parser.add_argument("--lora", type=Path, default=DEFAULT_LORA, help="Input LoRA safetensors file.")
41
- parser.add_argument(
42
- "--converted-lora",
43
- type=Path,
44
- default=None,
45
- help="Optional path for a converted diffusers-format LoRA safetensors file.",
46
- )
47
- parser.add_argument(
48
- "--model",
49
- default=DEFAULT_MODEL,
50
- help="Local path or Hugging Face model id. Defaults to the 4-bit FLUX.2-dev diffusers repo.",
51
- )
52
- parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
53
- parser.add_argument("--output-path", type=Path, default=None)
54
- parser.add_argument(
55
- "--batch-size",
56
- type=int,
57
- default=1,
58
- help=(
59
- "Number of input images to edit per pipeline call. Keep this low on <24GB VRAM; "
60
- "try 2 first, then increase if memory allows."
61
- ),
62
- )
63
- parser.add_argument("--height", type=int, default=1024)
64
- parser.add_argument("--width", type=int, default=1024)
65
- parser.add_argument("--num-inference-steps", type=int, default=28)
66
- parser.add_argument("--guidance-scale", type=float, default=2.5)
67
- parser.add_argument("--lora-scale", type=float, default=1.0)
68
- parser.add_argument("--seed", type=int, default=None)
69
- parser.add_argument(
70
- "--torch-dtype",
71
- choices=("auto", "float32", "float16", "bfloat16"),
72
- default="bfloat16",
73
- help="Pipeline dtype. Use bfloat16 on modern NVIDIA GPUs.",
74
- )
75
- parser.add_argument(
76
- "--device",
77
- default=None,
78
- help="Torch device. Defaults to cuda if available, otherwise cpu.",
79
- )
80
- parser.add_argument(
81
- "--device-map",
82
- default=None,
83
- help='Optional diffusers/accelerate device map, for example "balanced".',
84
- )
85
- parser.add_argument(
86
- "--local-files-only",
87
- action="store_true",
88
- help="Do not download model files from Hugging Face.",
89
- )
90
- parser.add_argument(
91
- "--check-only",
92
- action="store_true",
93
- help="Validate/convert LoRA against the default Flux2Transformer2DModel shape without loading the base model.",
94
- )
95
- return parser.parse_args()
96
-
97
-
98
- def dtype_from_arg(value: str) -> torch.dtype | str:
99
- if value == "auto":
100
- return "auto"
101
- return {
102
- "float32": torch.float32,
103
- "float16": torch.float16,
104
- "bfloat16": torch.bfloat16,
105
- }[value]
106
-
107
-
108
- def require_module(import_name: str, install_name: str | None = None) -> None:
109
- if importlib.util.find_spec(import_name) is None:
110
- package = install_name or import_name
111
- raise RuntimeError(f"Missing required package `{package}`. Install it with `uv add {package}`.")
112
-
113
-
114
- def uses_4bit_model(model: str) -> bool:
115
- return "bnb-4bit" in model.lower() or "4bit" in model.lower()
116
-
117
-
118
- def preflight_environment(args: argparse.Namespace) -> None:
119
- if args.check_only:
120
- return
121
-
122
- require_module("google.protobuf", "protobuf")
123
-
124
- device_name = args.device or ("cuda" if torch.cuda.is_available() else "cpu")
125
- if uses_4bit_model(args.model):
126
- require_module("bitsandbytes")
127
- if device_name == "cpu" or not torch.cuda.is_available():
128
- raise RuntimeError(
129
- "The 4-bit FLUX.2 model needs a CUDA GPU with bitsandbytes. "
130
- "This environment does not expose CUDA to PyTorch."
131
- )
132
- def convert_fal_key(key: str, tensor: torch.Tensor) -> dict[str, torch.Tensor]:
133
- prefix = "base_model.model."
134
- if not key.startswith(prefix):
135
- return {key: tensor}
136
-
137
- body = key.removeprefix(prefix)
138
- suffix = ".lora_A.weight" if body.endswith(".lora_A.weight") else ".lora_B.weight"
139
- base = body.removesuffix(suffix)
140
-
141
- simple_map = {
142
- "img_in": "x_embedder",
143
- "txt_in": "context_embedder",
144
- "time_in.in_layer": "time_guidance_embed.timestep_embedder.linear_1",
145
- "time_in.out_layer": "time_guidance_embed.timestep_embedder.linear_2",
146
- "guidance_in.in_layer": "time_guidance_embed.guidance_embedder.linear_1",
147
- "guidance_in.out_layer": "time_guidance_embed.guidance_embedder.linear_2",
148
- "double_stream_modulation_img.lin": "double_stream_modulation_img.linear",
149
- "double_stream_modulation_txt.lin": "double_stream_modulation_txt.linear",
150
- "single_stream_modulation.lin": "single_stream_modulation.linear",
151
- "final_layer.linear": "proj_out",
152
- }
153
- if base in simple_map:
154
- return {f"transformer.{simple_map[base]}{suffix}": tensor}
155
-
156
- double_match = re.fullmatch(r"double_blocks\.(\d+)\.(img_attn|txt_attn)\.(qkv|proj)", base)
157
- if double_match:
158
- block, stream, layer = double_match.groups()
159
- stem = f"transformer.transformer_blocks.{block}.attn"
160
- if layer == "proj":
161
- target = "to_out.0" if stream == "img_attn" else "to_add_out"
162
- return {f"{stem}.{target}{suffix}": tensor}
163
-
164
- targets = (
165
- ("to_q", "to_k", "to_v")
166
- if stream == "img_attn"
167
- else ("add_q_proj", "add_k_proj", "add_v_proj")
168
- )
169
- if suffix == ".lora_A.weight":
170
- return {f"{stem}.{target}{suffix}": tensor.clone() for target in targets}
171
-
172
- chunks = tensor.chunk(3, dim=0)
173
- return {f"{stem}.{target}{suffix}": chunk.contiguous() for target, chunk in zip(targets, chunks)}
174
-
175
- single_match = re.fullmatch(r"single_blocks\.(\d+)\.(linear1|linear2)", base)
176
- if single_match:
177
- block, layer = single_match.groups()
178
- target = "to_qkv_mlp_proj" if layer == "linear1" else "to_out"
179
- return {f"transformer.single_transformer_blocks.{block}.attn.{target}{suffix}": tensor}
180
-
181
- raise ValueError(f"Unsupported fal LoRA key: {key}")
182
-
183
-
184
- def convert_fal_lora_to_diffusers(input_path: Path, output_path: Path) -> dict[str, Any]:
185
- state = load_file(input_path)
186
- converted: dict[str, torch.Tensor] = {}
187
- for key, tensor in state.items():
188
- for new_key, new_tensor in convert_fal_key(key, tensor).items():
189
- if new_key in converted:
190
- raise ValueError(f"Duplicate converted LoRA key: {new_key}")
191
- converted[new_key] = new_tensor
192
-
193
- output_path.parent.mkdir(parents=True, exist_ok=True)
194
- save_file(converted, output_path, metadata={"format": "pt"})
195
- return {
196
- "input_keys": len(state),
197
- "converted_keys": len(converted),
198
- "input_bytes": input_path.stat().st_size,
199
- "converted_bytes": output_path.stat().st_size,
200
- }
201
-
202
-
203
- def expected_linear_shapes() -> dict[str, tuple[int, ...]]:
204
- from accelerate import init_empty_weights
205
- from diffusers import Flux2Transformer2DModel
206
-
207
- with init_empty_weights():
208
- model = Flux2Transformer2DModel()
209
- return {
210
- f"transformer.{name}": tuple(module.weight.shape)
211
- for name, module in model.named_modules()
212
- if module.__class__.__name__ == "Linear"
213
- }
214
-
215
-
216
- def validate_converted_lora(path: Path) -> dict[str, Any]:
217
- state = load_file(path)
218
- shapes = expected_linear_shapes()
219
- missing_targets = []
220
- bad_shapes = []
221
- ranks = set()
222
-
223
- for key, tensor in state.items():
224
- if key.endswith(".lora_A.weight"):
225
- target = key.removesuffix(".lora_A.weight")
226
- ranks.add(tensor.shape[0])
227
- expected = shapes.get(target)
228
- if expected is None:
229
- missing_targets.append(target)
230
- elif tuple(tensor.shape[1:]) != (expected[1],):
231
- bad_shapes.append((key, tuple(tensor.shape), expected))
232
- elif key.endswith(".lora_B.weight"):
233
- target = key.removesuffix(".lora_B.weight")
234
- ranks.add(tensor.shape[1])
235
- expected = shapes.get(target)
236
- if expected is None:
237
- missing_targets.append(target)
238
- elif tuple(tensor.shape[:1]) != (expected[0],):
239
- bad_shapes.append((key, tuple(tensor.shape), expected))
240
- else:
241
- missing_targets.append(key)
242
-
243
- return {
244
- "keys": len(state),
245
- "target_modules": len({key.rsplit(".lora_", 1)[0] for key in state}),
246
- "ranks": sorted(ranks),
247
- "missing_targets": sorted(set(missing_targets)),
248
- "bad_shapes": bad_shapes,
249
- "valid": not missing_targets and not bad_shapes,
250
- }
251
-
252
-
253
- def load_flux2_pipeline(args: argparse.Namespace, dtype: torch.dtype | str, device_name: str):
254
- from diffusers import Flux2Pipeline
255
-
256
- if uses_4bit_model(args.model) and device_name.startswith("cuda"):
257
- from diffusers import AutoModel
258
- from transformers import Mistral3ForConditionalGeneration
259
-
260
- print("Loading 4-bit FLUX.2 with local text encoder on CPU and model CPU offload.", flush=True)
261
- text_encoder = Mistral3ForConditionalGeneration.from_pretrained(
262
- args.model,
263
- subfolder="text_encoder",
264
- torch_dtype=dtype,
265
- device_map="cpu",
266
- local_files_only=args.local_files_only,
267
- )
268
- transformer = AutoModel.from_pretrained(
269
- args.model,
270
- subfolder="transformer",
271
- torch_dtype=dtype,
272
- device_map="cpu",
273
- local_files_only=args.local_files_only,
274
- )
275
- pipe = Flux2Pipeline.from_pretrained(
276
- args.model,
277
- text_encoder=text_encoder,
278
- transformer=transformer,
279
- torch_dtype=dtype,
280
- local_files_only=args.local_files_only,
281
- )
282
- pipe.enable_model_cpu_offload()
283
- return pipe
284
-
285
- load_kwargs: dict[str, Any] = {
286
- "torch_dtype": dtype,
287
- "local_files_only": args.local_files_only,
288
- }
289
- if args.device_map is not None:
290
- load_kwargs["device_map"] = args.device_map
291
- elif device_name.startswith("cuda"):
292
- load_kwargs["device_map"] = device_name
293
-
294
- pipe = Flux2Pipeline.from_pretrained(args.model, **load_kwargs)
295
- if "device_map" not in load_kwargs:
296
- pipe.to(device_name)
297
- return pipe
298
-
299
-
300
- def batched(values: list[Path], batch_size: int) -> list[list[Path]]:
301
- return [values[index : index + batch_size] for index in range(0, len(values), batch_size)]
302
-
303
-
304
- def discover_input_images(input_dir: Path) -> list[Path]:
305
- return sorted(
306
- (
307
- path
308
- for path in input_dir.iterdir()
309
- if path.is_file() and path.suffix.lower() in SUPPORTED_IMAGE_SUFFIXES
310
- ),
311
- key=lambda path: path.name.lower(),
312
- )
313
-
314
-
315
- def output_paths_for_inputs(args: argparse.Namespace, input_images: list[Path]) -> list[Path]:
316
- if args.output_path is None:
317
- return [
318
- args.output_dir / f"{input_path.stem}-flux2-local-stylized.png"
319
- for input_path in input_images
320
- ]
321
-
322
- if args.output_path.suffix:
323
- stem = args.output_path.with_suffix("")
324
- suffix = args.output_path.suffix
325
- return [
326
- stem.with_name(f"{stem.name}-{index:04d}{suffix}")
327
- for index, _input_path in enumerate(input_images, start=1)
328
- ]
329
-
330
- return [
331
- args.output_path / f"{input_path.stem}-flux2-local-stylized.png"
332
- for input_path in input_images
333
- ]
334
-
335
-
336
- def generators_for_batch(
337
- seed: int | None,
338
- device_name: str,
339
- *,
340
- start_index: int,
341
- batch_size: int,
342
- ) -> torch.Generator | list[torch.Generator] | None:
343
- if seed is None:
344
- return None
345
- if batch_size == 1:
346
- return torch.Generator(device=device_name).manual_seed(seed + start_index)
347
- return [
348
- torch.Generator(device=device_name).manual_seed(seed + start_index + index)
349
- for index in range(batch_size)
350
- ]
351
-
352
-
353
- def run_inference(args: argparse.Namespace, lora_path: Path) -> list[Path]:
354
- from diffusers.utils import load_image
355
-
356
- device_name = args.device or ("cuda" if torch.cuda.is_available() else "cpu")
357
- dtype = dtype_from_arg(args.torch_dtype)
358
- input_paths = discover_input_images(args.input_dir.expanduser().resolve())
359
- output_paths = output_paths_for_inputs(args, input_paths)
360
-
361
- print(
362
- f"Found {len(input_paths)} input image(s). Processing in batches of {args.batch_size}.",
363
- flush=True,
364
- )
365
- print("Using text encoder mode: local", flush=True)
366
- pipe = load_flux2_pipeline(args, dtype, device_name)
367
- pipe.load_lora_weights(str(lora_path), adapter_name="aura")
368
- pipe.set_adapters(["aura"], adapter_weights=[args.lora_scale])
369
-
370
- for start_index, batch_paths in enumerate(batched(input_paths, args.batch_size)):
371
- batch_offset = start_index * args.batch_size
372
- input_images = [load_image(str(input_path)) for input_path in batch_paths]
373
- image_arg: Any = input_images[0] if len(input_images) == 1 else input_images
374
- prompt_arg: Any = args.prompt if len(input_images) == 1 else [args.prompt] * len(batch_paths)
375
- call_kwargs: dict[str, Any] = {
376
- "image": image_arg,
377
- "height": args.height,
378
- "width": args.width,
379
- "num_inference_steps": args.num_inference_steps,
380
- "guidance_scale": args.guidance_scale,
381
- "generator": generators_for_batch(
382
- args.seed,
383
- device_name,
384
- start_index=batch_offset,
385
- batch_size=len(batch_paths),
386
- ),
387
- "prompt": prompt_arg,
388
- }
389
-
390
- images = pipe(**call_kwargs).images
391
- if len(images) != len(batch_paths):
392
- raise RuntimeError(f"Expected {len(batch_paths)} outputs from pipeline, received {len(images)}.")
393
-
394
- for image, output_path in zip(images, output_paths[batch_offset : batch_offset + len(images)]):
395
- output_path.parent.mkdir(parents=True, exist_ok=True)
396
- image.save(output_path)
397
-
398
- return output_paths
399
-
400
-
401
- def main() -> int:
402
- args = parse_args()
403
- lora_path = args.lora.expanduser().resolve()
404
- if not lora_path.exists():
405
- print(f"LoRA file does not exist: {lora_path}", file=sys.stderr)
406
- return 1
407
- if not args.check_only:
408
- if args.input_dir is None:
409
- print("input_dir is required unless --check-only is set.", file=sys.stderr)
410
- return 1
411
- if args.batch_size < 1:
412
- print("--batch-size must be at least 1.", file=sys.stderr)
413
- return 1
414
- input_dir = args.input_dir.expanduser().resolve()
415
- if not input_dir.exists():
416
- print(f"Input directory does not exist: {input_dir}", file=sys.stderr)
417
- return 1
418
- if not input_dir.is_dir():
419
- print(f"Input path is not a directory: {input_dir}", file=sys.stderr)
420
- return 1
421
- input_images = discover_input_images(input_dir)
422
- if not input_images:
423
- print(
424
- f"No supported images found in {input_dir}. "
425
- f"Supported extensions: {', '.join(sorted(SUPPORTED_IMAGE_SUFFIXES))}.",
426
- file=sys.stderr,
427
- )
428
- return 1
429
-
430
- output_dir = args.output_dir.expanduser().resolve()
431
- output_dir.mkdir(parents=True, exist_ok=True)
432
- converted_path = (
433
- args.converted_lora.expanduser().resolve()
434
- if args.converted_lora
435
- else output_dir / "pytorch_lora_weights.diffusers.safetensors"
436
- )
437
-
438
- try:
439
- conversion = convert_fal_lora_to_diffusers(lora_path, converted_path)
440
- validation = validate_converted_lora(converted_path)
441
- except Exception as exc:
442
- print(str(exc), file=sys.stderr)
443
- return 1
444
-
445
- report: dict[str, Any] = {
446
- "model": args.model,
447
- "text_encoder_mode": "local",
448
- "lora": str(lora_path),
449
- "converted_lora": str(converted_path),
450
- "conversion": conversion,
451
- "validation": validation,
452
- }
453
- print(json.dumps(report, indent=2, default=str))
454
-
455
- if not validation["valid"]:
456
- print("Converted LoRA did not validate against Flux2Transformer2DModel.", file=sys.stderr)
457
- return 1
458
- if args.check_only:
459
- return 0
460
- try:
461
- preflight_environment(args)
462
- except RuntimeError as exc:
463
- print(str(exc), file=sys.stderr)
464
- return 1
465
-
466
- started_at = time.time()
467
- try:
468
- image_paths = run_inference(args, converted_path)
469
- except Exception as exc:
470
- print(f"Local inference failed after {time.time() - started_at:.1f}s: {exc}", file=sys.stderr)
471
- return 1
472
-
473
- for image_path in image_paths:
474
- print(f"Saved local FLUX.2 edit output: {image_path}")
475
- return 0
476
-
477
-
478
- if __name__ == "__main__":
479
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/scripts/train_dreambooth_lora_flux_lowmem.py DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env python
2
- """Compatibility wrapper for the packaged FLUX low-memory trainer."""
3
-
4
- from lora.trainers.flux_lowmem import main, parse_args
5
-
6
-
7
- if __name__ == "__main__":
8
- main(parse_args())
 
 
 
 
 
 
 
 
 
code/scripts/train_flux2_edit_lora_fal.py DELETED
@@ -1,326 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import argparse
5
- import json
6
- import os
7
- import re
8
- import sys
9
- import tempfile
10
- import time
11
- import urllib.parse
12
- import urllib.request
13
- import zipfile
14
- from pathlib import Path
15
- from typing import Any
16
-
17
-
18
- REPO_ROOT = Path(__file__).resolve().parents[1]
19
- DEFAULT_TRAIN_DIR = REPO_ROOT / "training_data" / "flux_aura_style" / "train"
20
- DEFAULT_OUTPUT_DIR = REPO_ROOT / "outputs" / "fal_flux2_edit_lora"
21
- DEFAULT_ENDPOINT = "fal-ai/flux-2-trainer-v2/edit"
22
- DEFAULT_CAPTION = (
23
- "Apply a radiant aura lighting style with smooth colorful gradients, ethereal haze, subtle "
24
- "contour lighting, and a refined cinematic glow while preserving the subject and composition."
25
- )
26
-
27
-
28
- def parse_args() -> argparse.Namespace:
29
- parser = argparse.ArgumentParser(
30
- description="Package paired image-edit data and submit a FLUX.2 edit LoRA training job to fal."
31
- )
32
- parser.add_argument(
33
- "--train-dir",
34
- type=Path,
35
- default=DEFAULT_TRAIN_DIR,
36
- help="Prepared train directory containing metadata.jsonl and target images.",
37
- )
38
- parser.add_argument(
39
- "--output-dir",
40
- type=Path,
41
- default=DEFAULT_OUTPUT_DIR,
42
- help="Directory for the packaged zip, result JSON, and downloaded LoRA files.",
43
- )
44
- parser.add_argument(
45
- "--zip-path",
46
- type=Path,
47
- default=None,
48
- help="Optional explicit path for the generated training zip.",
49
- )
50
- parser.add_argument(
51
- "--default-caption",
52
- default=DEFAULT_CAPTION,
53
- help="Fallback edit instruction passed to fal if any pair has no prompt text file.",
54
- )
55
- parser.add_argument(
56
- "--steps",
57
- type=int,
58
- default=1000,
59
- help="Training steps. fal accepts 100 to 10000 in increments of 100.",
60
- )
61
- parser.add_argument(
62
- "--learning-rate",
63
- type=float,
64
- default=0.00005,
65
- help="LoRA learning rate.",
66
- )
67
- parser.add_argument(
68
- "--output-lora-format",
69
- choices=("fal", "comfy"),
70
- default="fal",
71
- help="Output weight naming format.",
72
- )
73
- parser.add_argument(
74
- "--endpoint",
75
- default=DEFAULT_ENDPOINT,
76
- help="fal endpoint id.",
77
- )
78
- parser.add_argument(
79
- "--limit",
80
- type=int,
81
- default=None,
82
- help="Maximum number of paired examples to package.",
83
- )
84
- parser.add_argument(
85
- "--start-pair",
86
- type=int,
87
- default=None,
88
- help="First pair number to include, inclusive.",
89
- )
90
- parser.add_argument(
91
- "--end-pair",
92
- type=int,
93
- default=None,
94
- help="Last pair number to include, inclusive.",
95
- )
96
- parser.add_argument(
97
- "--no-download",
98
- action="store_true",
99
- help="Do not download result files after training completes.",
100
- )
101
- parser.add_argument(
102
- "--package-only",
103
- action="store_true",
104
- help="Only create the zip; do not upload or start training.",
105
- )
106
- parser.add_argument(
107
- "--dry-run",
108
- action="store_true",
109
- help="Print selected pairs without creating a zip or calling fal.",
110
- )
111
- return parser.parse_args()
112
-
113
-
114
- def pair_number(path: str | Path) -> int:
115
- match = re.fullmatch(r"pair-(\d+)\.[^.]+", Path(path).name)
116
- if not match:
117
- raise ValueError(f"Not a pair file name: {path}")
118
- return int(match.group(1))
119
-
120
-
121
- def read_metadata(metadata_path: Path) -> list[dict[str, Any]]:
122
- with metadata_path.open("r", encoding="utf-8") as fh:
123
- return [json.loads(line) for line in fh if line.strip()]
124
-
125
-
126
- def selected_pair_rows(args: argparse.Namespace) -> list[dict[str, Any]]:
127
- metadata_path = args.train_dir / "metadata.jsonl"
128
- if not metadata_path.exists():
129
- raise FileNotFoundError(f"Metadata file does not exist: {metadata_path}")
130
-
131
- rows = []
132
- for row in read_metadata(metadata_path):
133
- if row.get("kind") != "paired":
134
- continue
135
- file_name = row.get("file_name")
136
- conditioning_path = row.get("conditioning_path")
137
- if not isinstance(file_name, str) or not isinstance(conditioning_path, str):
138
- continue
139
- number = pair_number(file_name)
140
- if args.start_pair is not None and number < args.start_pair:
141
- continue
142
- if args.end_pair is not None and number > args.end_pair:
143
- continue
144
- rows.append(row)
145
-
146
- rows.sort(key=lambda row: pair_number(row["file_name"]))
147
- if args.limit is not None:
148
- rows = rows[: args.limit]
149
- return rows
150
-
151
-
152
- def resolve_conditioning_path(train_dir: Path, value: str) -> Path:
153
- path = Path(value)
154
- if path.is_absolute():
155
- return path
156
- return (train_dir / path).resolve()
157
-
158
-
159
- def validate_rows(train_dir: Path, rows: list[dict[str, Any]]) -> None:
160
- for row in rows:
161
- target_path = train_dir / row["file_name"]
162
- conditioning_path = resolve_conditioning_path(train_dir, row["conditioning_path"])
163
- if not target_path.exists():
164
- raise FileNotFoundError(f"Missing target image: {target_path}")
165
- if not conditioning_path.exists():
166
- raise FileNotFoundError(f"Missing conditioning image: {conditioning_path}")
167
-
168
-
169
- def build_zip(train_dir: Path, rows: list[dict[str, Any]], zip_path: Path) -> None:
170
- zip_path.parent.mkdir(parents=True, exist_ok=True)
171
- with tempfile.NamedTemporaryFile(
172
- prefix=f".{zip_path.name}.",
173
- suffix=".tmp",
174
- dir=zip_path.parent,
175
- delete=False,
176
- ) as tmp:
177
- tmp_path = Path(tmp.name)
178
-
179
- try:
180
- with zipfile.ZipFile(tmp_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
181
- for row in rows:
182
- target_path = train_dir / row["file_name"]
183
- conditioning_path = resolve_conditioning_path(train_dir, row["conditioning_path"])
184
- stem = Path(row["file_name"]).stem
185
-
186
- archive.write(conditioning_path, f"{stem}_start{conditioning_path.suffix.lower()}")
187
- archive.write(target_path, f"{stem}_end{target_path.suffix.lower()}")
188
-
189
- prompt = row.get("prompt")
190
- if isinstance(prompt, str) and prompt.strip():
191
- archive.writestr(f"{stem}.txt", prompt.strip())
192
-
193
- tmp_path.replace(zip_path)
194
- except Exception:
195
- tmp_path.unlink(missing_ok=True)
196
- raise
197
-
198
-
199
- def on_queue_update(update: object) -> None:
200
- try:
201
- import fal_client
202
- except ImportError:
203
- return
204
-
205
- if isinstance(update, fal_client.InProgress) and update.logs:
206
- for log in update.logs:
207
- message = log.get("message")
208
- if message:
209
- print(message, flush=True)
210
-
211
-
212
- def train_with_fal(args: argparse.Namespace, zip_path: Path) -> tuple[dict[str, Any], str | None]:
213
- try:
214
- import fal_client
215
- except ImportError as exc:
216
- raise RuntimeError(
217
- "fal-client is not installed. Run `uv sync` after this script was added, or install "
218
- "it with `uv pip install fal-client`."
219
- ) from exc
220
-
221
- image_data_url = fal_client.upload_file(str(zip_path)) # type: ignore
222
- print(f"Uploaded dataset zip: {image_data_url}", flush=True)
223
-
224
- result = fal_client.subscribe(
225
- args.endpoint,
226
- arguments={
227
- "image_data_url": image_data_url,
228
- "steps": args.steps,
229
- "learning_rate": args.learning_rate,
230
- "default_caption": args.default_caption,
231
- "output_lora_format": args.output_lora_format,
232
- },
233
- with_logs=True,
234
- on_queue_update=on_queue_update,
235
- )
236
- if hasattr(result, "data"):
237
- return dict(result.data), getattr(result, "request_id", None)
238
- return dict(result), None
239
-
240
-
241
- def download_file(file_info: dict[str, Any], output_dir: Path) -> Path | None:
242
- url = file_info.get("url")
243
- if not isinstance(url, str) or not url:
244
- return None
245
-
246
- file_name = file_info.get("file_name")
247
- if not isinstance(file_name, str) or not file_name:
248
- file_name = Path(urllib.parse.urlparse(url).path).name or "downloaded-file"
249
-
250
- output_path = output_dir / file_name
251
- urllib.request.urlretrieve(url, output_path)
252
- return output_path
253
-
254
-
255
- def main() -> int:
256
- args = parse_args()
257
- train_dir = args.train_dir.resolve()
258
- output_dir = args.output_dir.resolve()
259
- zip_path = (args.zip_path or (output_dir / "flux2-edit-lora-pairs.zip")).resolve()
260
-
261
- if args.steps < 100 or args.steps > 10000 or args.steps % 100 != 0:
262
- print("--steps must be between 100 and 10000, in increments of 100.", file=sys.stderr)
263
- return 1
264
- if not args.package_only and not args.dry_run and not os.environ.get("FAL_KEY"):
265
- print("Missing fal key. Set FAL_KEY before submitting training.", file=sys.stderr)
266
- return 1
267
-
268
- rows = selected_pair_rows(args)
269
- if not rows:
270
- print("No paired examples selected.", file=sys.stderr)
271
- return 1
272
- validate_rows(train_dir, rows)
273
-
274
- print(
275
- json.dumps(
276
- {
277
- "pairs": len(rows),
278
- "first_pair": rows[0]["file_name"],
279
- "last_pair": rows[-1]["file_name"],
280
- "zip_path": str(zip_path),
281
- "endpoint": args.endpoint,
282
- "steps": args.steps,
283
- "learning_rate": args.learning_rate,
284
- "package_only": args.package_only,
285
- "dry_run": args.dry_run,
286
- },
287
- indent=2,
288
- )
289
- )
290
-
291
- if args.dry_run:
292
- for row in rows:
293
- print(f"{row['conditioning_path']} -> {row['file_name']}")
294
- return 0
295
-
296
- build_zip(train_dir, rows, zip_path)
297
- print(f"Wrote dataset zip: {zip_path} ({zip_path.stat().st_size:,} bytes)")
298
-
299
- if args.package_only:
300
- return 0
301
-
302
- output_dir.mkdir(parents=True, exist_ok=True)
303
- started_at = time.time()
304
- result, request_id = train_with_fal(args, zip_path)
305
- elapsed = time.time() - started_at
306
-
307
- result_path = output_dir / "fal-training-result.json"
308
- result_path.write_text(
309
- json.dumps({"request_id": request_id, "elapsed_seconds": elapsed, "result": result}, indent=2),
310
- encoding="utf-8",
311
- )
312
- print(f"Wrote result JSON: {result_path}")
313
-
314
- if not args.no_download:
315
- for key in ("diffusers_lora_file", "config_file"):
316
- value = result.get(key)
317
- if isinstance(value, dict):
318
- downloaded = download_file(value, output_dir)
319
- if downloaded is not None:
320
- print(f"Downloaded {key}: {downloaded}")
321
-
322
- return 0
323
-
324
-
325
- if __name__ == "__main__":
326
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/src/lora/__init__.py DELETED
@@ -1,3 +0,0 @@
1
- __all__ = ["__version__"]
2
-
3
- __version__ = "0.1.0"
 
 
 
 
code/src/lora/cli.py DELETED
@@ -1,442 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
- import os
6
- import re
7
- import shutil
8
- import subprocess
9
- import sys
10
- import textwrap
11
- import tomllib
12
- from dataclasses import dataclass
13
- from importlib.util import find_spec
14
- from pathlib import Path
15
- from typing import Any, Protocol, cast
16
-
17
-
18
- REPO_ROOT = Path(__file__).resolve().parents[2]
19
- DEFAULT_CONFIG = REPO_ROOT / "configs" / "flux_lora.toml"
20
-
21
-
22
- class CommandFunc(Protocol):
23
- def __call__(self, args: argparse.Namespace) -> int: ...
24
-
25
-
26
- class PipelineResult(Protocol):
27
- images: list[Any]
28
-
29
-
30
- @dataclass
31
- class PreparedItem:
32
- image_source: Path
33
- output_name: str
34
- prompt: str
35
- conditioning_source: Path | None = None
36
- kind: str = "label"
37
-
38
-
39
- def load_config(path: Path) -> dict[str, Any]:
40
- with path.open("rb") as fh:
41
- return tomllib.load(fh)
42
-
43
-
44
- def parse_bool(value: bool) -> bool:
45
- return bool(value)
46
-
47
-
48
- def normalize_subject(value: str) -> str:
49
- return value.lower().replace("-", " ").replace("_", " ").strip()
50
-
51
-
52
- def build_prompt(subject: str, prompt_cfg: dict[str, Any]) -> str:
53
- return prompt_cfg["prompt_template"].format(subject=subject)
54
-
55
-
56
- def convert_image(source: Path, target: Path) -> None:
57
- from PIL import Image, ImageOps
58
- from PIL.Image import Image as PILImage
59
-
60
- with Image.open(source) as image:
61
- transposed = cast(PILImage, ImageOps.exif_transpose(image))
62
- rgb_image = transposed.convert("RGB")
63
- rgb_image.save(target, format="PNG", optimize=True)
64
-
65
-
66
- def prepare_dataset(args: argparse.Namespace) -> int:
67
- if find_spec("PIL") is None:
68
- print("Pillow is required. Install project dependencies first.", file=sys.stderr)
69
- return 1
70
-
71
- config = load_config(Path(args.config))
72
- dataset_cfg = config["dataset"]
73
- prompt_cfg = config["prompts"]
74
-
75
- source_dir = REPO_ROOT / dataset_cfg["source_dir"]
76
- output_dir = REPO_ROOT / dataset_cfg["prepared_dir"]
77
- image_dir = output_dir / "train"
78
- conditioning_dir = output_dir / "conditioning"
79
- metadata_path = image_dir / "metadata.jsonl"
80
-
81
- if output_dir.exists():
82
- shutil.rmtree(output_dir)
83
-
84
- output_dir.mkdir(parents=True, exist_ok=True)
85
- image_dir.mkdir(parents=True, exist_ok=True)
86
- conditioning_dir.mkdir(parents=True, exist_ok=True)
87
-
88
- supported = {".jpg", ".jpeg", ".png", ".webp", ".avif"}
89
- files = sorted(
90
- path for path in source_dir.iterdir() if path.is_file() and path.suffix.lower() in supported
91
- )
92
- if not files:
93
- print(f"No supported images found in {source_dir}", file=sys.stderr)
94
- return 1
95
-
96
- source_pattern = re.compile(r"^(?P<index>\d+)$")
97
- output_pattern = re.compile(r"^(?P<index>\d+)-output$")
98
- pairs: dict[str, dict[str, Path]] = {}
99
- label_images: list[Path] = []
100
-
101
- for path in files:
102
- stem = path.stem
103
- source_match = source_pattern.match(stem)
104
- output_match = output_pattern.match(stem)
105
- if output_match:
106
- pairs.setdefault(output_match.group("index"), {})["output"] = path
107
- continue
108
- if source_match:
109
- pairs.setdefault(source_match.group("index"), {})["source"] = path
110
- continue
111
- label_images.append(path)
112
-
113
- items: list[PreparedItem] = []
114
- pair_subject = prompt_cfg["paired_subject"]
115
- for pair_index in sorted(pairs, key=lambda value: int(value)):
116
- pair = pairs[pair_index]
117
- source = pair.get("source")
118
- output = pair.get("output")
119
- if source is None or output is None:
120
- missing = "source" if source is None else "output"
121
- print(f"Skipping pair {pair_index}: missing {missing} image.", file=sys.stderr)
122
- continue
123
-
124
- items.append(
125
- PreparedItem(
126
- image_source=output,
127
- conditioning_source=source,
128
- output_name=f"pair-{int(pair_index):03d}.png",
129
- prompt=build_prompt(pair_subject, prompt_cfg),
130
- kind="paired",
131
- )
132
- )
133
-
134
- for label_index, image in enumerate(sorted(label_images), start=1):
135
- items.append(
136
- PreparedItem(
137
- image_source=image,
138
- output_name=f"label-{label_index:03d}.png",
139
- prompt=build_prompt(normalize_subject(image.stem), prompt_cfg),
140
- kind="label",
141
- )
142
- )
143
-
144
- for item in items:
145
- target = image_dir / item.output_name
146
- convert_image(item.image_source, target)
147
- if item.conditioning_source is not None:
148
- convert_image(item.conditioning_source, conditioning_dir / item.output_name)
149
-
150
- with metadata_path.open("w", encoding="utf-8") as fh:
151
- for item in items:
152
- row = {
153
- "file_name": item.output_name,
154
- "prompt": item.prompt,
155
- "kind": item.kind,
156
- }
157
- if item.conditioning_source is not None:
158
- row["conditioning_path"] = f"../conditioning/{item.output_name}"
159
- fh.write(json.dumps(row) + "\n")
160
-
161
- summary = {
162
- "prepared_count": len(items),
163
- "paired_count": sum(1 for item in items if item.kind == "paired"),
164
- "label_count": sum(1 for item in items if item.kind == "label"),
165
- "prepared_dir": str(output_dir),
166
- }
167
- print(json.dumps(summary, indent=2))
168
- return 0
169
-
170
-
171
- def run_checked(cmd: list[str], cwd: Path | None = None) -> None:
172
- subprocess.run(cmd, cwd=cwd, check=True)
173
-
174
-
175
- def missing_modules(module_names: list[str]) -> list[str]:
176
- return [module_name for module_name in module_names if find_spec(module_name) is None]
177
-
178
-
179
- def ensure_diffusers_checkout(cache_dir: Path, ref: str) -> Path:
180
- repo_dir = cache_dir / "diffusers"
181
- if not repo_dir.exists():
182
- run_checked(["git", "clone", "https://github.com/huggingface/diffusers.git", str(repo_dir)])
183
- run_checked(["git", "fetch", "origin"], cwd=repo_dir)
184
- run_checked(["git", "checkout", ref], cwd=repo_dir)
185
- return repo_dir
186
-
187
-
188
- def append_flag(command: list[str], name: str, value: Any) -> None:
189
- if value is None:
190
- return
191
- if isinstance(value, bool):
192
- if value:
193
- command.append(name)
194
- return
195
- command.extend([name, str(value)])
196
-
197
-
198
- def train(args: argparse.Namespace) -> int:
199
- config = load_config(Path(args.config))
200
- train_cfg = config["training"]
201
- prompt_cfg = config["prompts"]
202
-
203
- missing = missing_modules(
204
- [
205
- "accelerate",
206
- "datasets",
207
- "diffusers",
208
- "ftfy",
209
- "hf_transfer",
210
- "peft",
211
- "sentencepiece",
212
- "tensorboard",
213
- "torch",
214
- "torchvision",
215
- "transformers",
216
- ]
217
- )
218
- if missing:
219
- print(
220
- "Training dependencies are missing: "
221
- + ", ".join(missing)
222
- + ". Run `uv sync` or `lora install` before `lora train`.",
223
- file=sys.stderr,
224
- )
225
- return 1
226
-
227
- prepared_dir = REPO_ROOT / config["dataset"]["prepared_dir"]
228
- if not prepared_dir.exists():
229
- print(
230
- "Prepared dataset is missing. Run `lora prepare-dataset` first.",
231
- file=sys.stderr,
232
- )
233
- return 1
234
-
235
- command = [
236
- "accelerate",
237
- "launch",
238
- "-m",
239
- train_cfg.get("training_module", "lora.trainers.flux_lowmem"),
240
- "--pretrained_model_name_or_path",
241
- train_cfg["model_name"],
242
- "--dataset_name",
243
- str(prepared_dir),
244
- "--caption_column",
245
- "prompt",
246
- "--instance_prompt",
247
- prompt_cfg["instance_prompt"],
248
- "--output_dir",
249
- str(REPO_ROOT / train_cfg["output_dir"]),
250
- ]
251
-
252
- flags = {
253
- "--mixed_precision": train_cfg["mixed_precision"],
254
- "--resolution": train_cfg["resolution"],
255
- "--train_batch_size": train_cfg["train_batch_size"],
256
- "--gradient_accumulation_steps": train_cfg["gradient_accumulation_steps"],
257
- "--optimizer": train_cfg["optimizer"],
258
- "--learning_rate": train_cfg["learning_rate"],
259
- "--lr_scheduler": train_cfg["lr_scheduler"],
260
- "--lr_warmup_steps": train_cfg["lr_warmup_steps"],
261
- "--max_train_steps": train_cfg["max_train_steps"],
262
- "--rank": train_cfg["rank"],
263
- "--lora_alpha": train_cfg["lora_alpha"],
264
- "--validation_prompt": train_cfg.get("validation_prompt") or None,
265
- "--validation_epochs": train_cfg["validation_epochs"],
266
- "--num_validation_images": train_cfg["num_validation_images"],
267
- "--seed": train_cfg["seed"],
268
- "--report_to": train_cfg["report_to"],
269
- "--repeats": train_cfg["repeats"],
270
- "--max_sequence_length": train_cfg["max_sequence_length"],
271
- "--dataloader_num_workers": train_cfg.get("dataloader_num_workers"),
272
- }
273
- for flag_name, value in flags.items():
274
- append_flag(command, flag_name, value)
275
-
276
- if parse_bool(train_cfg.get("gradient_checkpointing", False)):
277
- command.append("--gradient_checkpointing")
278
- if parse_bool(train_cfg.get("cache_latents", False)):
279
- command.append("--cache_latents")
280
- if parse_bool(train_cfg.get("use_8bit_adam", False)):
281
- command.append("--use_8bit_adam")
282
- if parse_bool(train_cfg.get("push_to_hub", False)):
283
- command.append("--push_to_hub")
284
-
285
- env = os.environ.copy()
286
- env.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
287
-
288
- print("Launching training command:\n")
289
- print(" ".join(command))
290
- print()
291
- subprocess.run(command, cwd=REPO_ROOT, env=env, check=True)
292
- return 0
293
-
294
-
295
- def infer(args: argparse.Namespace) -> int:
296
- try:
297
- import diffusers # pyright: ignore[reportMissingImports]
298
- import torch
299
- except ImportError:
300
- print(
301
- "Inference requires diffusers and torch installed in the active environment.",
302
- file=sys.stderr,
303
- )
304
- return 1
305
-
306
- config = load_config(Path(args.config))
307
- infer_cfg = config["inference"]
308
- train_cfg = config["training"]
309
-
310
- lora_dir = REPO_ROOT / train_cfg["output_dir"]
311
- weight_name = infer_cfg["weight_name"]
312
- prompt = args.prompt or infer_cfg["prompt"]
313
- output_path = REPO_ROOT / infer_cfg["output_path"]
314
- output_path.parent.mkdir(parents=True, exist_ok=True)
315
-
316
- pipeline_cls = cast(Any, getattr(diffusers, "DiffusionPipeline"))
317
- pipe = cast(
318
- Any,
319
- pipeline_cls.from_pretrained(
320
- train_cfg["model_name"],
321
- torch_dtype=getattr(torch, "bfloat16"),
322
- ),
323
- )
324
- pipe.enable_model_cpu_offload()
325
- pipe.load_lora_weights(str(lora_dir), weight_name=weight_name)
326
-
327
- result = cast(
328
- PipelineResult,
329
- pipe(
330
- prompt=prompt,
331
- height=infer_cfg["height"],
332
- width=infer_cfg["width"],
333
- guidance_scale=infer_cfg["guidance_scale"],
334
- num_inference_steps=infer_cfg["num_inference_steps"],
335
- max_sequence_length=train_cfg["max_sequence_length"],
336
- ),
337
- )
338
- image = result.images[0]
339
- image.save(output_path)
340
- print(json.dumps({"prompt": prompt, "output_path": str(output_path)}, indent=2))
341
- return 0
342
-
343
-
344
- def install(args: argparse.Namespace) -> int:
345
- config = load_config(Path(args.config))
346
- train_cfg = config["training"]
347
- cache_dir = REPO_ROOT / ".cache"
348
- cache_dir.mkdir(exist_ok=True)
349
- diffusers_dir = ensure_diffusers_checkout(cache_dir, train_cfg["diffusers_ref"])
350
-
351
- steps = [
352
- [sys.executable, "-m", "pip", "install", "-e", "."],
353
- [sys.executable, "-m", "pip", "install", "-e", str(diffusers_dir)],
354
- [
355
- sys.executable,
356
- "-m",
357
- "pip",
358
- "install",
359
- "-r",
360
- str(diffusers_dir / "examples" / "dreambooth" / "requirements_flux.txt"),
361
- ],
362
- ]
363
- for step in steps:
364
- run_checked(step, cwd=REPO_ROOT)
365
-
366
- note = textwrap.dedent(
367
- """
368
- Environment bootstrap complete.
369
- Next steps:
370
- 1. Accept the gated model terms for black-forest-labs/FLUX.1-dev on Hugging Face.
371
- 2. Run `hf auth login`.
372
- 3. Run `accelerate config default`.
373
- 4. Run `lora prepare-dataset`.
374
- 5. Run `lora train`.
375
- """
376
- ).strip()
377
- print(note)
378
- return 0
379
-
380
-
381
- def clean(args: argparse.Namespace) -> int:
382
- config = load_config(Path(args.config))
383
- paths = [
384
- REPO_ROOT / config["dataset"]["prepared_dir"],
385
- REPO_ROOT / config["training"]["output_dir"],
386
- REPO_ROOT / config["inference"]["output_path"],
387
- ]
388
- for path in paths:
389
- if path.is_dir():
390
- shutil.rmtree(path)
391
- elif path.exists():
392
- path.unlink()
393
- print("Removed generated dataset, output weights, and sample image.")
394
- return 0
395
-
396
-
397
- def build_parser() -> argparse.ArgumentParser:
398
- parser = argparse.ArgumentParser(description="FLUX.1-dev LoRA workspace utilities.")
399
- parser.add_argument(
400
- "--config",
401
- default=str(DEFAULT_CONFIG),
402
- help="Path to the TOML config file.",
403
- )
404
-
405
- subparsers = parser.add_subparsers(dest="command", required=True)
406
-
407
- prepare_parser = subparsers.add_parser(
408
- "prepare-dataset", help="Normalize images into a local HF dataset."
409
- )
410
- prepare_parser.set_defaults(func=prepare_dataset)
411
-
412
- install_parser = subparsers.add_parser(
413
- "install", help="Install local and upstream training dependencies."
414
- )
415
- install_parser.set_defaults(func=install)
416
-
417
- train_parser = subparsers.add_parser(
418
- "train", help="Launch the official diffusers FLUX LoRA trainer."
419
- )
420
- train_parser.set_defaults(func=train)
421
-
422
- infer_parser = subparsers.add_parser(
423
- "infer", help="Run a quick inference pass with the trained LoRA."
424
- )
425
- infer_parser.add_argument("prompt", nargs="?", help="Optional prompt override.")
426
- infer_parser.set_defaults(func=infer)
427
-
428
- clean_parser = subparsers.add_parser("clean", help="Remove generated artifacts.")
429
- clean_parser.set_defaults(func=clean)
430
-
431
- return parser
432
-
433
-
434
- def main(argv: list[str] | None = None) -> int:
435
- parser = build_parser()
436
- args = parser.parse_args(argv)
437
- try:
438
- func = cast(CommandFunc, args.func)
439
- return func(args)
440
- except subprocess.CalledProcessError as exc:
441
- print(f"Command failed with exit code {exc.returncode}: {exc.cmd}", file=sys.stderr)
442
- return exc.returncode or 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/src/lora/trainers/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Training entrypoints for the lora package."""
 
 
code/src/lora/trainers/flux_lowmem.py DELETED
@@ -1,2059 +0,0 @@
1
- #!/usr/bin/env python
2
- # coding=utf-8
3
- # Copyright 2025 The HuggingFace Inc. team. All rights reserved.
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
-
17
- # pyright: reportAttributeAccessIssue=false, reportPrivateImportUsage=false, reportMissingImports=false, reportArgumentType=false, reportPossiblyUnboundVariable=false, reportOptionalSubscript=false, reportUndefinedVariable=false, reportCallIssue=false, reportAssignmentType=false, reportOptionalMemberAccess=false
18
-
19
- # /// script
20
- # dependencies = [
21
- # "diffusers @ git+https://github.com/huggingface/diffusers.git",
22
- # "torch>=2.0.0",
23
- # "accelerate>=0.31.0",
24
- # "transformers>=4.41.2",
25
- # "ftfy",
26
- # "tensorboard",
27
- # "Jinja2",
28
- # "peft>=0.11.1",
29
- # "sentencepiece",
30
- # "torchvision",
31
- # "datasets",
32
- # "bitsandbytes",
33
- # "prodigyopt",
34
- # ]
35
- # ///
36
-
37
- from __future__ import annotations
38
-
39
- import argparse
40
- import copy
41
- import itertools
42
- import logging
43
- import math
44
- import os
45
- import random
46
- import shutil
47
- import warnings
48
- from contextlib import nullcontext
49
- from pathlib import Path
50
- from typing import TYPE_CHECKING
51
-
52
- import numpy as np
53
- import torch
54
- import transformers
55
- from accelerate import Accelerator
56
- from accelerate.logging import get_logger
57
- from accelerate.utils import DistributedDataParallelKwargs, ProjectConfiguration, set_seed
58
- from huggingface_hub import create_repo, upload_folder
59
- from huggingface_hub.utils import insecure_hashlib
60
- from peft import LoraConfig, set_peft_model_state_dict
61
- from peft.utils import get_peft_model_state_dict
62
- from PIL import Image
63
- from PIL.ImageOps import exif_transpose
64
- from torch.utils.data import Dataset
65
- from torchvision import transforms
66
- from torchvision.transforms.functional import crop
67
- from tqdm.auto import tqdm
68
- from transformers import CLIPTokenizer, PretrainedConfig, T5TokenizerFast
69
-
70
- import diffusers
71
- from diffusers import (
72
- AutoencoderKL,
73
- FlowMatchEulerDiscreteScheduler,
74
- FluxPipeline,
75
- FluxTransformer2DModel,
76
- )
77
- from diffusers.models.autoencoders.vae import DiagonalGaussianDistribution
78
- from diffusers.optimization import get_scheduler
79
- from diffusers.training_utils import (
80
- _collate_lora_metadata,
81
- _set_state_dict_into_text_encoder,
82
- cast_training_params,
83
- compute_density_for_timestep_sampling,
84
- compute_loss_weighting_for_sd3,
85
- free_memory,
86
- )
87
- from diffusers.utils import (
88
- check_min_version,
89
- convert_unet_state_dict_to_peft,
90
- is_wandb_available,
91
- )
92
- from diffusers.utils.hub_utils import load_or_create_model_card, populate_model_card
93
- from diffusers.utils.import_utils import is_torch_npu_available
94
- from diffusers.utils.torch_utils import is_compiled_module
95
-
96
- if TYPE_CHECKING:
97
- from collections.abc import Sequence
98
-
99
- from PIL.Image import Image as PILImage
100
-
101
-
102
- if is_wandb_available():
103
- import wandb
104
-
105
- # Will error if the minimal version of diffusers is not installed. Remove at your own risks.
106
- check_min_version("0.39.0.dev0")
107
-
108
- logger = get_logger(__name__)
109
-
110
-
111
- def save_model_card(
112
- repo_id: str,
113
- images: list[PILImage] | None = None,
114
- base_model: str | None = None,
115
- train_text_encoder: bool = False,
116
- instance_prompt: str | None = None,
117
- validation_prompt: str | None = None,
118
- repo_folder: str | os.PathLike[str] | None = None,
119
- ) -> None:
120
- if repo_folder is None:
121
- raise ValueError("repo_folder must be provided when saving a model card.")
122
-
123
- repo_folder_path = os.fspath(repo_folder)
124
- widget_dict: list[dict[str, object]] = []
125
- if images is not None:
126
- for i, image in enumerate(images):
127
- image.save(os.path.join(repo_folder_path, f"image_{i}.png"))
128
- widget_dict.append(
129
- {"text": validation_prompt if validation_prompt else " ", "output": {"url": f"image_{i}.png"}}
130
- )
131
-
132
- model_description = f"""
133
- # Flux DreamBooth LoRA - {repo_id}
134
-
135
- <Gallery />
136
-
137
- ## Model description
138
-
139
- These are {repo_id} DreamBooth LoRA weights for {base_model}.
140
-
141
- The weights were trained using [DreamBooth](https://dreambooth.github.io/) with the [Flux diffusers trainer](https://github.com/huggingface/diffusers/blob/main/examples/dreambooth/README_flux.md).
142
-
143
- Was LoRA for the text encoder enabled? {train_text_encoder}.
144
-
145
- ## Trigger words
146
-
147
- You should use `{instance_prompt}` to trigger the image generation.
148
-
149
- ## Download model
150
-
151
- [Download the *.safetensors LoRA]({repo_id}/tree/main) in the Files & versions tab.
152
-
153
- ## Use it with the [🧨 diffusers library](https://github.com/huggingface/diffusers)
154
-
155
- ```py
156
- from diffusers import AutoPipelineForText2Image
157
- import torch
158
- pipeline = AutoPipelineForText2Image.from_pretrained("black-forest-labs/FLUX.1-dev", torch_dtype=torch.bfloat16).to('cuda')
159
- pipeline.load_lora_weights('{repo_id}', weight_name='pytorch_lora_weights.safetensors')
160
- image = pipeline('{validation_prompt if validation_prompt else instance_prompt}').images[0]
161
- ```
162
-
163
- For more details, including weighting, merging and fusing LoRAs, check the [documentation on loading LoRAs in diffusers](https://huggingface.co/docs/diffusers/main/en/using-diffusers/loading_adapters)
164
-
165
- ## License
166
-
167
- Please adhere to the licensing terms as described [here](https://huggingface.co/black-forest-labs/FLUX.1-dev/blob/main/LICENSE.md).
168
- """
169
- model_card = load_or_create_model_card(
170
- repo_id_or_path=repo_id,
171
- from_training=True,
172
- license="other",
173
- base_model=base_model,
174
- prompt=instance_prompt,
175
- model_description=model_description,
176
- widget=widget_dict,
177
- )
178
- tags = [
179
- "text-to-image",
180
- "diffusers-training",
181
- "diffusers",
182
- "lora",
183
- "flux",
184
- "flux-diffusers",
185
- "template:sd-lora",
186
- ]
187
-
188
- model_card = populate_model_card(model_card, tags=tags)
189
- model_card.save(os.path.join(repo_folder_path, "README.md"))
190
-
191
-
192
- def load_text_encoders(
193
- args: argparse.Namespace,
194
- class_one: type,
195
- class_two: type,
196
- torch_dtype: torch.dtype | None = None,
197
- ):
198
- text_encoder_one = class_one.from_pretrained(
199
- args.pretrained_model_name_or_path,
200
- subfolder="text_encoder",
201
- revision=args.revision,
202
- variant=args.variant,
203
- torch_dtype=torch_dtype,
204
- )
205
- text_encoder_two = class_two.from_pretrained(
206
- args.pretrained_model_name_or_path,
207
- subfolder="text_encoder_2",
208
- revision=args.revision,
209
- variant=args.variant,
210
- torch_dtype=torch_dtype,
211
- )
212
- return text_encoder_one, text_encoder_two
213
-
214
-
215
- def log_validation(
216
- pipeline,
217
- args,
218
- accelerator,
219
- pipeline_args,
220
- epoch,
221
- torch_dtype,
222
- is_final_validation=False,
223
- ):
224
- logger.info(
225
- f"Running validation... \n Generating {args.num_validation_images} images with prompt:"
226
- f" {args.validation_prompt}."
227
- )
228
- pipeline = pipeline.to(accelerator.device, dtype=torch_dtype)
229
- pipeline.set_progress_bar_config(disable=True)
230
-
231
- # run inference
232
- generator = torch.Generator(device=accelerator.device).manual_seed(args.seed) if args.seed is not None else None
233
- autocast_ctx = torch.autocast(accelerator.device.type) if not is_final_validation else nullcontext()
234
-
235
- # pre-calculate prompt embeds, pooled prompt embeds, text ids because t5 does not support autocast
236
- with torch.no_grad():
237
- prompt_embeds, pooled_prompt_embeds, text_ids = pipeline.encode_prompt(
238
- pipeline_args["prompt"], prompt_2=pipeline_args["prompt"]
239
- )
240
- images = []
241
- for _ in range(args.num_validation_images):
242
- with autocast_ctx:
243
- image = pipeline(
244
- prompt_embeds=prompt_embeds, pooled_prompt_embeds=pooled_prompt_embeds, generator=generator
245
- ).images[0]
246
- images.append(image)
247
-
248
- for tracker in accelerator.trackers:
249
- phase_name = "test" if is_final_validation else "validation"
250
- if tracker.name == "tensorboard":
251
- np_images = np.stack([np.asarray(img) for img in images])
252
- tracker.writer.add_images(phase_name, np_images, epoch, dataformats="NHWC")
253
- if tracker.name == "wandb":
254
- tracker.log(
255
- {
256
- phase_name: [
257
- wandb.Image(image, caption=f"{i}: {args.validation_prompt}") for i, image in enumerate(images)
258
- ]
259
- }
260
- )
261
-
262
- del pipeline
263
- free_memory()
264
-
265
- return images
266
-
267
-
268
- def import_model_class_from_model_name_or_path(
269
- pretrained_model_name_or_path: str, revision: str, subfolder: str = "text_encoder"
270
- ):
271
- text_encoder_config = PretrainedConfig.from_pretrained(
272
- pretrained_model_name_or_path, subfolder=subfolder, revision=revision
273
- )
274
- model_class = text_encoder_config.architectures[0]
275
- if model_class == "CLIPTextModel":
276
- from transformers import CLIPTextModel
277
-
278
- return CLIPTextModel
279
- elif model_class == "T5EncoderModel":
280
- from transformers import T5EncoderModel
281
-
282
- return T5EncoderModel
283
- else:
284
- raise ValueError(f"{model_class} is not supported.")
285
-
286
-
287
- def parse_args(input_args: Sequence[str] | None = None) -> argparse.Namespace:
288
- parser = argparse.ArgumentParser(description="Simple example of a training script.")
289
- parser.add_argument(
290
- "--pretrained_model_name_or_path",
291
- type=str,
292
- default=None,
293
- required=True,
294
- help="Path to pretrained model or model identifier from huggingface.co/models.",
295
- )
296
- parser.add_argument(
297
- "--revision",
298
- type=str,
299
- default=None,
300
- required=False,
301
- help="Revision of pretrained model identifier from huggingface.co/models.",
302
- )
303
- parser.add_argument(
304
- "--variant",
305
- type=str,
306
- default=None,
307
- help="Variant of the model files of the pretrained model identifier from huggingface.co/models, 'e.g.' fp16",
308
- )
309
- parser.add_argument(
310
- "--dataset_name",
311
- type=str,
312
- default=None,
313
- help=(
314
- "The name of the Dataset (from the HuggingFace hub) containing the training data of instance images (could be your own, possibly private,"
315
- " dataset). It can also be a path pointing to a local copy of a dataset in your filesystem,"
316
- " or to a folder containing files that 🤗 Datasets can understand."
317
- ),
318
- )
319
- parser.add_argument(
320
- "--dataset_config_name",
321
- type=str,
322
- default=None,
323
- help="The config of the Dataset, leave as None if there's only one config.",
324
- )
325
- parser.add_argument(
326
- "--instance_data_dir",
327
- type=str,
328
- default=None,
329
- help=("A folder containing the training data. "),
330
- )
331
-
332
- parser.add_argument(
333
- "--cache_dir",
334
- type=str,
335
- default=None,
336
- help="The directory where the downloaded models and datasets will be stored.",
337
- )
338
-
339
- parser.add_argument(
340
- "--image_column",
341
- type=str,
342
- default="image",
343
- help="The column of the dataset containing the target image. By "
344
- "default, the standard Image Dataset maps out 'file_name' "
345
- "to 'image'.",
346
- )
347
- parser.add_argument(
348
- "--caption_column",
349
- type=str,
350
- default=None,
351
- help="The column of the dataset containing the instance prompt for each image",
352
- )
353
-
354
- parser.add_argument("--repeats", type=int, default=1, help="How many times to repeat the training data.")
355
-
356
- parser.add_argument(
357
- "--class_data_dir",
358
- type=str,
359
- default=None,
360
- required=False,
361
- help="A folder containing the training data of class images.",
362
- )
363
- parser.add_argument(
364
- "--instance_prompt",
365
- type=str,
366
- default=None,
367
- required=True,
368
- help="The prompt with identifier specifying the instance, e.g. 'photo of a TOK dog', 'in the style of TOK'",
369
- )
370
- parser.add_argument(
371
- "--class_prompt",
372
- type=str,
373
- default=None,
374
- help="The prompt to specify images in the same class as provided instance images.",
375
- )
376
- parser.add_argument(
377
- "--max_sequence_length",
378
- type=int,
379
- default=512,
380
- help="Maximum sequence length to use with with the T5 text encoder",
381
- )
382
- parser.add_argument(
383
- "--validation_prompt",
384
- type=str,
385
- default=None,
386
- help="A prompt that is used during validation to verify that the model is learning.",
387
- )
388
- parser.add_argument(
389
- "--num_validation_images",
390
- type=int,
391
- default=4,
392
- help="Number of images that should be generated during validation with `validation_prompt`.",
393
- )
394
- parser.add_argument(
395
- "--validation_epochs",
396
- type=int,
397
- default=50,
398
- help=(
399
- "Run dreambooth validation every X epochs. Dreambooth validation consists of running the prompt"
400
- " `args.validation_prompt` multiple times: `args.num_validation_images`."
401
- ),
402
- )
403
- parser.add_argument(
404
- "--rank",
405
- type=int,
406
- default=4,
407
- help=("The dimension of the LoRA update matrices."),
408
- )
409
- parser.add_argument(
410
- "--lora_alpha",
411
- type=int,
412
- default=4,
413
- help="LoRA alpha to be used for additional scaling.",
414
- )
415
- parser.add_argument("--lora_dropout", type=float, default=0.0, help="Dropout probability for LoRA layers")
416
-
417
- parser.add_argument(
418
- "--with_prior_preservation",
419
- default=False,
420
- action="store_true",
421
- help="Flag to add prior preservation loss.",
422
- )
423
- parser.add_argument("--prior_loss_weight", type=float, default=1.0, help="The weight of prior preservation loss.")
424
- parser.add_argument(
425
- "--num_class_images",
426
- type=int,
427
- default=100,
428
- help=(
429
- "Minimal class images for prior preservation loss. If there are not enough images already present in"
430
- " class_data_dir, additional images will be sampled with class_prompt."
431
- ),
432
- )
433
- parser.add_argument(
434
- "--output_dir",
435
- type=str,
436
- default="flux-dreambooth-lora",
437
- help="The output directory where the model predictions and checkpoints will be written.",
438
- )
439
- parser.add_argument("--seed", type=int, default=None, help="A seed for reproducible training.")
440
- parser.add_argument(
441
- "--resolution",
442
- type=int,
443
- default=512,
444
- help=(
445
- "The resolution for input images, all the images in the train/validation dataset will be resized to this"
446
- " resolution"
447
- ),
448
- )
449
- parser.add_argument(
450
- "--center_crop",
451
- default=False,
452
- action="store_true",
453
- help=(
454
- "Whether to center crop the input images to the resolution. If not set, the images will be randomly"
455
- " cropped. The images will be resized to the resolution first before cropping."
456
- ),
457
- )
458
- parser.add_argument(
459
- "--random_flip",
460
- action="store_true",
461
- help="whether to randomly flip images horizontally",
462
- )
463
- parser.add_argument(
464
- "--train_text_encoder",
465
- action="store_true",
466
- help="Whether to train the text encoder. If set, the text encoder should be float32 precision.",
467
- )
468
- parser.add_argument(
469
- "--train_batch_size", type=int, default=4, help="Batch size (per device) for the training dataloader."
470
- )
471
- parser.add_argument(
472
- "--sample_batch_size", type=int, default=4, help="Batch size (per device) for sampling images."
473
- )
474
- parser.add_argument("--num_train_epochs", type=int, default=1)
475
- parser.add_argument(
476
- "--max_train_steps",
477
- type=int,
478
- default=None,
479
- help="Total number of training steps to perform. If provided, overrides num_train_epochs.",
480
- )
481
- parser.add_argument(
482
- "--checkpointing_steps",
483
- type=int,
484
- default=500,
485
- help=(
486
- "Save a checkpoint of the training state every X updates. These checkpoints can be used both as final"
487
- " checkpoints in case they are better than the last checkpoint, and are also suitable for resuming"
488
- " training using `--resume_from_checkpoint`."
489
- ),
490
- )
491
- parser.add_argument(
492
- "--checkpoints_total_limit",
493
- type=int,
494
- default=None,
495
- help=("Max number of checkpoints to store."),
496
- )
497
- parser.add_argument(
498
- "--resume_from_checkpoint",
499
- type=str,
500
- default=None,
501
- help=(
502
- "Whether training should be resumed from a previous checkpoint. Use a path saved by"
503
- ' `--checkpointing_steps`, or `"latest"` to automatically select the last available checkpoint.'
504
- ),
505
- )
506
- parser.add_argument(
507
- "--gradient_accumulation_steps",
508
- type=int,
509
- default=1,
510
- help="Number of updates steps to accumulate before performing a backward/update pass.",
511
- )
512
- parser.add_argument(
513
- "--gradient_checkpointing",
514
- action="store_true",
515
- help="Whether or not to use gradient checkpointing to save memory at the expense of slower backward pass.",
516
- )
517
- parser.add_argument(
518
- "--learning_rate",
519
- type=float,
520
- default=1e-4,
521
- help="Initial learning rate (after the potential warmup period) to use.",
522
- )
523
-
524
- parser.add_argument(
525
- "--guidance_scale",
526
- type=float,
527
- default=3.5,
528
- help="the FLUX.1 dev variant is a guidance distilled model",
529
- )
530
-
531
- parser.add_argument(
532
- "--text_encoder_lr",
533
- type=float,
534
- default=5e-6,
535
- help="Text encoder learning rate to use.",
536
- )
537
- parser.add_argument(
538
- "--scale_lr",
539
- action="store_true",
540
- default=False,
541
- help="Scale the learning rate by the number of GPUs, gradient accumulation steps, and batch size.",
542
- )
543
- parser.add_argument(
544
- "--lr_scheduler",
545
- type=str,
546
- default="constant",
547
- help=(
548
- 'The scheduler type to use. Choose between ["linear", "cosine", "cosine_with_restarts", "polynomial",'
549
- ' "constant", "constant_with_warmup"]'
550
- ),
551
- )
552
- parser.add_argument(
553
- "--lr_warmup_steps", type=int, default=500, help="Number of steps for the warmup in the lr scheduler."
554
- )
555
- parser.add_argument(
556
- "--lr_num_cycles",
557
- type=int,
558
- default=1,
559
- help="Number of hard resets of the lr in cosine_with_restarts scheduler.",
560
- )
561
- parser.add_argument("--lr_power", type=float, default=1.0, help="Power factor of the polynomial scheduler.")
562
- parser.add_argument(
563
- "--dataloader_num_workers",
564
- type=int,
565
- default=0,
566
- help=(
567
- "Number of subprocesses to use for data loading. 0 means that the data will be loaded in the main process."
568
- ),
569
- )
570
- parser.add_argument(
571
- "--weighting_scheme",
572
- type=str,
573
- default="none",
574
- choices=["sigma_sqrt", "logit_normal", "mode", "cosmap", "none"],
575
- help=('We default to the "none" weighting scheme for uniform sampling and uniform loss'),
576
- )
577
- parser.add_argument(
578
- "--logit_mean", type=float, default=0.0, help="mean to use when using the `'logit_normal'` weighting scheme."
579
- )
580
- parser.add_argument(
581
- "--logit_std", type=float, default=1.0, help="std to use when using the `'logit_normal'` weighting scheme."
582
- )
583
- parser.add_argument(
584
- "--mode_scale",
585
- type=float,
586
- default=1.29,
587
- help="Scale of mode weighting scheme. Only effective when using the `'mode'` as the `weighting_scheme`.",
588
- )
589
- parser.add_argument(
590
- "--optimizer",
591
- type=str,
592
- default="AdamW",
593
- help=('The optimizer type to use. Choose between ["AdamW", "prodigy"]'),
594
- )
595
-
596
- parser.add_argument(
597
- "--use_8bit_adam",
598
- action="store_true",
599
- help="Whether or not to use 8-bit Adam from bitsandbytes. Ignored if optimizer is not set to AdamW",
600
- )
601
-
602
- parser.add_argument(
603
- "--adam_beta1", type=float, default=0.9, help="The beta1 parameter for the Adam and Prodigy optimizers."
604
- )
605
- parser.add_argument(
606
- "--adam_beta2", type=float, default=0.999, help="The beta2 parameter for the Adam and Prodigy optimizers."
607
- )
608
- parser.add_argument(
609
- "--prodigy_beta3",
610
- type=float,
611
- default=None,
612
- help="coefficients for computing the Prodigy stepsize using running averages. If set to None, "
613
- "uses the value of square root of beta2. Ignored if optimizer is adamW",
614
- )
615
- parser.add_argument("--prodigy_decouple", type=bool, default=True, help="Use AdamW style decoupled weight decay")
616
- parser.add_argument("--adam_weight_decay", type=float, default=1e-04, help="Weight decay to use for unet params")
617
- parser.add_argument(
618
- "--adam_weight_decay_text_encoder", type=float, default=1e-03, help="Weight decay to use for text_encoder"
619
- )
620
-
621
- parser.add_argument(
622
- "--lora_layers",
623
- type=str,
624
- default=None,
625
- help=(
626
- 'The transformer modules to apply LoRA training on. Please specify the layers in a comma separated. E.g. - "to_k,to_q,to_v,to_out.0" will result in lora training of attention layers only'
627
- ),
628
- )
629
-
630
- parser.add_argument(
631
- "--adam_epsilon",
632
- type=float,
633
- default=1e-08,
634
- help="Epsilon value for the Adam optimizer and Prodigy optimizers.",
635
- )
636
-
637
- parser.add_argument(
638
- "--prodigy_use_bias_correction",
639
- type=bool,
640
- default=True,
641
- help="Turn on Adam's bias correction. True by default. Ignored if optimizer is adamW",
642
- )
643
- parser.add_argument(
644
- "--prodigy_safeguard_warmup",
645
- type=bool,
646
- default=True,
647
- help="Remove lr from the denominator of D estimate to avoid issues during warm-up stage. True by default. "
648
- "Ignored if optimizer is adamW",
649
- )
650
- parser.add_argument("--max_grad_norm", default=1.0, type=float, help="Max gradient norm.")
651
- parser.add_argument("--push_to_hub", action="store_true", help="Whether or not to push the model to the Hub.")
652
- parser.add_argument("--hub_token", type=str, default=None, help="The token to use to push to the Model Hub.")
653
- parser.add_argument(
654
- "--hub_model_id",
655
- type=str,
656
- default=None,
657
- help="The name of the repository to keep in sync with the local `output_dir`.",
658
- )
659
- parser.add_argument(
660
- "--logging_dir",
661
- type=str,
662
- default="logs",
663
- help=(
664
- "[TensorBoard](https://www.tensorflow.org/tensorboard) log directory. Will default to"
665
- " *output_dir/runs/**CURRENT_DATETIME_HOSTNAME***."
666
- ),
667
- )
668
- parser.add_argument(
669
- "--allow_tf32",
670
- action="store_true",
671
- help=(
672
- "Whether or not to allow TF32 on Ampere GPUs. Can be used to speed up training. For more information, see"
673
- " https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices"
674
- ),
675
- )
676
- parser.add_argument(
677
- "--cache_latents",
678
- action="store_true",
679
- default=False,
680
- help="Cache the VAE latents",
681
- )
682
- parser.add_argument(
683
- "--report_to",
684
- type=str,
685
- default="tensorboard",
686
- help=(
687
- 'The integration to report the results and logs to. Supported platforms are `"tensorboard"`'
688
- ' (default), `"wandb"` and `"comet_ml"`. Use `"all"` to report to all integrations.'
689
- ),
690
- )
691
- parser.add_argument(
692
- "--mixed_precision",
693
- type=str,
694
- default=None,
695
- choices=["no", "fp16", "bf16"],
696
- help=(
697
- "Whether to use mixed precision. Choose between fp16 and bf16 (bfloat16). Bf16 requires PyTorch >="
698
- " 1.10.and an Nvidia Ampere GPU. Default to the value of accelerate config of the current system or the"
699
- " flag passed with the `accelerate.launch` command. Use this argument to override the accelerate config."
700
- ),
701
- )
702
- parser.add_argument(
703
- "--upcast_before_saving",
704
- action="store_true",
705
- default=False,
706
- help=(
707
- "Whether to upcast the trained transformer layers to float32 before saving (at the end of training). "
708
- "Defaults to precision dtype used for training to save memory"
709
- ),
710
- )
711
- parser.add_argument(
712
- "--prior_generation_precision",
713
- type=str,
714
- default=None,
715
- choices=["no", "fp32", "fp16", "bf16"],
716
- help=(
717
- "Choose prior generation precision between fp32, fp16 and bf16 (bfloat16). Bf16 requires PyTorch >="
718
- " 1.10.and an Nvidia Ampere GPU. Default to fp16 if a GPU is available else fp32."
719
- ),
720
- )
721
- parser.add_argument("--local_rank", type=int, default=-1, help="For distributed training: local_rank")
722
- parser.add_argument("--enable_npu_flash_attention", action="store_true", help="Enabla Flash Attention for NPU")
723
-
724
- if input_args is not None:
725
- args = parser.parse_args(input_args)
726
- else:
727
- args = parser.parse_args()
728
-
729
- if args.dataset_name is None and args.instance_data_dir is None:
730
- raise ValueError("Specify either `--dataset_name` or `--instance_data_dir`")
731
-
732
- if args.dataset_name is not None and args.instance_data_dir is not None:
733
- raise ValueError("Specify only one of `--dataset_name` or `--instance_data_dir`")
734
-
735
- env_local_rank = int(os.environ.get("LOCAL_RANK", -1))
736
- if env_local_rank != -1 and env_local_rank != args.local_rank:
737
- args.local_rank = env_local_rank
738
-
739
- if args.with_prior_preservation:
740
- if args.class_data_dir is None:
741
- raise ValueError("You must specify a data directory for class images.")
742
- if args.class_prompt is None:
743
- raise ValueError("You must specify prompt for class images.")
744
- else:
745
- # logger is not available yet
746
- if args.class_data_dir is not None:
747
- warnings.warn("You need not use --class_data_dir without --with_prior_preservation.")
748
- if args.class_prompt is not None:
749
- warnings.warn("You need not use --class_prompt without --with_prior_preservation.")
750
-
751
- return args
752
-
753
-
754
- class DreamBoothDataset(Dataset):
755
- """
756
- A dataset to prepare the instance and class images with the prompts for fine-tuning the model.
757
- It pre-processes the images.
758
- """
759
-
760
- def __init__(
761
- self,
762
- args: argparse.Namespace,
763
- instance_data_root: str | os.PathLike[str] | None,
764
- instance_prompt: str,
765
- class_prompt: str | None,
766
- class_data_root: str | os.PathLike[str] | None = None,
767
- class_num: int | None = None,
768
- size: int = 1024,
769
- repeats: int = 1,
770
- center_crop: bool = False,
771
- ) -> None:
772
- self.args = args
773
- self.size = size
774
- self.center_crop = center_crop
775
-
776
- self.instance_prompt = instance_prompt
777
- self.custom_instance_prompts = None
778
- self.class_prompt = class_prompt
779
-
780
- # if --dataset_name is provided or a metadata jsonl file is provided in the local --instance_data directory,
781
- # we load the training data using load_dataset
782
- if self.args.dataset_name is not None:
783
- try:
784
- from datasets import load_dataset
785
- except ImportError:
786
- raise ImportError(
787
- "You are trying to load your data using the datasets library. If you wish to train using custom "
788
- "captions please install the datasets library: `pip install datasets`. If you wish to load a "
789
- "local folder containing images only, specify --instance_data_dir instead."
790
- )
791
- # Downloading and loading a dataset from the hub.
792
- # See more about loading custom images at
793
- # https://huggingface.co/docs/datasets/v2.0.0/en/dataset_script
794
- dataset = load_dataset(
795
- self.args.dataset_name,
796
- self.args.dataset_config_name,
797
- cache_dir=self.args.cache_dir,
798
- )
799
- # Preprocessing the datasets.
800
- column_names = dataset["train"].column_names
801
-
802
- # 6. Get the column names for input/target.
803
- if self.args.image_column is None:
804
- image_column = column_names[0]
805
- logger.info(f"image column defaulting to {image_column}")
806
- else:
807
- image_column = self.args.image_column
808
- if image_column not in column_names:
809
- raise ValueError(
810
- f"`--image_column` value '{self.args.image_column}' not found in dataset columns. Dataset columns are: {', '.join(column_names)}"
811
- )
812
- instance_images = dataset["train"][image_column]
813
-
814
- if self.args.caption_column is None:
815
- logger.info(
816
- "No caption column provided, defaulting to instance_prompt for all images. If your dataset "
817
- "contains captions/prompts for the images, make sure to specify the "
818
- "column as --caption_column"
819
- )
820
- self.custom_instance_prompts = None
821
- else:
822
- if self.args.caption_column not in column_names:
823
- raise ValueError(
824
- f"`--caption_column` value '{self.args.caption_column}' not found in dataset columns. Dataset columns are: {', '.join(column_names)}"
825
- )
826
- custom_instance_prompts = dataset["train"][self.args.caption_column]
827
- # create final list of captions according to --repeats
828
- self.custom_instance_prompts = []
829
- for caption in custom_instance_prompts:
830
- self.custom_instance_prompts.extend(itertools.repeat(caption, repeats))
831
- else:
832
- self.instance_data_root = Path(instance_data_root)
833
- if not self.instance_data_root.exists():
834
- raise ValueError("Instance images root doesn't exists.")
835
-
836
- instance_images = [Image.open(path) for path in list(Path(instance_data_root).iterdir())]
837
- self.custom_instance_prompts = None
838
-
839
- self.instance_images = []
840
- for img in instance_images:
841
- self.instance_images.extend(itertools.repeat(img, repeats))
842
-
843
- self.pixel_values = []
844
- train_resize = transforms.Resize(size, interpolation=transforms.InterpolationMode.BILINEAR)
845
- train_crop = transforms.CenterCrop(size) if center_crop else transforms.RandomCrop(size)
846
- train_flip = transforms.RandomHorizontalFlip(p=1.0)
847
- train_transforms = transforms.Compose(
848
- [
849
- transforms.ToTensor(),
850
- transforms.Normalize([0.5], [0.5]),
851
- ]
852
- )
853
- for image in self.instance_images:
854
- image = exif_transpose(image)
855
- if not image.mode == "RGB":
856
- image = image.convert("RGB")
857
- image = train_resize(image)
858
- if self.args.random_flip and random.random() < 0.5:
859
- # flip
860
- image = train_flip(image)
861
- if self.args.center_crop:
862
- y1 = max(0, int(round((image.height - self.args.resolution) / 2.0)))
863
- x1 = max(0, int(round((image.width - self.args.resolution) / 2.0)))
864
- image = train_crop(image)
865
- else:
866
- y1, x1, h, w = train_crop.get_params(image, (self.args.resolution, self.args.resolution))
867
- image = crop(image, y1, x1, h, w)
868
- image = train_transforms(image)
869
- self.pixel_values.append(image)
870
-
871
- self.num_instance_images = len(self.instance_images)
872
- self._length = self.num_instance_images
873
-
874
- if class_data_root is not None:
875
- self.class_data_root = Path(class_data_root)
876
- self.class_data_root.mkdir(parents=True, exist_ok=True)
877
- self.class_images_path = list(self.class_data_root.iterdir())
878
- if class_num is not None:
879
- self.num_class_images = min(len(self.class_images_path), class_num)
880
- else:
881
- self.num_class_images = len(self.class_images_path)
882
- self._length = max(self.num_class_images, self.num_instance_images)
883
- else:
884
- self.class_data_root = None
885
-
886
- self.image_transforms = transforms.Compose(
887
- [
888
- transforms.Resize(size, interpolation=transforms.InterpolationMode.BILINEAR),
889
- transforms.CenterCrop(size) if center_crop else transforms.RandomCrop(size),
890
- transforms.ToTensor(),
891
- transforms.Normalize([0.5], [0.5]),
892
- ]
893
- )
894
-
895
- def __len__(self):
896
- return self._length
897
-
898
- def __getitem__(self, index):
899
- example = {}
900
- instance_index = index % self.num_instance_images
901
- instance_image = self.pixel_values[instance_index]
902
- example["instance_images"] = instance_image
903
- example["instance_index"] = instance_index
904
-
905
- if self.custom_instance_prompts:
906
- caption = self.custom_instance_prompts[instance_index]
907
- if caption:
908
- example["instance_prompt"] = caption
909
- else:
910
- example["instance_prompt"] = self.instance_prompt
911
-
912
- else: # custom prompts were provided, but length does not match size of image dataset
913
- example["instance_prompt"] = self.instance_prompt
914
-
915
- if self.class_data_root:
916
- class_image = Image.open(self.class_images_path[index % self.num_class_images])
917
- class_image = exif_transpose(class_image)
918
-
919
- if not class_image.mode == "RGB":
920
- class_image = class_image.convert("RGB")
921
- example["class_images"] = self.image_transforms(class_image)
922
- example["class_prompt"] = self.class_prompt
923
-
924
- return example
925
-
926
-
927
- def collate_fn(examples, with_prior_preservation=False):
928
- pixel_values = [example["instance_images"] for example in examples]
929
- prompts = [example["instance_prompt"] for example in examples]
930
- instance_indices = [example["instance_index"] for example in examples]
931
-
932
- # Concat class and instance examples for prior preservation.
933
- # We do this to avoid doing two forward passes.
934
- if with_prior_preservation:
935
- pixel_values += [example["class_images"] for example in examples]
936
- prompts += [example["class_prompt"] for example in examples]
937
-
938
- pixel_values = torch.stack(pixel_values)
939
- pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float()
940
-
941
- batch = {"pixel_values": pixel_values, "prompts": prompts, "instance_indices": instance_indices}
942
- return batch
943
-
944
-
945
- def collate_instance_latents(examples):
946
- pixel_values = torch.stack([example["instance_images"] for example in examples])
947
- pixel_values = pixel_values.to(memory_format=torch.contiguous_format).float()
948
- instance_indices = [example["instance_index"] for example in examples]
949
- return {"pixel_values": pixel_values, "instance_indices": instance_indices}
950
-
951
-
952
- class PromptDataset(Dataset):
953
- "A simple dataset to prepare the prompts to generate class images on multiple GPUs."
954
-
955
- def __init__(self, prompt, num_samples):
956
- self.prompt = prompt
957
- self.num_samples = num_samples
958
-
959
- def __len__(self):
960
- return self.num_samples
961
-
962
- def __getitem__(self, index):
963
- example = {}
964
- example["prompt"] = self.prompt
965
- example["index"] = index
966
- return example
967
-
968
-
969
- def tokenize_prompt(tokenizer, prompt, max_sequence_length):
970
- text_inputs = tokenizer(
971
- prompt,
972
- padding="max_length",
973
- max_length=max_sequence_length,
974
- truncation=True,
975
- return_length=False,
976
- return_overflowing_tokens=False,
977
- return_tensors="pt",
978
- )
979
- text_input_ids = text_inputs.input_ids
980
- return text_input_ids
981
-
982
-
983
- def _encode_prompt_with_t5(
984
- text_encoder,
985
- tokenizer,
986
- max_sequence_length=512,
987
- prompt=None,
988
- num_images_per_prompt=1,
989
- device=None,
990
- text_input_ids=None,
991
- ):
992
- prompt = [prompt] if isinstance(prompt, str) else prompt
993
- batch_size = len(prompt)
994
-
995
- if tokenizer is not None:
996
- text_inputs = tokenizer(
997
- prompt,
998
- padding="max_length",
999
- max_length=max_sequence_length,
1000
- truncation=True,
1001
- return_length=False,
1002
- return_overflowing_tokens=False,
1003
- return_tensors="pt",
1004
- )
1005
- text_input_ids = text_inputs.input_ids
1006
- else:
1007
- if text_input_ids is None:
1008
- raise ValueError("text_input_ids must be provided when the tokenizer is not specified")
1009
-
1010
- prompt_embeds = text_encoder(text_input_ids.to(device))[0]
1011
-
1012
- if hasattr(text_encoder, "module"):
1013
- dtype = text_encoder.module.dtype
1014
- else:
1015
- dtype = text_encoder.dtype
1016
- prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
1017
-
1018
- _, seq_len, _ = prompt_embeds.shape
1019
-
1020
- # duplicate text embeddings and attention mask for each generation per prompt, using mps friendly method
1021
- prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
1022
- prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)
1023
-
1024
- return prompt_embeds
1025
-
1026
-
1027
- def _encode_prompt_with_clip(
1028
- text_encoder,
1029
- tokenizer,
1030
- prompt: str,
1031
- device=None,
1032
- text_input_ids=None,
1033
- num_images_per_prompt: int = 1,
1034
- ):
1035
- prompt = [prompt] if isinstance(prompt, str) else prompt
1036
- batch_size = len(prompt)
1037
-
1038
- if tokenizer is not None:
1039
- text_inputs = tokenizer(
1040
- prompt,
1041
- padding="max_length",
1042
- max_length=77,
1043
- truncation=True,
1044
- return_overflowing_tokens=False,
1045
- return_length=False,
1046
- return_tensors="pt",
1047
- )
1048
-
1049
- text_input_ids = text_inputs.input_ids
1050
- else:
1051
- if text_input_ids is None:
1052
- raise ValueError("text_input_ids must be provided when the tokenizer is not specified")
1053
-
1054
- prompt_embeds = text_encoder(text_input_ids.to(device), output_hidden_states=False)
1055
-
1056
- if hasattr(text_encoder, "module"):
1057
- dtype = text_encoder.module.dtype
1058
- else:
1059
- dtype = text_encoder.dtype
1060
- # Use pooled output of CLIPTextModel
1061
- prompt_embeds = prompt_embeds.pooler_output
1062
- prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
1063
-
1064
- # duplicate text embeddings for each generation per prompt, using mps friendly method
1065
- prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
1066
- prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, -1)
1067
-
1068
- return prompt_embeds
1069
-
1070
-
1071
- def encode_prompt(
1072
- text_encoders,
1073
- tokenizers,
1074
- prompt: str,
1075
- max_sequence_length,
1076
- device=None,
1077
- num_images_per_prompt: int = 1,
1078
- text_input_ids_list=None,
1079
- ):
1080
- prompt = [prompt] if isinstance(prompt, str) else prompt
1081
-
1082
- if hasattr(text_encoders[0], "module"):
1083
- dtype = text_encoders[0].module.dtype
1084
- else:
1085
- dtype = text_encoders[0].dtype
1086
-
1087
- pooled_prompt_embeds = _encode_prompt_with_clip(
1088
- text_encoder=text_encoders[0],
1089
- tokenizer=tokenizers[0],
1090
- prompt=prompt,
1091
- device=device if device is not None else text_encoders[0].device,
1092
- num_images_per_prompt=num_images_per_prompt,
1093
- text_input_ids=text_input_ids_list[0] if text_input_ids_list else None,
1094
- )
1095
-
1096
- prompt_embeds = _encode_prompt_with_t5(
1097
- text_encoder=text_encoders[1],
1098
- tokenizer=tokenizers[1],
1099
- max_sequence_length=max_sequence_length,
1100
- prompt=prompt,
1101
- num_images_per_prompt=num_images_per_prompt,
1102
- device=device if device is not None else text_encoders[1].device,
1103
- text_input_ids=text_input_ids_list[1] if text_input_ids_list else None,
1104
- )
1105
-
1106
- text_ids = torch.zeros(prompt_embeds.shape[1], 3).to(device=device, dtype=dtype)
1107
-
1108
- return prompt_embeds, pooled_prompt_embeds, text_ids
1109
-
1110
-
1111
- def get_cached_text_embeddings(
1112
- prompt_embed_cache: dict[str, tuple[torch.Tensor, torch.Tensor, torch.Tensor]],
1113
- prompts: Sequence[str],
1114
- device: torch.device,
1115
- ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
1116
- cached = [prompt_embed_cache[prompt] for prompt in prompts]
1117
- prompt_embeds = torch.cat([entry[0] for entry in cached], dim=0).to(device)
1118
- pooled_prompt_embeds = torch.cat([entry[1] for entry in cached], dim=0).to(device)
1119
- text_ids = cached[0][2].to(device)
1120
- return prompt_embeds, pooled_prompt_embeds, text_ids
1121
-
1122
-
1123
- def build_latents_cache(
1124
- train_dataset: Dataset,
1125
- vae: AutoencoderKL,
1126
- batch_size: int,
1127
- num_workers: int,
1128
- device: torch.device,
1129
- dtype: torch.dtype,
1130
- ) -> list[torch.Tensor]:
1131
- latents_cache: list[torch.Tensor | None] = [None] * len(train_dataset)
1132
- latent_cache_loader = torch.utils.data.DataLoader(
1133
- train_dataset,
1134
- batch_size=batch_size,
1135
- shuffle=False,
1136
- collate_fn=collate_instance_latents,
1137
- num_workers=num_workers,
1138
- )
1139
- for batch in tqdm(latent_cache_loader, desc="Caching latents"):
1140
- with torch.no_grad():
1141
- pixel_values = batch["pixel_values"].to(device, non_blocking=True, dtype=dtype)
1142
- batch_latents = vae.encode(pixel_values).latent_dist
1143
- for latent_index, cached_latent in zip(batch["instance_indices"], batch_latents.parameters):
1144
- latents_cache[latent_index] = cached_latent.detach().cpu()
1145
-
1146
- if any(cached_latent is None for cached_latent in latents_cache):
1147
- raise RuntimeError("Latent cache build was incomplete.")
1148
-
1149
- return [cached_latent for cached_latent in latents_cache if cached_latent is not None]
1150
-
1151
-
1152
- def get_cached_latent_dist(
1153
- latents_cache: Sequence[torch.Tensor],
1154
- instance_indices: Sequence[int],
1155
- device: torch.device,
1156
- dtype: torch.dtype,
1157
- ) -> DiagonalGaussianDistribution:
1158
- cached_latent_params = torch.stack([latents_cache[index] for index in instance_indices]).to(
1159
- device=device,
1160
- dtype=dtype,
1161
- non_blocking=True,
1162
- )
1163
- return DiagonalGaussianDistribution(cached_latent_params)
1164
-
1165
-
1166
- def main(args: argparse.Namespace) -> None:
1167
- if args.report_to == "wandb" and args.hub_token is not None:
1168
- raise ValueError(
1169
- "You cannot use both --report_to=wandb and --hub_token due to a security risk of exposing your token."
1170
- " Please use `hf auth login` to authenticate with the Hub."
1171
- )
1172
-
1173
- if torch.backends.mps.is_available() and args.mixed_precision == "bf16":
1174
- # due to pytorch#99272, MPS does not yet support bfloat16.
1175
- raise ValueError(
1176
- "Mixed precision training with bfloat16 is not supported on MPS. Please use fp16 (recommended) or fp32 instead."
1177
- )
1178
-
1179
- logging_dir = Path(args.output_dir, args.logging_dir)
1180
-
1181
- accelerator_project_config = ProjectConfiguration(project_dir=args.output_dir, logging_dir=logging_dir)
1182
- kwargs = DistributedDataParallelKwargs(find_unused_parameters=True)
1183
- accelerator = Accelerator(
1184
- gradient_accumulation_steps=args.gradient_accumulation_steps,
1185
- mixed_precision=args.mixed_precision,
1186
- log_with=args.report_to,
1187
- project_config=accelerator_project_config,
1188
- kwargs_handlers=[kwargs],
1189
- )
1190
-
1191
- # Disable AMP for MPS.
1192
- if torch.backends.mps.is_available():
1193
- accelerator.native_amp = False
1194
-
1195
- if args.report_to == "wandb":
1196
- if not is_wandb_available():
1197
- raise ImportError("Make sure to install wandb if you want to use it for logging during training.")
1198
-
1199
- # Make one log on every process with the configuration for debugging.
1200
- logging.basicConfig(
1201
- format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
1202
- datefmt="%m/%d/%Y %H:%M:%S",
1203
- level=logging.INFO,
1204
- )
1205
- logger.info(accelerator.state, main_process_only=False)
1206
- if accelerator.is_local_main_process:
1207
- transformers.utils.logging.set_verbosity_warning()
1208
- diffusers.utils.logging.set_verbosity_info()
1209
- else:
1210
- transformers.utils.logging.set_verbosity_error()
1211
- diffusers.utils.logging.set_verbosity_error()
1212
-
1213
- # If passed along, set the training seed now.
1214
- if args.seed is not None:
1215
- set_seed(args.seed)
1216
-
1217
- # Generate class images if prior preservation is enabled.
1218
- if args.with_prior_preservation:
1219
- class_images_dir = Path(args.class_data_dir)
1220
- if not class_images_dir.exists():
1221
- class_images_dir.mkdir(parents=True)
1222
- cur_class_images = len(list(class_images_dir.iterdir()))
1223
-
1224
- if cur_class_images < args.num_class_images:
1225
- has_supported_fp16_accelerator = torch.cuda.is_available() or torch.backends.mps.is_available()
1226
- torch_dtype = torch.float16 if has_supported_fp16_accelerator else torch.float32
1227
- if args.prior_generation_precision == "fp32":
1228
- torch_dtype = torch.float32
1229
- elif args.prior_generation_precision == "fp16":
1230
- torch_dtype = torch.float16
1231
- elif args.prior_generation_precision == "bf16":
1232
- torch_dtype = torch.bfloat16
1233
-
1234
- pipeline = FluxPipeline.from_pretrained(
1235
- args.pretrained_model_name_or_path,
1236
- torch_dtype=torch_dtype,
1237
- revision=args.revision,
1238
- variant=args.variant,
1239
- )
1240
- pipeline.set_progress_bar_config(disable=True)
1241
-
1242
- num_new_images = args.num_class_images - cur_class_images
1243
- logger.info(f"Number of class images to sample: {num_new_images}.")
1244
-
1245
- sample_dataset = PromptDataset(args.class_prompt, num_new_images)
1246
- sample_dataloader = torch.utils.data.DataLoader(sample_dataset, batch_size=args.sample_batch_size)
1247
-
1248
- sample_dataloader = accelerator.prepare(sample_dataloader)
1249
- pipeline.to(accelerator.device)
1250
-
1251
- for example in tqdm(
1252
- sample_dataloader, desc="Generating class images", disable=not accelerator.is_local_main_process
1253
- ):
1254
- with torch.autocast(device_type=accelerator.device.type, dtype=torch_dtype):
1255
- images = pipeline(prompt=example["prompt"]).images
1256
-
1257
- for i, image in enumerate(images):
1258
- hash_image = insecure_hashlib.sha1(image.tobytes()).hexdigest()
1259
- image_filename = class_images_dir / f"{example['index'][i] + cur_class_images}-{hash_image}.jpg"
1260
- image.save(image_filename)
1261
-
1262
- del pipeline
1263
- free_memory()
1264
-
1265
- # Handle the repository creation
1266
- if accelerator.is_main_process:
1267
- if args.output_dir is not None:
1268
- os.makedirs(args.output_dir, exist_ok=True)
1269
-
1270
- if args.push_to_hub:
1271
- repo_id = create_repo(
1272
- repo_id=args.hub_model_id or Path(args.output_dir).name,
1273
- exist_ok=True,
1274
- ).repo_id
1275
-
1276
- # Load the tokenizers
1277
- tokenizer_one = CLIPTokenizer.from_pretrained(
1278
- args.pretrained_model_name_or_path,
1279
- subfolder="tokenizer",
1280
- revision=args.revision,
1281
- )
1282
- tokenizer_two = T5TokenizerFast.from_pretrained(
1283
- args.pretrained_model_name_or_path,
1284
- subfolder="tokenizer_2",
1285
- revision=args.revision,
1286
- )
1287
-
1288
- # import correct text encoder classes
1289
- text_encoder_cls_one = import_model_class_from_model_name_or_path(
1290
- args.pretrained_model_name_or_path, args.revision
1291
- )
1292
- text_encoder_cls_two = import_model_class_from_model_name_or_path(
1293
- args.pretrained_model_name_or_path, args.revision, subfolder="text_encoder_2"
1294
- )
1295
-
1296
- # For mixed precision training we cast all non-trainable weights (vae, text_encoder and transformer) to half-precision
1297
- # as these weights are only used for inference, keeping weights in full precision is not required.
1298
- weight_dtype = torch.float32
1299
- if accelerator.mixed_precision == "fp16":
1300
- weight_dtype = torch.float16
1301
- elif accelerator.mixed_precision == "bf16":
1302
- weight_dtype = torch.bfloat16
1303
-
1304
- if torch.backends.mps.is_available() and weight_dtype == torch.bfloat16:
1305
- # due to pytorch#99272, MPS does not yet support bfloat16.
1306
- raise ValueError(
1307
- "Mixed precision training with bfloat16 is not supported on MPS. Please use fp16 (recommended) or fp32 instead."
1308
- )
1309
-
1310
- # Load scheduler and models
1311
- noise_scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
1312
- args.pretrained_model_name_or_path, subfolder="scheduler"
1313
- )
1314
- noise_scheduler_copy = copy.deepcopy(noise_scheduler)
1315
- text_encoder_one, text_encoder_two = load_text_encoders(
1316
- args, text_encoder_cls_one, text_encoder_cls_two, weight_dtype
1317
- )
1318
- text_encoder_one.requires_grad_(False)
1319
- text_encoder_two.requires_grad_(False)
1320
-
1321
- if args.train_text_encoder:
1322
- raise ValueError(
1323
- "This low-memory trainer does not support --train_text_encoder. "
1324
- "LoRA training is restricted to the transformer so CLIP/T5 can be freed before transformer training."
1325
- )
1326
-
1327
- # Dataset and DataLoaders creation. This is intentionally before transformer/VAE device placement
1328
- # so all prompt embeddings can be cached and the text encoders freed first.
1329
- train_dataset = DreamBoothDataset(
1330
- args=args,
1331
- instance_data_root=args.instance_data_dir,
1332
- instance_prompt=args.instance_prompt,
1333
- class_prompt=args.class_prompt,
1334
- class_data_root=args.class_data_dir if args.with_prior_preservation else None,
1335
- class_num=args.num_class_images,
1336
- size=args.resolution,
1337
- repeats=args.repeats,
1338
- center_crop=args.center_crop,
1339
- )
1340
-
1341
- train_dataloader = torch.utils.data.DataLoader(
1342
- train_dataset,
1343
- batch_size=args.train_batch_size,
1344
- shuffle=True,
1345
- collate_fn=lambda examples: collate_fn(examples, args.with_prior_preservation),
1346
- num_workers=args.dataloader_num_workers,
1347
- )
1348
-
1349
- text_encoder_one.to(accelerator.device, dtype=weight_dtype)
1350
- text_encoder_two.to(accelerator.device, dtype=weight_dtype)
1351
-
1352
- tokenizers = [tokenizer_one, tokenizer_two]
1353
- text_encoders = [text_encoder_one, text_encoder_two]
1354
-
1355
- def compute_text_embeddings(prompt, text_encoders, tokenizers):
1356
- with torch.no_grad():
1357
- prompt_embeds, pooled_prompt_embeds, text_ids = encode_prompt(
1358
- text_encoders, tokenizers, prompt, args.max_sequence_length
1359
- )
1360
- prompt_embeds = prompt_embeds.to(accelerator.device)
1361
- pooled_prompt_embeds = pooled_prompt_embeds.to(accelerator.device)
1362
- text_ids = text_ids.to(accelerator.device)
1363
- return prompt_embeds, pooled_prompt_embeds, text_ids
1364
-
1365
- prompt_embed_cache = {}
1366
- prompts_to_cache = {args.instance_prompt}
1367
- if train_dataset.custom_instance_prompts:
1368
- prompts_to_cache.update(prompt for prompt in train_dataset.custom_instance_prompts if prompt)
1369
- if args.with_prior_preservation:
1370
- prompts_to_cache.add(args.class_prompt)
1371
-
1372
- for prompt in sorted(prompts_to_cache):
1373
- prompt_embed_cache[prompt] = tuple(tensor.cpu() for tensor in compute_text_embeddings(prompt, text_encoders, tokenizers))
1374
-
1375
- del text_encoder_one, text_encoder_two, tokenizer_one, tokenizer_two, text_encoders, tokenizers
1376
- text_encoder_one = None
1377
- text_encoder_two = None
1378
- free_memory()
1379
-
1380
- vae = AutoencoderKL.from_pretrained(
1381
- args.pretrained_model_name_or_path,
1382
- subfolder="vae",
1383
- revision=args.revision,
1384
- variant=args.variant,
1385
- torch_dtype=weight_dtype,
1386
- )
1387
- transformer = FluxTransformer2DModel.from_pretrained(
1388
- args.pretrained_model_name_or_path,
1389
- subfolder="transformer",
1390
- revision=args.revision,
1391
- variant=args.variant,
1392
- torch_dtype=weight_dtype,
1393
- )
1394
-
1395
- # We only train the additional adapter LoRA layers on the denoising transformer.
1396
- transformer.requires_grad_(False)
1397
- vae.requires_grad_(False)
1398
-
1399
- if args.enable_npu_flash_attention:
1400
- if is_torch_npu_available():
1401
- logger.info("npu flash attention enabled.")
1402
- transformer.set_attention_backend("_native_npu")
1403
- else:
1404
- raise ValueError("npu flash attention requires torch_npu extensions and is supported only on npu device ")
1405
-
1406
- vae.to(accelerator.device, dtype=weight_dtype)
1407
- transformer.to(accelerator.device, dtype=weight_dtype)
1408
-
1409
- if args.gradient_checkpointing:
1410
- transformer.enable_gradient_checkpointing()
1411
- if args.train_text_encoder:
1412
- text_encoder_one.gradient_checkpointing_enable()
1413
-
1414
- if args.lora_layers is not None:
1415
- target_modules = [layer.strip() for layer in args.lora_layers.split(",")]
1416
- else:
1417
- target_modules = [
1418
- "attn.to_k",
1419
- "attn.to_q",
1420
- "attn.to_v",
1421
- "attn.to_out.0",
1422
- "attn.add_k_proj",
1423
- "attn.add_q_proj",
1424
- "attn.add_v_proj",
1425
- "attn.to_add_out",
1426
- "ff.net.0.proj",
1427
- "ff.net.2",
1428
- "ff_context.net.0.proj",
1429
- "ff_context.net.2",
1430
- ]
1431
-
1432
- # now we will add new LoRA weights the transformer layers
1433
- transformer_lora_config = LoraConfig(
1434
- r=args.rank,
1435
- lora_alpha=args.lora_alpha,
1436
- lora_dropout=args.lora_dropout,
1437
- init_lora_weights="gaussian",
1438
- target_modules=target_modules,
1439
- )
1440
- transformer.add_adapter(transformer_lora_config)
1441
- if args.train_text_encoder:
1442
- text_lora_config = LoraConfig(
1443
- r=args.rank,
1444
- lora_alpha=args.lora_alpha,
1445
- lora_dropout=args.lora_dropout,
1446
- init_lora_weights="gaussian",
1447
- target_modules=["q_proj", "k_proj", "v_proj", "out_proj"],
1448
- )
1449
- text_encoder_one.add_adapter(text_lora_config)
1450
-
1451
- def unwrap_model(model):
1452
- model = accelerator.unwrap_model(model)
1453
- model = model._orig_mod if is_compiled_module(model) else model
1454
- return model
1455
-
1456
- # create custom saving & loading hooks so that `accelerator.save_state(...)` serializes in a nice format
1457
- def save_model_hook(models, weights, output_dir):
1458
- if accelerator.is_main_process:
1459
- transformer_lora_layers_to_save = None
1460
- text_encoder_one_lora_layers_to_save = None
1461
- modules_to_save = {}
1462
- for model in models:
1463
- if isinstance(model, type(unwrap_model(transformer))):
1464
- transformer_lora_layers_to_save = get_peft_model_state_dict(model)
1465
- modules_to_save["transformer"] = model
1466
- elif isinstance(model, type(unwrap_model(text_encoder_one))):
1467
- text_encoder_one_lora_layers_to_save = get_peft_model_state_dict(model)
1468
- modules_to_save["text_encoder"] = model
1469
- else:
1470
- raise ValueError(f"unexpected save model: {model.__class__}")
1471
-
1472
- # make sure to pop weight so that corresponding model is not saved again
1473
- weights.pop()
1474
-
1475
- FluxPipeline.save_lora_weights(
1476
- output_dir,
1477
- transformer_lora_layers=transformer_lora_layers_to_save,
1478
- text_encoder_lora_layers=text_encoder_one_lora_layers_to_save,
1479
- **_collate_lora_metadata(modules_to_save),
1480
- )
1481
-
1482
- def load_model_hook(models, input_dir):
1483
- transformer_ = None
1484
- text_encoder_one_ = None
1485
-
1486
- while len(models) > 0:
1487
- model = models.pop()
1488
-
1489
- if isinstance(model, type(unwrap_model(transformer))):
1490
- transformer_ = model
1491
- elif isinstance(model, type(unwrap_model(text_encoder_one))):
1492
- text_encoder_one_ = model
1493
- else:
1494
- raise ValueError(f"unexpected save model: {model.__class__}")
1495
-
1496
- lora_state_dict = FluxPipeline.lora_state_dict(input_dir)
1497
-
1498
- transformer_state_dict = {
1499
- f"{k.replace('transformer.', '')}": v for k, v in lora_state_dict.items() if k.startswith("transformer.")
1500
- }
1501
- transformer_state_dict = convert_unet_state_dict_to_peft(transformer_state_dict)
1502
- incompatible_keys = set_peft_model_state_dict(transformer_, transformer_state_dict, adapter_name="default")
1503
- if incompatible_keys is not None:
1504
- # check only for unexpected keys
1505
- unexpected_keys = getattr(incompatible_keys, "unexpected_keys", None)
1506
- if unexpected_keys:
1507
- logger.warning(
1508
- f"Loading adapter weights from state_dict led to unexpected keys not found in the model: "
1509
- f" {unexpected_keys}. "
1510
- )
1511
- if args.train_text_encoder:
1512
- # Do we need to call `scale_lora_layers()` here?
1513
- _set_state_dict_into_text_encoder(lora_state_dict, prefix="text_encoder.", text_encoder=text_encoder_one_)
1514
-
1515
- # Make sure the trainable params are in float32. This is again needed since the base models
1516
- # are in `weight_dtype`. More details:
1517
- # https://github.com/huggingface/diffusers/pull/6514#discussion_r1449796804
1518
- if args.mixed_precision == "fp16":
1519
- models = [transformer_]
1520
- if args.train_text_encoder:
1521
- models.extend([text_encoder_one_])
1522
- # only upcast trainable parameters (LoRA) into fp32
1523
- cast_training_params(models)
1524
-
1525
- accelerator.register_save_state_pre_hook(save_model_hook)
1526
- accelerator.register_load_state_pre_hook(load_model_hook)
1527
-
1528
- # Enable TF32 for faster training on Ampere GPUs,
1529
- # cf https://pytorch.org/docs/stable/notes/cuda.html#tensorfloat-32-tf32-on-ampere-devices
1530
- if args.allow_tf32 and torch.cuda.is_available():
1531
- torch.backends.cuda.matmul.allow_tf32 = True
1532
-
1533
- if args.scale_lr:
1534
- args.learning_rate = (
1535
- args.learning_rate * args.gradient_accumulation_steps * args.train_batch_size * accelerator.num_processes
1536
- )
1537
-
1538
- # Make sure the trainable params are in float32.
1539
- if args.mixed_precision == "fp16":
1540
- models = [transformer]
1541
- if args.train_text_encoder:
1542
- models.extend([text_encoder_one])
1543
- # only upcast trainable parameters (LoRA) into fp32
1544
- cast_training_params(models, dtype=torch.float32)
1545
-
1546
- transformer_lora_parameters = list(filter(lambda p: p.requires_grad, transformer.parameters()))
1547
- if args.train_text_encoder:
1548
- text_lora_parameters_one = list(filter(lambda p: p.requires_grad, text_encoder_one.parameters()))
1549
-
1550
- # Optimization parameters
1551
- transformer_parameters_with_lr = {"params": transformer_lora_parameters, "lr": args.learning_rate}
1552
- if args.train_text_encoder:
1553
- # different learning rate for text encoder and unet
1554
- text_parameters_one_with_lr = {
1555
- "params": text_lora_parameters_one,
1556
- "weight_decay": args.adam_weight_decay_text_encoder,
1557
- "lr": args.text_encoder_lr if args.text_encoder_lr else args.learning_rate,
1558
- }
1559
- params_to_optimize = [transformer_parameters_with_lr, text_parameters_one_with_lr]
1560
- else:
1561
- params_to_optimize = [transformer_parameters_with_lr]
1562
-
1563
- # Optimizer creation
1564
- if not (args.optimizer.lower() == "prodigy" or args.optimizer.lower() == "adamw"):
1565
- logger.warning(
1566
- f"Unsupported choice of optimizer: {args.optimizer}.Supported optimizers include [adamW, prodigy]."
1567
- "Defaulting to adamW"
1568
- )
1569
- args.optimizer = "adamw"
1570
-
1571
- if args.use_8bit_adam and not args.optimizer.lower() == "adamw":
1572
- logger.warning(
1573
- f"use_8bit_adam is ignored when optimizer is not set to 'AdamW'. Optimizer was "
1574
- f"set to {args.optimizer.lower()}"
1575
- )
1576
-
1577
- if args.optimizer.lower() == "adamw":
1578
- if args.use_8bit_adam:
1579
- try:
1580
- import bitsandbytes as bnb
1581
- except ImportError:
1582
- raise ImportError(
1583
- "To use 8-bit Adam, please install the bitsandbytes library: `pip install bitsandbytes`."
1584
- )
1585
-
1586
- optimizer_class = bnb.optim.AdamW8bit
1587
- else:
1588
- optimizer_class = torch.optim.AdamW
1589
-
1590
- optimizer = optimizer_class(
1591
- params_to_optimize,
1592
- betas=(args.adam_beta1, args.adam_beta2),
1593
- weight_decay=args.adam_weight_decay,
1594
- eps=args.adam_epsilon,
1595
- )
1596
-
1597
- if args.optimizer.lower() == "prodigy":
1598
- try:
1599
- import prodigyopt
1600
- except ImportError:
1601
- raise ImportError("To use Prodigy, please install the prodigyopt library: `pip install prodigyopt`")
1602
-
1603
- optimizer_class = prodigyopt.Prodigy
1604
-
1605
- if args.learning_rate <= 0.1:
1606
- logger.warning(
1607
- "Learning rate is too low. When using prodigy, it's generally better to set learning rate around 1.0"
1608
- )
1609
- if args.train_text_encoder and args.text_encoder_lr:
1610
- logger.warning(
1611
- f"Learning rates were provided both for the transformer and the text encoder- e.g. text_encoder_lr:"
1612
- f" {args.text_encoder_lr} and learning_rate: {args.learning_rate}. "
1613
- f"When using prodigy only learning_rate is used as the initial learning rate."
1614
- )
1615
- # changes the learning rate of text_encoder_parameters_one to be
1616
- # --learning_rate
1617
- params_to_optimize[1]["lr"] = args.learning_rate
1618
-
1619
- optimizer = optimizer_class(
1620
- params_to_optimize,
1621
- betas=(args.adam_beta1, args.adam_beta2),
1622
- beta3=args.prodigy_beta3,
1623
- weight_decay=args.adam_weight_decay,
1624
- eps=args.adam_epsilon,
1625
- decouple=args.prodigy_decouple,
1626
- use_bias_correction=args.prodigy_use_bias_correction,
1627
- safeguard_warmup=args.prodigy_safeguard_warmup,
1628
- )
1629
-
1630
- vae_config_shift_factor = vae.config.shift_factor
1631
- vae_config_scaling_factor = vae.config.scaling_factor
1632
- vae_config_block_out_channels = vae.config.block_out_channels
1633
- if args.cache_latents:
1634
- latents_cache = build_latents_cache(
1635
- train_dataset=train_dataset,
1636
- vae=vae,
1637
- batch_size=args.train_batch_size,
1638
- num_workers=args.dataloader_num_workers,
1639
- device=accelerator.device,
1640
- dtype=weight_dtype,
1641
- )
1642
-
1643
- if not args.validation_prompt or args.num_validation_images <= 0:
1644
- del vae
1645
- free_memory()
1646
-
1647
- # Scheduler and math around the number of training steps.
1648
- # Check the PR https://github.com/huggingface/diffusers/pull/8312 for detailed explanation.
1649
- num_warmup_steps_for_scheduler = args.lr_warmup_steps * accelerator.num_processes
1650
- if args.max_train_steps is None:
1651
- len_train_dataloader_after_sharding = math.ceil(len(train_dataloader) / accelerator.num_processes)
1652
- num_update_steps_per_epoch = math.ceil(len_train_dataloader_after_sharding / args.gradient_accumulation_steps)
1653
- num_training_steps_for_scheduler = (
1654
- args.num_train_epochs * accelerator.num_processes * num_update_steps_per_epoch
1655
- )
1656
- else:
1657
- num_training_steps_for_scheduler = args.max_train_steps * accelerator.num_processes
1658
-
1659
- lr_scheduler = get_scheduler(
1660
- args.lr_scheduler,
1661
- optimizer=optimizer,
1662
- num_warmup_steps=num_warmup_steps_for_scheduler,
1663
- num_training_steps=num_training_steps_for_scheduler,
1664
- num_cycles=args.lr_num_cycles,
1665
- power=args.lr_power,
1666
- )
1667
-
1668
- # Prepare everything with our `accelerator`.
1669
- if args.train_text_encoder:
1670
- (
1671
- transformer,
1672
- text_encoder_one,
1673
- optimizer,
1674
- train_dataloader,
1675
- lr_scheduler,
1676
- ) = accelerator.prepare(
1677
- transformer,
1678
- text_encoder_one,
1679
- optimizer,
1680
- train_dataloader,
1681
- lr_scheduler,
1682
- )
1683
- else:
1684
- transformer, optimizer, train_dataloader, lr_scheduler = accelerator.prepare(
1685
- transformer, optimizer, train_dataloader, lr_scheduler
1686
- )
1687
-
1688
- # We need to recalculate our total training steps as the size of the training dataloader may have changed.
1689
- num_update_steps_per_epoch = math.ceil(len(train_dataloader) / args.gradient_accumulation_steps)
1690
- if args.max_train_steps is None:
1691
- args.max_train_steps = args.num_train_epochs * num_update_steps_per_epoch
1692
- if num_training_steps_for_scheduler != args.max_train_steps:
1693
- logger.warning(
1694
- f"The length of the 'train_dataloader' after 'accelerator.prepare' ({len(train_dataloader)}) does not match "
1695
- f"the expected length ({len_train_dataloader_after_sharding}) when the learning rate scheduler was created. "
1696
- f"This inconsistency may result in the learning rate scheduler not functioning properly."
1697
- )
1698
- # Afterwards we recalculate our number of training epochs
1699
- args.num_train_epochs = math.ceil(args.max_train_steps / num_update_steps_per_epoch)
1700
-
1701
- # We need to initialize the trackers we use, and also store our configuration.
1702
- # The trackers initializes automatically on the main process.
1703
- if accelerator.is_main_process:
1704
- tracker_name = "dreambooth-flux-dev-lora"
1705
- accelerator.init_trackers(tracker_name, config=vars(args))
1706
-
1707
- # Train!
1708
- total_batch_size = args.train_batch_size * accelerator.num_processes * args.gradient_accumulation_steps
1709
-
1710
- logger.info("***** Running training *****")
1711
- logger.info(f" Num examples = {len(train_dataset)}")
1712
- logger.info(f" Num batches each epoch = {len(train_dataloader)}")
1713
- logger.info(f" Num Epochs = {args.num_train_epochs}")
1714
- logger.info(f" Instantaneous batch size per device = {args.train_batch_size}")
1715
- logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size}")
1716
- logger.info(f" Gradient Accumulation steps = {args.gradient_accumulation_steps}")
1717
- logger.info(f" Total optimization steps = {args.max_train_steps}")
1718
- global_step = 0
1719
- first_epoch = 0
1720
-
1721
- # Potentially load in the weights and states from a previous save
1722
- if args.resume_from_checkpoint:
1723
- if args.resume_from_checkpoint != "latest":
1724
- path = os.path.basename(args.resume_from_checkpoint)
1725
- else:
1726
- # Get the mos recent checkpoint
1727
- dirs = os.listdir(args.output_dir)
1728
- dirs = [d for d in dirs if d.startswith("checkpoint")]
1729
- dirs = sorted(dirs, key=lambda x: int(x.split("-")[1]))
1730
- path = dirs[-1] if len(dirs) > 0 else None
1731
-
1732
- if path is None:
1733
- accelerator.print(
1734
- f"Checkpoint '{args.resume_from_checkpoint}' does not exist. Starting a new training run."
1735
- )
1736
- args.resume_from_checkpoint = None
1737
- initial_global_step = 0
1738
- else:
1739
- accelerator.print(f"Resuming from checkpoint {path}")
1740
- accelerator.load_state(os.path.join(args.output_dir, path))
1741
- global_step = int(path.split("-")[1])
1742
-
1743
- initial_global_step = global_step
1744
- first_epoch = global_step // num_update_steps_per_epoch
1745
-
1746
- else:
1747
- initial_global_step = 0
1748
-
1749
- progress_bar = tqdm(
1750
- range(0, args.max_train_steps),
1751
- initial=initial_global_step,
1752
- desc="Steps",
1753
- # Only show the progress bar once on each machine.
1754
- disable=not accelerator.is_local_main_process,
1755
- )
1756
-
1757
- def get_sigmas(timesteps, n_dim=4, dtype=torch.float32):
1758
- sigmas = noise_scheduler_copy.sigmas.to(device=accelerator.device, dtype=dtype)
1759
- schedule_timesteps = noise_scheduler_copy.timesteps.to(accelerator.device)
1760
- timesteps = timesteps.to(accelerator.device)
1761
- step_indices = [(schedule_timesteps == t).nonzero().item() for t in timesteps]
1762
-
1763
- sigma = sigmas[step_indices].flatten()
1764
- while len(sigma.shape) < n_dim:
1765
- sigma = sigma.unsqueeze(-1)
1766
- return sigma
1767
-
1768
- for epoch in range(first_epoch, args.num_train_epochs):
1769
- transformer.train()
1770
- if args.train_text_encoder:
1771
- text_encoder_one.train()
1772
- # set top parameter requires_grad = True for gradient checkpointing works
1773
- _te_one = unwrap_model(text_encoder_one)
1774
- (_te_one.text_model if hasattr(_te_one, "text_model") else _te_one).embeddings.requires_grad_(True)
1775
-
1776
- for step, batch in enumerate(train_dataloader):
1777
- models_to_accumulate = [transformer]
1778
- if args.train_text_encoder:
1779
- models_to_accumulate.extend([text_encoder_one])
1780
- with accelerator.accumulate(models_to_accumulate):
1781
- prompts = batch["prompts"]
1782
-
1783
- prompt_embeds, pooled_prompt_embeds, text_ids = get_cached_text_embeddings(
1784
- prompt_embed_cache,
1785
- prompts,
1786
- accelerator.device,
1787
- )
1788
-
1789
- # Convert images to latent space
1790
- if args.cache_latents:
1791
- model_input = get_cached_latent_dist(
1792
- latents_cache,
1793
- batch["instance_indices"],
1794
- accelerator.device,
1795
- weight_dtype,
1796
- ).sample()
1797
- else:
1798
- pixel_values = batch["pixel_values"].to(dtype=vae.dtype)
1799
- model_input = vae.encode(pixel_values).latent_dist.sample()
1800
- model_input = (model_input - vae_config_shift_factor) * vae_config_scaling_factor
1801
- model_input = model_input.to(dtype=weight_dtype)
1802
-
1803
- vae_scale_factor = 2 ** (len(vae_config_block_out_channels) - 1)
1804
-
1805
- latent_image_ids = FluxPipeline._prepare_latent_image_ids(
1806
- model_input.shape[0],
1807
- model_input.shape[2] // 2,
1808
- model_input.shape[3] // 2,
1809
- accelerator.device,
1810
- weight_dtype,
1811
- )
1812
- # Sample noise that we'll add to the latents
1813
- noise = torch.randn_like(model_input)
1814
- bsz = model_input.shape[0]
1815
-
1816
- # Sample a random timestep for each image
1817
- # for weighting schemes where we sample timesteps non-uniformly
1818
- u = compute_density_for_timestep_sampling(
1819
- weighting_scheme=args.weighting_scheme,
1820
- batch_size=bsz,
1821
- logit_mean=args.logit_mean,
1822
- logit_std=args.logit_std,
1823
- mode_scale=args.mode_scale,
1824
- )
1825
- indices = (u * noise_scheduler_copy.config.num_train_timesteps).long()
1826
- timesteps = noise_scheduler_copy.timesteps[indices].to(device=model_input.device)
1827
-
1828
- # Add noise according to flow matching.
1829
- # zt = (1 - texp) * x + texp * z1
1830
- sigmas = get_sigmas(timesteps, n_dim=model_input.ndim, dtype=model_input.dtype)
1831
- noisy_model_input = (1.0 - sigmas) * model_input + sigmas * noise
1832
-
1833
- packed_noisy_model_input = FluxPipeline._pack_latents(
1834
- noisy_model_input,
1835
- batch_size=model_input.shape[0],
1836
- num_channels_latents=model_input.shape[1],
1837
- height=model_input.shape[2],
1838
- width=model_input.shape[3],
1839
- )
1840
-
1841
- # handle guidance
1842
- if unwrap_model(transformer).config.guidance_embeds:
1843
- guidance = torch.tensor([args.guidance_scale], device=accelerator.device)
1844
- guidance = guidance.expand(model_input.shape[0])
1845
- else:
1846
- guidance = None
1847
-
1848
- # Predict the noise residual
1849
- model_pred = transformer(
1850
- hidden_states=packed_noisy_model_input,
1851
- # YiYi notes: divide it by 1000 for now because we scale it by 1000 in the transformer model (we should not keep it but I want to keep the inputs same for the model for testing)
1852
- timestep=timesteps / 1000,
1853
- guidance=guidance,
1854
- pooled_projections=pooled_prompt_embeds,
1855
- encoder_hidden_states=prompt_embeds,
1856
- txt_ids=text_ids,
1857
- img_ids=latent_image_ids,
1858
- return_dict=False,
1859
- )[0]
1860
- model_pred = FluxPipeline._unpack_latents(
1861
- model_pred,
1862
- height=model_input.shape[2] * vae_scale_factor,
1863
- width=model_input.shape[3] * vae_scale_factor,
1864
- vae_scale_factor=vae_scale_factor,
1865
- )
1866
-
1867
- # these weighting schemes use a uniform timestep sampling
1868
- # and instead post-weight the loss
1869
- weighting = compute_loss_weighting_for_sd3(weighting_scheme=args.weighting_scheme, sigmas=sigmas)
1870
-
1871
- # flow matching loss
1872
- target = noise - model_input
1873
-
1874
- if args.with_prior_preservation:
1875
- # Chunk the noise and model_pred into two parts and compute the loss on each part separately.
1876
- model_pred, model_pred_prior = torch.chunk(model_pred, 2, dim=0)
1877
- target, target_prior = torch.chunk(target, 2, dim=0)
1878
- weighting, weighting_prior = torch.chunk(weighting, 2, dim=0)
1879
-
1880
- # Compute prior loss
1881
- prior_loss = torch.mean(
1882
- (weighting_prior.float() * (model_pred_prior.float() - target_prior.float()) ** 2).reshape(
1883
- target_prior.shape[0], -1
1884
- ),
1885
- 1,
1886
- )
1887
- prior_loss = prior_loss.mean()
1888
-
1889
- # Compute regular loss.
1890
- loss = torch.mean(
1891
- (weighting.float() * (model_pred.float() - target.float()) ** 2).reshape(target.shape[0], -1),
1892
- 1,
1893
- )
1894
- loss = loss.mean()
1895
-
1896
- if args.with_prior_preservation:
1897
- # Add the prior loss to the instance loss.
1898
- loss = loss + args.prior_loss_weight * prior_loss
1899
-
1900
- accelerator.backward(loss)
1901
- if accelerator.sync_gradients:
1902
- params_to_clip = (
1903
- itertools.chain(transformer.parameters(), text_encoder_one.parameters())
1904
- if args.train_text_encoder
1905
- else transformer.parameters()
1906
- )
1907
- accelerator.clip_grad_norm_(params_to_clip, args.max_grad_norm)
1908
-
1909
- optimizer.step()
1910
- lr_scheduler.step()
1911
- optimizer.zero_grad()
1912
-
1913
- # Checks if the accelerator has performed an optimization step behind the scenes
1914
- if accelerator.sync_gradients:
1915
- progress_bar.update(1)
1916
- global_step += 1
1917
-
1918
- if accelerator.is_main_process:
1919
- if global_step % args.checkpointing_steps == 0:
1920
- # _before_ saving state, check if this save would set us over the `checkpoints_total_limit`
1921
- if args.checkpoints_total_limit is not None:
1922
- checkpoints = os.listdir(args.output_dir)
1923
- checkpoints = [d for d in checkpoints if d.startswith("checkpoint")]
1924
- checkpoints = sorted(checkpoints, key=lambda x: int(x.split("-")[1]))
1925
-
1926
- # before we save the new checkpoint, we need to have at _most_ `checkpoints_total_limit - 1` checkpoints
1927
- if len(checkpoints) >= args.checkpoints_total_limit:
1928
- num_to_remove = len(checkpoints) - args.checkpoints_total_limit + 1
1929
- removing_checkpoints = checkpoints[0:num_to_remove]
1930
-
1931
- logger.info(
1932
- f"{len(checkpoints)} checkpoints already exist, removing {len(removing_checkpoints)} checkpoints"
1933
- )
1934
- logger.info(f"removing checkpoints: {', '.join(removing_checkpoints)}")
1935
-
1936
- for removing_checkpoint in removing_checkpoints:
1937
- removing_checkpoint = os.path.join(args.output_dir, removing_checkpoint)
1938
- shutil.rmtree(removing_checkpoint)
1939
-
1940
- save_path = os.path.join(args.output_dir, f"checkpoint-{global_step}")
1941
- accelerator.save_state(save_path)
1942
- logger.info(f"Saved state to {save_path}")
1943
-
1944
- logs = {"loss": loss.detach().item(), "lr": lr_scheduler.get_last_lr()[0]}
1945
- progress_bar.set_postfix(**logs)
1946
- accelerator.log(logs, step=global_step)
1947
-
1948
- if global_step >= args.max_train_steps:
1949
- break
1950
-
1951
- if accelerator.is_main_process:
1952
- if args.validation_prompt and args.num_validation_images > 0 and epoch % args.validation_epochs == 0:
1953
- # create pipeline
1954
- if not args.train_text_encoder:
1955
- text_encoder_one, text_encoder_two = load_text_encoders(
1956
- args, text_encoder_cls_one, text_encoder_cls_two, weight_dtype
1957
- )
1958
- pipeline = FluxPipeline.from_pretrained(
1959
- args.pretrained_model_name_or_path,
1960
- vae=vae,
1961
- text_encoder=unwrap_model(text_encoder_one),
1962
- text_encoder_2=unwrap_model(text_encoder_two),
1963
- transformer=unwrap_model(transformer),
1964
- revision=args.revision,
1965
- variant=args.variant,
1966
- torch_dtype=weight_dtype,
1967
- )
1968
- pipeline_args = {"prompt": args.validation_prompt}
1969
- images = log_validation(
1970
- pipeline=pipeline,
1971
- args=args,
1972
- accelerator=accelerator,
1973
- pipeline_args=pipeline_args,
1974
- epoch=epoch,
1975
- torch_dtype=weight_dtype,
1976
- )
1977
- if not args.train_text_encoder:
1978
- del text_encoder_one, text_encoder_two
1979
- free_memory()
1980
-
1981
- images = None
1982
- del pipeline
1983
-
1984
- # Save the lora layers
1985
- accelerator.wait_for_everyone()
1986
- if accelerator.is_main_process:
1987
- modules_to_save = {}
1988
- transformer = unwrap_model(transformer)
1989
- if args.upcast_before_saving:
1990
- transformer.to(torch.float32)
1991
- else:
1992
- transformer = transformer.to(weight_dtype)
1993
- transformer_lora_layers = get_peft_model_state_dict(transformer)
1994
- modules_to_save["transformer"] = transformer
1995
-
1996
- if args.train_text_encoder:
1997
- text_encoder_one = unwrap_model(text_encoder_one)
1998
- text_encoder_lora_layers = get_peft_model_state_dict(text_encoder_one.to(torch.float32))
1999
- modules_to_save["text_encoder"] = text_encoder_one
2000
- else:
2001
- text_encoder_lora_layers = None
2002
-
2003
- FluxPipeline.save_lora_weights(
2004
- save_directory=args.output_dir,
2005
- transformer_lora_layers=transformer_lora_layers,
2006
- text_encoder_lora_layers=text_encoder_lora_layers,
2007
- **_collate_lora_metadata(modules_to_save),
2008
- )
2009
-
2010
- images = []
2011
- if args.validation_prompt and args.num_validation_images > 0:
2012
- # Final inference
2013
- # Load previous pipeline only when validation output is requested.
2014
- pipeline = FluxPipeline.from_pretrained(
2015
- args.pretrained_model_name_or_path,
2016
- revision=args.revision,
2017
- variant=args.variant,
2018
- torch_dtype=weight_dtype,
2019
- )
2020
- # load attention processors
2021
- pipeline.load_lora_weights(args.output_dir)
2022
-
2023
- # run inference
2024
- pipeline_args = {"prompt": args.validation_prompt}
2025
- images = log_validation(
2026
- pipeline=pipeline,
2027
- args=args,
2028
- accelerator=accelerator,
2029
- pipeline_args=pipeline_args,
2030
- epoch=epoch,
2031
- is_final_validation=True,
2032
- torch_dtype=weight_dtype,
2033
- )
2034
- del pipeline
2035
-
2036
- if args.push_to_hub:
2037
- save_model_card(
2038
- repo_id,
2039
- images=images,
2040
- base_model=args.pretrained_model_name_or_path,
2041
- train_text_encoder=args.train_text_encoder,
2042
- instance_prompt=args.instance_prompt,
2043
- validation_prompt=args.validation_prompt,
2044
- repo_folder=args.output_dir,
2045
- )
2046
- upload_folder(
2047
- repo_id=repo_id,
2048
- folder_path=args.output_dir,
2049
- commit_message="End of training",
2050
- ignore_patterns=["step_*", "epoch_*"],
2051
- )
2052
-
2053
- images = None
2054
-
2055
- accelerator.end_training()
2056
-
2057
-
2058
- if __name__ == "__main__":
2059
- main(parse_args())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/tests/test_flux_lowmem.py DELETED
@@ -1,67 +0,0 @@
1
- from __future__ import annotations
2
-
3
- # pyright: reportPrivateImportUsage=false
4
-
5
- import sys
6
- import unittest
7
- from pathlib import Path
8
-
9
- import torch
10
- from torch import device, equal, ones, randn, zeros
11
-
12
- sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
13
-
14
- from lora.trainers.flux_lowmem import get_cached_latent_dist, get_cached_text_embeddings
15
-
16
-
17
- class GetCachedTextEmbeddingsTests(unittest.TestCase):
18
- def test_reuses_single_text_ids_tensor_for_batched_prompts(self) -> None:
19
- seq_len = 5
20
- prompt_embed_cache = {
21
- "a": (
22
- randn(1, seq_len, 4),
23
- randn(1, 6),
24
- zeros(seq_len, 3),
25
- ),
26
- "b": (
27
- randn(1, seq_len, 4),
28
- randn(1, 6),
29
- ones(seq_len, 3),
30
- ),
31
- }
32
-
33
- prompt_embeds, pooled_prompt_embeds, text_ids = get_cached_text_embeddings(
34
- prompt_embed_cache,
35
- ["a", "b"],
36
- device("cpu"),
37
- )
38
-
39
- self.assertEqual(prompt_embeds.shape, (2, seq_len, 4))
40
- self.assertEqual(pooled_prompt_embeds.shape, (2, 6))
41
- self.assertEqual(text_ids.shape, (seq_len, 3))
42
- self.assertTrue(equal(text_ids, prompt_embed_cache["a"][2]))
43
-
44
-
45
- class GetCachedLatentDistTests(unittest.TestCase):
46
- def test_rebuilds_batched_distribution_from_instance_indices(self) -> None:
47
- latents_cache = [
48
- randn(32, 64, 64),
49
- randn(32, 64, 64),
50
- randn(32, 64, 64),
51
- ]
52
-
53
- latent_dist = get_cached_latent_dist(
54
- latents_cache,
55
- [2, 0],
56
- device("cpu"),
57
- torch.float32,
58
- )
59
-
60
- self.assertEqual(latent_dist.parameters.shape, (2, 32, 64, 64))
61
- self.assertTrue(equal(latent_dist.parameters[0], latents_cache[2]))
62
- self.assertTrue(equal(latent_dist.parameters[1], latents_cache[0]))
63
- self.assertEqual(latent_dist.sample().shape, (2, 16, 64, 64))
64
-
65
-
66
- if __name__ == "__main__":
67
- unittest.main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/uv.lock DELETED
The diff for this file is too large to render. See raw diff