Spaces:
Running on Zero
Running on Zero
File size: 12,659 Bytes
d0e7c02 | 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 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | """Train one LoRA adapter for slot reading, jointly across tasks.
Every slot's answer is the full-vocabulary log-probability of its labels at the token before its
placeholder; the loss is cross-entropy against soft ordinal targets, normalized per number (a coarse
slot adds to its number's loss rather than diluting it). Only the logits at the slot positions are
computed (logits_to_keep), so a whole layout costs one forward pass.
# the blog's arms (Gemma 4 31B, all three tasks)
python -m slotread.train --arm E --out runs/j_E --run-name j-E
python -m slotread.train --arm A --out runs/j_A # also F, Bplus (and C, Bplusw)
# E3: general-object boxes, warm-started from E
python -m slotread.train --arm E --init runs/j_E/adapter --lr 1e-4 --n-agent 5000 --n-click 1000 \\
--n-box 4000 --n-box-photo 5000 --n-seg 6000 --out runs/j_E3
# a smaller model, computer use only
python -m slotread.train --arm E --model mPLUG/GUI-Owl-1.5-2B-Instruct --processor mPLUG/GUI-Owl-1.5-2B-Instruct \\
--tasks agent --n-agent 5000 --n-click 2000 --image-cap 1048576 --out runs/owl_E
Writes <out>/config.json (each task's system prompt, slot order and label ids: what a reader needs),
<out>/adapter and <out>/adapter-<n> checkpoints.
"""
import argparse, json, math, os, random, time
from multiprocessing import Pool
import torch
from . import images
from . import labels as L
from .geometry import poly_mask, radial_encode
from .numbers import LOG_MIN
from .tasks import AgentTask, BoxTask, SegTask
LORA_TARGETS = r".*language_model.*\.(q_proj|k_proj|v_proj|o_proj|gate_proj|up_proj|down_proj)"
def encode(proc, task, ex, arm):
"""The chat-templated input and the positions whose logits predict each slot (the token before
each placeholder)."""
enc = proc.apply_chat_template(task.messages(ex, arm), tokenize=True, return_dict=True, return_tensors="pt",
add_generation_prompt=False, continue_final_message=True, enable_thinking=False)
ids = enc["input_ids"][0]
n = len(task.layout(arm))
pos = (ids == L.PH).nonzero().flatten()[-n:]
assert len(pos) == n, f"{task.name}: {len(pos)} placeholders for {n} slots"
return enc, pos - 1
def read(model, enc, keep, slots, labels):
"""Full-vocabulary log-probabilities of each slot's labels (a loss on them also moves mass onto
the labels; readouts renormalize over the labels)."""
out = model(**{k: v.to(model.device) for k, v in enc.items()}, logits_to_keep=keep.to(model.device),
use_cache=False)
lv = torch.log_softmax(out.logits[0].float(), -1)
return {s: lv[i, labels[s]] for i, s in enumerate(slots)}
def _radial(args):
polys, size, K = args
return radial_encode(poly_mask(polys, *size), K)
def aggregate(name, rows):
keys = {k for r in rows for k, v in r.items() if v is not None}
m = {k: sum(r[k] for r in rows if r.get(k) is not None) / max(1, sum(r.get(k) is not None for r in rows))
for k in keys if k not in ("inter", "union")}
if name == "seg":
m["ciou"] = sum(r["inter"] for r in rows) / max(1, sum(r["union"] for r in rows))
m["miou"] = m.pop("iou")
return m
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--arm", choices=list(L.ARMS), required=True)
ap.add_argument("--tasks", default="agent,box,seg")
ap.add_argument("--n-agent", type=int, default=7000, help="AGUVIS training steps")
ap.add_argument("--n-click", type=int, default=2000, help="Wave-UI clicks in the agent layout")
ap.add_argument("--n-box", type=int, default=6000, help="Wave-UI element boxes")
ap.add_argument("--n-box-photo", type=int, default=0, help="RefCOCO referent boxes (general objects)")
ap.add_argument("--n-seg", type=int, default=9000, help="RefCOCO masks")
ap.add_argument("--n-val", type=int, default=100, help="per task, for the in-run evals")
ap.add_argument("--rays", type=int, default=24)
ap.add_argument("--init", default="", help="continue this adapter (warm start) instead of a fresh LoRA")
ap.add_argument("--max-train", type=int, default=0, help="cap the shuffled training list (pilots)")
ap.add_argument("--model", default="google/gemma-4-31B-it")
ap.add_argument("--processor", default="nvidia/Gemma-4-31B-IT-NVFP4", help="the served model's processor (template)")
ap.add_argument("--image-cap", type=int, default=0, help="max pixels per image, as served; 0: as given")
ap.add_argument("--out", required=True)
ap.add_argument("--lr", type=float, default=2e-4)
ap.add_argument("--accum", type=int, default=8)
ap.add_argument("--r", type=int, default=16)
ap.add_argument("--eval-every", type=int, default=4000)
ap.add_argument("--clip", type=float, default=1.0, help="gradient-norm clip (the norm before clipping is logged)")
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--wandb", default="", help="wandb project (needs WANDB_API_KEY); empty: stdout only")
ap.add_argument("--run-name", default="")
a = ap.parse_args()
os.makedirs(a.out, exist_ok=True)
random.seed(a.seed); torch.manual_seed(a.seed)
from transformers import AutoModelForImageTextToText, AutoProcessor
from peft import LoraConfig, PeftModel, get_peft_model
proc = AutoProcessor.from_pretrained(a.processor)
tok = proc.tokenizer
if "gemma" not in a.model.lower(): # placeholder and digit ids from this tokenizer
L.configure_tokenizer(tok)
images.IMAGE_CAP = a.image_cap
arm = L.make_arm(a.arm, tok)
make = {"agent": AgentTask, "box": BoxTask, "seg": lambda: SegTask(a.rays)}
tasks = {n: make[n]() for n in a.tasks.split(",")}
labels = {n: t.labels(arm, tok) for n, t in tasks.items()}
for n, t in tasks.items():
print(f"{n}: {len(t.layout(arm))} slots | {' '.join(t.layout(arm))[:160]}", flush=True)
json.dump(dict(arm=arm.name, coarse=arm.coarse, bins=arm.bins, tokens=arm.tokens, fine=arm.fine,
log_min=LOG_MIN, rays=a.rays, placeholder=L.PH, seed=a.seed, run_name=a.run_name,
model=a.model, image_cap=a.image_cap, init=a.init,
counts=dict(agent=a.n_agent, click=a.n_click, box=a.n_box, box_photo=a.n_box_photo, seg=a.n_seg),
tasks={n: t.config(arm, tok) for n, t in tasks.items()}),
open(f"{a.out}/config.json", "w"), indent=1)
t0 = time.time()
train, val = [], {}
if "agent" in tasks:
tr, va = tasks["agent"].examples(a.n_agent, a.n_click, 200)
train += [("agent", ex) for ex in tr]
val["agent"] = va[:a.n_val]
if "box" in tasks:
tr, va, va_photo = tasks["box"].examples(a.n_box, a.n_val, a.n_box_photo, a.seed)
train += [("box", ex) for ex in tr]
val["box"] = va
if va_photo:
val["box_photo"] = va_photo
if "seg" in tasks:
tr, va = tasks["seg"].examples(a.n_seg, a.n_val, seed=a.seed)
with Pool(16) as p: # the radial encodings, once, off the training loop
for ex, enc in zip(tr + va, p.map(_radial, [(e["polys"], e["size"], a.rays) for e in tr + va], chunksize=16)):
ex["radial"] = enc
train += [("seg", ex) for ex in tr]
val["seg"] = va
random.Random(a.seed).shuffle(train)
if a.max_train:
train = train[:a.max_train]
counts = {n: sum(t == n for t, _ in train) for n in tasks}
if a.n_box_photo:
counts["box_photo"] = sum(t == "box" and ex.get("source") == "refcoco" for t, ex in train)
print(f"data: {len(train)} train {counts}, val {({n: len(v) for n, v in val.items()})} in {time.time() - t0:.0f}s",
flush=True)
model = AutoModelForImageTextToText.from_pretrained(a.model, dtype=torch.bfloat16, device_map={"": 0},
attn_implementation="sdpa")
model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
model.enable_input_require_grads()
if a.init:
model = PeftModel.from_pretrained(model, a.init, is_trainable=True)
else:
model = get_peft_model(model, LoraConfig(r=a.r, lora_alpha=2 * a.r, lora_dropout=0.05, task_type="CAUSAL_LM",
target_modules=LORA_TARGETS))
model.print_trainable_parameters()
params = [p for p in model.parameters() if p.requires_grad]
opt = torch.optim.AdamW(params, lr=a.lr, weight_decay=0.0)
steps = math.ceil(len(train) / a.accum)
sched = torch.optim.lr_scheduler.LambdaLR( # 30 warm-up steps, then cosine to zero
opt, lambda s: min(1, (s + 1) / 30) * 0.5 * (1 + math.cos(math.pi * min(s, steps) / steps)))
wb = None
if a.wandb and os.environ.get("WANDB_API_KEY"):
import wandb
wb = wandb.init(project=a.wandb, name=a.run_name or os.path.basename(a.out), dir=a.out,
config=dict(vars(a), bins=arm.bins, coarse=arm.coarse, tokens=arm.tokens, counts=counts))
elif a.wandb:
print("no WANDB_API_KEY: logging to stdout only", flush=True)
def evaluate(tag, step):
model.eval()
out = {}
with torch.no_grad():
for n, exs in val.items():
tn = "box" if n == "box_photo" else n
task, slots = tasks[tn], tasks[tn].layout(arm)
rows = []
for ex in exs:
enc, keep = encode(proc, task, ex, arm)
rows.append(task.score(task.decode(read(model, enc, keep, slots, labels[tn]), arm), ex))
out[n] = aggregate(n, rows)
model.train()
show = {"agent": ("step", "action", "point"), "box": ("iou", "iou75", "center_in"),
"box_photo": ("iou", "iou50", "iou75"), "seg": ("miou", "ciou", "rec50")}
print(f"EVAL {tag}: " + " | ".join(f"{n} " + " ".join(f"{k}={out[n][k]:.3f}" for k in show[n]
if out[n].get(k) is not None) for n in out), flush=True)
if wb:
wb.log({f"val/{n}/{k}": v for n, m in out.items() for k, v in m.items()}, step=step)
return out
evaluate("start", 0)
model.train()
t1, run, seen, norms, skipped = time.time(), {}, 0, [], 0
for n, ex in train:
task = tasks[n]
try:
enc, keep = encode(proc, task, ex, arm)
except (OSError, ValueError) as e: # an unreadable image: skip it rather than lose the run
skipped += 1
print(f"SKIP {n} example ({skipped} so far): {repr(e)[:160]}", flush=True)
enc = None
if enc is not None:
lp = read(model, enc, keep, task.layout(arm), labels[n])
tg = task.targets(ex, arm)
losses = {s: -(t.to(lp[s].device) * lp[s]).sum() for s, t in tg.items()}
numbers = {task.number_of(s) for s in tg}
loss = sum(losses.values()) / len(numbers) # per number: coarse slots add, they do not dilute
(loss / a.accum).backward()
mass = sum(float(lp[s].detach().exp().sum()) for s in tg) / len(tg) # probability on the labels
run[f"loss_{n}"] = 0.98 * run.get(f"loss_{n}", float(loss.detach())) + 0.02 * float(loss.detach())
run[f"mass_{n}"] = 0.98 * run.get(f"mass_{n}", mass) + 0.02 * mass
seen += 1
if seen % a.accum == 0 or seen == len(train):
gn = float(torch.nn.utils.clip_grad_norm_(params, a.clip))
norms.append(gn)
run["grad_norm"] = 0.9 * run.get("grad_norm", gn) + 0.1 * gn # before clipping
opt.step(); sched.step(); opt.zero_grad(set_to_none=True)
if seen % 50 == 0:
el = time.time() - t1
print(f"[{seen}/{len(train)}] {el / seen:.2f}s/ex eta {el / seen * (len(train) - seen) / 60:.0f}min | "
+ " ".join(f"{k}={v:.3f}" for k, v in sorted(run.items())), flush=True)
if wb:
wb.log({**{f"train/{k}": v for k, v in run.items()}, "train/sec_per_example": el / seen,
"train/lr": sched.get_last_lr()[0]}, step=seen)
if seen % a.eval_every == 0 and seen < len(train):
evaluate(f"after {seen}", seen)
model.save_pretrained(f"{a.out}/adapter-{seen}")
model.save_pretrained(f"{a.out}/adapter")
print(f"skipped {skipped} unreadable examples", flush=True)
if norms:
json.dump(norms, open(f"{a.out}/grad_norms.json", "w"))
evaluate("end", seen)
if wb:
wb.finish()
print("TRAIN_DONE", flush=True)
if __name__ == "__main__":
main()
|