File size: 7,499 Bytes
2ca14fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | """
T.I.T.S.2 data prep — streams CC12M, keeps only the rows OpenDiffusionAI's cleaned
list vouches for (watermark/junk filtered, LLaVA-written captions), crops to 256px,
encodes with the SD VAE, and writes latent shards.
Why the join: `opendiffusionai/cc12m-cleaned` has the good captions but is URL-only
(~2 img/s once you account for dead links). `pixparse/cc12m-wds` has the actual image
bytes and streams fast. Same source, same order, so we walk the image stream and keep
a rolling window of the cleaned list to look up each url.
Resume works at tar-file granularity: CC12M is 2176 tars, so we record which tar we're
on rather than "row N", because skipping N rows means re-downloading them. The cleaned
list is parquet (cheap to skip), so its position is tracked as a row offset.
Nothing full-size ever hits disk: images are encoded to 4x32x32 fp16 latents (8KB each)
and written in shards of 10k alongside their captions.
Usage:
python prepare_data2.py --out_dir data2 --num 600000
"""
import argparse
import collections
import glob
import json
import os
import re
import time
import numpy as np
import torch
from datasets import load_dataset
from huggingface_hub import HfApi
from PIL import Image
SHARD_SIZE = 10000
WDS_REPO = "pixparse/cc12m-wds"
CLEAN_REPO = "opendiffusionai/cc12m-cleaned"
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--out_dir", type=str, default="data2")
p.add_argument("--num", type=int, default=600000, help="how many usable images to collect")
p.add_argument("--image_size", type=int, default=256)
p.add_argument("--min_side", type=int, default=256, help="skip images smaller than this")
p.add_argument("--max_aspect", type=float, default=1.6, help="skip images more oblong than this")
p.add_argument("--window", type=int, default=60000, help="rolling lookup window into the cleaned list")
p.add_argument("--batch_size", type=int, default=32, help="VAE encode batch")
p.add_argument("--vae", type=str, default="stabilityai/sd-vae-ft-mse")
p.add_argument("--device", type=str, default="cuda" if torch.cuda.is_available() else "cpu")
return p.parse_args()
def center_crop_resize(img, size):
w, h = img.size
side = min(w, h)
img = img.crop(((w - side) // 2, (h - side) // 2, (w + side) // 2, (h + side) // 2))
return img.resize((size, size), Image.BICUBIC)
def tar_urls():
files = sorted(f for f in HfApi().list_repo_files(WDS_REPO, repo_type="dataset") if f.endswith(".tar"))
return [f"https://huggingface.co/datasets/{WDS_REPO}/resolve/main/{f}" for f in files]
def main():
args = parse_args()
os.makedirs(args.out_dir, exist_ok=True)
state_path = os.path.join(args.out_dir, "state.json")
from diffusers import AutoencoderKL
vae = AutoencoderKL.from_pretrained(args.vae, torch_dtype=torch.float16).to(args.device).eval()
vae.requires_grad_(False)
existing = sorted(glob.glob(os.path.join(args.out_dir, "shard_*.pt")))
next_shard = max((int(re.search(r"(\d+)", os.path.basename(p)).group(1)) for p in existing), default=-1) + 1
state = {"tar_index": 0, "clean_rows": 0, "images": 0}
if os.path.exists(state_path) and existing:
state.update(json.load(open(state_path)))
print(f"resuming: {state['images']} images, tar {state['tar_index']}, "
f"cleaned-list row {state['clean_rows']}", flush=True)
urls = tar_urls()
print(f"{len(urls)} source tars", flush=True)
clean = load_dataset(CLEAN_REPO, split="train", streaming=True)
if state["clean_rows"]:
clean = clean.skip(state["clean_rows"]) # parquet: cheap to skip
clean = iter(clean)
clean_pulled = state["clean_rows"]
window = collections.OrderedDict()
def top_up():
nonlocal clean_pulled
while len(window) < args.window:
try:
r = next(clean)
except StopIteration:
return False
window[r["url"]] = r["caption_llava_short"] or r["caption_llava"]
clean_pulled += 1
return True
top_up()
batch_imgs, batch_caps, shard_lat, shard_cap = [], [], [], []
n = state["images"]
t0, t_imgs, miss = time.time(), 0, 0
def flush_batch():
if not batch_imgs:
return
x = torch.stack(batch_imgs).to(args.device, torch.float16)
with torch.no_grad():
lat = vae.encode(x).latent_dist.sample() * vae.config.scaling_factor
shard_lat.append(lat.cpu().to(torch.float16))
shard_cap.extend(batch_caps)
batch_imgs.clear()
batch_caps.clear()
def save_state(tar_index):
# Re-read the still-pending window entries on resume rather than lose them.
json.dump({"tar_index": tar_index, "clean_rows": max(0, clean_pulled - len(window)),
"images": n}, open(state_path, "w"))
def flush_shard(tar_index):
nonlocal next_shard, shard_lat, shard_cap
if not shard_cap:
return
path = os.path.join(args.out_dir, f"shard_{next_shard:05d}.pt")
torch.save({"latents": torch.cat(shard_lat), "captions": list(shard_cap)}, path)
next_shard += 1
shard_lat, shard_cap = [], []
save_state(tar_index)
rate = t_imgs / (time.time() - t0 + 1e-9)
print(f"saved {path} ({n} images total, tar {tar_index}, {rate:.0f} img/s)", flush=True)
for ti in range(state["tar_index"], len(urls)):
if n >= args.num:
break
try:
ds = load_dataset("webdataset", data_files={"train": [urls[ti]]}, split="train", streaming=True)
for ex in ds:
caption = window.pop(ex["json"].get("url"), None)
if caption is None:
# Not in the cleaned list -> watermarked/junk. But a long miss streak
# means the window has drifted behind the image stream (e.g. after a
# resume), so walk it forward until matches resume.
miss += 1
if miss > 3000:
for _ in range(min(20000, len(window))):
window.popitem(last=False)
top_up()
miss = 0
continue
miss = 0
if len(window) < args.window // 2:
top_up()
img = ex["jpg"]
w, h = img.size
if min(w, h) < args.min_side or max(w, h) / min(w, h) > args.max_aspect:
continue
img = center_crop_resize(img.convert("RGB"), args.image_size)
batch_imgs.append(torch.from_numpy(np.asarray(img).copy()).permute(2, 0, 1).float() / 127.5 - 1.0)
batch_caps.append(caption.strip())
n += 1
t_imgs += 1
if len(batch_imgs) >= args.batch_size:
flush_batch()
if len(shard_cap) >= SHARD_SIZE:
flush_shard(ti)
if n >= args.num:
break
except Exception as e:
# Truncated tar / dropped connection: lose this file, not the run.
print(f"tar {ti} failed ({type(e).__name__}: {e}) — skipping", flush=True)
save_state(ti + 1)
flush_batch()
flush_shard(min(state["tar_index"] + 1, len(urls)))
print(f"done: {n} images", flush=True)
if __name__ == "__main__":
main()
|