Text-to-Image
Diffusers
Safetensors
English
MOSSImagePipeline
moss-image
16gb
structured-pruning
alpha
Instructions to use AiArtLab/mossling with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use AiArtLab/mossling with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("AiArtLab/mossling", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- Draw Things
- DiffusionBee
Download src/window.py from AiArtLab/mossling: direct link, hf CLI and curl.
- Browser
- Download file 19.2 kB
-
https://huggingface.co/AiArtLab/mossling/resolve/main/src/window.py
- Command line
-
hf download hf://AiArtLab/mossling/src/window.py
-
curl -L -o window.py https://huggingface.co/AiArtLab/mossling/resolve/main/src/window.py
19.2 kB
| #!/usr/bin/env python3 | |
| """Слияние смежных блоков: учим LoRA блока s отдавать то же, что отдавали блоки s..s+k. | |
| Идея: блоки в середине меняют поток мало и частично друг друга отменяют. Значит из пары-тройки | |
| смежных блоков можно оставить один, добавив ему низкоранговую поправку, обученную на состояниях | |
| учителя (исходной модели). Так глубина падает без переобучения сети. | |
| python3 src/window.py cache --start 3 --drop 2 --samples 120 --size 1024 | |
| python3 src/window.py train --start 3 --drop 2 --samples 120 --size 1024 --rank 32 --steps 3000 | |
| python3 src/window.py eval --start 3 --drop 2 | |
| Кэш: для каждого сэмпла (промпт, латент, t) пишем вход блока s и выход блока s+k учителя. | |
| Память тренировки — один блок (226 МБ) + LoRA, поэтому шаг дешёвый, а учитель нужен только на кэше. | |
| """ | |
| import argparse | |
| import json | |
| import time | |
| import os | |
| import sys | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| from PIL import Image | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from lab import (EVAL, LAB, Metrics, build, conditions, grid, sample, # noqa: E402 | |
| drop_blocks) | |
| CACHE_DIR = os.path.join(LAB, "cache") | |
| TRAIN_PROMPTS = None | |
| def prompts_for_training(n, seed=0): | |
| """Строки из пула ~/dataset (по одной на файл), как в обучении адаптера ТЕ.""" | |
| global TRAIN_PROMPTS | |
| if TRAIN_PROMPTS is not None: | |
| return TRAIN_PROMPTS | |
| files = sorted(f for f in __import__("glob").glob(os.path.expanduser("~/dataset/civit/*.txt")) | |
| if os.path.getsize(f) > 40) | |
| lines = [] | |
| for f in files: | |
| with open(f, errors="ignore") as fh: | |
| for line in fh: | |
| line = line.strip() | |
| if 20 < len(line) < 400: | |
| lines.append(line) | |
| rng = np.random.default_rng(seed) | |
| TRAIN_PROMPTS = [lines[i] for i in rng.permutation(len(lines))[:n]] | |
| return TRAIN_PROMPTS | |
| # ---------------------------------------------------------------- LoRA | |
| class LoRALinear(torch.nn.Module): | |
| """y = W x + (B A x) * scale; A и B — единственные обучаемые тензоры.""" | |
| def __init__(self, base, rank, alpha=None): | |
| super().__init__() | |
| self.base = base | |
| for p in self.base.parameters(): | |
| p.requires_grad_(False) | |
| self.a = torch.nn.Parameter(torch.randn(rank, base.in_features) / base.in_features ** 0.5) | |
| self.b = torch.nn.Parameter(torch.zeros(base.out_features, rank)) | |
| self.scale = (alpha or rank) / rank | |
| # _Block читает .weight.dtype у проекций | |
| def weight(self): | |
| return self.base.weight | |
| def bias(self): | |
| return self.base.bias | |
| def forward(self, x): | |
| # a/b держим в fp32 (точность мелких поправок), считаем в dtype входа | |
| a, b = self.a.to(x.dtype), self.b.to(x.dtype) | |
| return self.base(x) + F.linear(F.linear(x, a), b) * self.scale | |
| def inject_lora(block, rank, targets=("q", "k", "v", "o", "ffn_up_gate", "ffn_down")): | |
| for name in targets: | |
| lin = block.proj[name] | |
| if not isinstance(lin, LoRALinear): | |
| block.proj[name] = LoRALinear(lin, rank) | |
| def merge_lora(block): | |
| """Влить LoRA в базовые веса: W' = W + B A * scale. На инференсе поправка ничего не стоит.""" | |
| for name, lin in list(block.proj.items()): | |
| if isinstance(lin, LoRALinear): | |
| delta = (lin.b.float() @ lin.a.float()) * lin.scale | |
| base = lin.base | |
| base.weight.data = (base.weight.data.float() + delta).to(base.weight.dtype) | |
| block.proj[name] = base | |
| return block | |
| def lora_state(block): | |
| return {k: v.detach().cpu() for k, v in block.state_dict().items() if k.endswith((".a", ".b"))} | |
| # ---------------------------------------------------------------- cache | |
| def cmd_cache(a): | |
| os.makedirs(CACHE_DIR, exist_ok=True) | |
| out = cache_path(a) | |
| if os.path.exists(out): | |
| print("cache exists:", out) | |
| return | |
| pipe = build() | |
| tr = pipe.transformer.eval() | |
| prompts = prompts_for_training(a.samples) | |
| # по одному-восемь промптов за раз: батч из 240 строк паддится до 4096 токенов и съедает 8 ГБ | |
| conds = [] | |
| with torch.no_grad(): | |
| for i in range(0, len(prompts), 8): | |
| e, m = pipe.encode_prompt(prompts[i:i + 8], 4096) | |
| conds += [(e[j:j + 1].cpu(), m[j:j + 1].cpu()) for j in range(e.shape[0])] | |
| nemb, nmask = pipe.encode_prompt([""], 4096) | |
| nemb, nmask = nemb.cpu(), nmask.cpu() | |
| pipe.text_encoder = None | |
| torch.cuda.empty_cache() | |
| start, last = a.start, a.start + a.drop | |
| rec = {} | |
| def make_hook(i): | |
| def hook(m, inp, out): | |
| if i in (start - 1, last): | |
| rec.setdefault("states", {})[i] = out.to(torch.bfloat16).cpu() | |
| if i == start - 1: | |
| rec["rope"] = inp[1] | |
| if i == start: | |
| rec["mod"] = inp[2] | |
| return hook | |
| handles = [l.register_forward_hook(make_hook(i)) for i, l in enumerate(tr.layers)] | |
| g = a.size // 16 | |
| xs, mods, tgts, ropes, meta = [], [], [], [], [] | |
| rng = np.random.default_rng(a.seed) | |
| def collect(si, ei_emb, ei_mask): | |
| rec.clear() | |
| lat = torch.randn(1, 128, g, g, | |
| generator=torch.Generator("cpu").manual_seed(int(rng.integers(1 << 30)))).cuda() | |
| lat = lat.bfloat16() | |
| t0 = float(rng.uniform(0.05, 0.95)) | |
| # пара шагов Эйлера, чтобы латент был «как на реальной траектории», а не чистый шум | |
| for _ in range(a.warm): | |
| v = tr(lat, torch.tensor([1000.0 * t0]), ei_emb.cuda(), ei_mask.cuda()).sample.float() | |
| lat = (lat.float() - 0.08 * v).bfloat16() | |
| t0 = max(0.02, t0 - 0.08 * a.warm) | |
| tr(lat, torch.tensor([1000.0 * t0]), ei_emb.cuda(), ei_mask.cuda()) | |
| return (rec["states"][start - 1], rec["states"][last], | |
| rec["mod"][0].cpu(), rec["rope"].cpu(), t0) | |
| for si in range(a.samples): | |
| x_s, t_s, mod_s, rope_s, t0 = collect(si, *conds[si]) | |
| xs.append(x_s) | |
| tgts.append(t_s) | |
| mods.append(mod_s) | |
| ropes.append(rope_s) | |
| meta.append({"prompt": si, "t": round(t0, 3)}) | |
| if si == 0: | |
| print(f" state norm {x_s.float().norm():.1f} -> target norm {t_s.float().norm():.1f}, " | |
| f"rel diff {((t_s.float() - x_s.float()).norm() / x_s.float().norm()):.3f}", flush=True) | |
| if (si + 1) % 25 == 0: | |
| print(f" cached {si + 1}/{a.samples}", flush=True) | |
| for h in handles: | |
| h.remove() | |
| torch.save({"x": xs, "target": tgts, | |
| "mod": torch.stack(mods), "rope": ropes, "meta": meta, | |
| "start": start, "drop": a.drop, "size": a.size, "warm": a.warm}, out) | |
| print("saved", out, f"{os.path.getsize(out) / 2 ** 30:.1f} GiB") | |
| def cache_path(a): | |
| return os.path.join(CACHE_DIR, f"win{a.start}_{a.drop}_{a.samples}_{a.size}.pt") | |
| # ---------------------------------------------------------------- train | |
| def cmd_train(a): | |
| data = cache_path(a) | |
| if not os.path.exists(data): | |
| raise SystemExit(f"нет кэша {data}; сначала запустите cache") | |
| d = torch.load(data, map_location="cpu", weights_only=False) | |
| xs, tgts = d["x"], d["target"] | |
| mod = d["mod"].to(torch.bfloat16) | |
| rope_map = {r.shape[1]: r.to("cuda") for r in d["rope"]} # rope зависит от числа токенов | |
| n = len(xs) | |
| # длины последовательностей разные (у промптов разное число токенов) — батчи собираем | |
| # по одинаковой длине, чтобы не паддить и не менять поведение блока | |
| buckets = {} | |
| for i, x in enumerate(xs): | |
| buckets.setdefault(x.shape[1], []).append(i) | |
| rng = np.random.default_rng(0) | |
| for k in buckets: | |
| buckets[k] = rng.permutation(buckets[k]).tolist() | |
| pipe = build() | |
| tr = pipe.transformer.eval() | |
| idx = d["start"] | |
| if a.base_tags: # refit учится ПОВЕРХ первых слияний, а не на голом блоке | |
| drops = apply_spec(tr, [t for t in a.base_tags.split(",") if t]) | |
| # после удаления блоков нумерация съезжает: переводим исходный номер в индекс живой модели | |
| idx = d["start"] - sum(1 for x in drops if x < d["start"]) | |
| print(f"база: {a.base_tags}, блок {d['start']} -> индекс {idx}", flush=True) | |
| block = tr.layers[idx].to("cuda") | |
| pipe.transformer = None | |
| pipe.vae = None | |
| pipe.text_encoder = None | |
| torch.cuda.empty_cache() | |
| if not a.full: | |
| inject_lora(block, a.rank) | |
| block.to("cuda") | |
| params = [p for p in block.parameters() if p.requires_grad] | |
| print(f"trainable {sum(p.numel() for p in params) / 1e6:.2f}M" | |
| f" (base {sum(p.numel() for p in block.parameters()) / 1e6:.1f}M)", flush=True) | |
| import bitsandbytes as bnb | |
| opt = bnb.optim.AdamW8bit(params, lr=a.lr, betas=(0.9, 0.95), weight_decay=0.0) | |
| def pack(idx): | |
| """индексы одной длины -> (x, target, mod) на GPU""" | |
| x = torch.cat([xs[i] for i in idx]).cuda().bfloat16() | |
| t = torch.cat([tgts[i] for i in idx]).cuda().bfloat16() | |
| return x, t, mod[list(idx)].cuda(), rope_map[x.shape[1]] | |
| def quality(idx): | |
| block.eval() | |
| rels, coss = [], [] | |
| for i in idx: | |
| x, t, m, rope = pack([i]) | |
| pred = block(x, rope, m).float() | |
| rels.append(float((((pred - t.float()) ** 2).mean(-1) | |
| / t.float().pow(2).mean(-1).clamp_min(1e-6)).mean())) | |
| coss.append(float(F.cosine_similarity(pred.flatten(1), t.float().flatten(1), dim=1))) | |
| return sum(rels) / len(rels), sum(coss) / len(coss) | |
| held = [int(v) for k in buckets for v in buckets[k][:max(1, len(buckets[k]) // 10)]][:12] | |
| rel0, cos0 = quality(held) | |
| print(f"до обучения (просто удалить блоки): rel {rel0:.4f} cos {cos0:.5f}", flush=True) | |
| tag = a.tag or f"win{d['start']}_{d['start'] + d['drop']}" | |
| out = os.path.join(LAB, "lora", f"{tag}.pt") | |
| os.makedirs(os.path.dirname(out), exist_ok=True) | |
| keys = [k for k in buckets if len(buckets[k]) > 1] or list(buckets) # бывает и по одному | |
| cursor = {k: 0 for k in keys} | |
| def next_batch(): | |
| k = keys[int(rng.integers(len(keys)))] | |
| b = buckets[k] | |
| c = cursor[k] | |
| idx = [b[(c + j) % len(b)] for j in range(min(a.batch, len(b)))] | |
| cursor[k] = c + a.batch | |
| return idx | |
| step, t_start = 0, time.time() | |
| while step < a.steps: | |
| idx = next_batch() | |
| block.train() | |
| x, t, m, rope = pack(idx) | |
| pred = block(x, rope, m) | |
| tf = t.float() | |
| rel_tok = ((pred.float() - tf) ** 2).mean(-1) / tf.pow(2).mean(-1).clamp_min(1e-6) | |
| loss = rel_tok.mean() + a.cos_w * (1 - F.cosine_similarity( | |
| pred.float().flatten(1), tf.flatten(1), dim=1).mean()) | |
| opt.zero_grad(set_to_none=True) | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(params, 1.0) | |
| opt.step() | |
| step += 1 | |
| if step % 500 == 0 or step == a.steps: | |
| rel, cos = quality(held) | |
| print(f"step {step:5d} loss {float(loss):.4f} | held rel {rel:.4f} cos {cos:.5f} " | |
| f"| {step / (time.time() - t_start):.1f} шаг/с", flush=True) | |
| save(out, block, a, d) | |
| rel, cos = quality(held) | |
| save(out, block, a, d) | |
| row = {"tag": tag, "start": d["start"], "drop": d["drop"], "rank": a.rank, "steps": step, | |
| "held_rel_before": round(rel0, 4), "held_cos_before": round(cos0, 5), | |
| "held_rel_after": round(rel, 4), "held_cos_after": round(cos, 5), | |
| "trainable_m": round(sum(p.numel() for p in params) / 1e6, 2)} | |
| with open(os.path.join(LAB, "window_results.jsonl"), "a") as f: | |
| f.write(json.dumps(row) + "\n") | |
| print(json.dumps(row), flush=True) | |
| def save(path, block, a, d): | |
| torch.save({"state": lora_state(block) if not a.full else | |
| {k: v.cpu() for k, v in block.state_dict().items()}, | |
| "full": a.full, "rank": a.rank, "start": d["start"], "drop": d["drop"]}, path) | |
| # ---------------------------------------------------------------- eval | |
| def cmd_eval(a): | |
| tag = a.tag or f"win{a.start}_{a.start + a.drop}" | |
| path = os.path.join(LAB, "lora", f"{tag}.pt") | |
| ck = torch.load(path, map_location="cpu", weights_only=False) | |
| pipe = build() | |
| tr = pipe.transformer.eval() | |
| cond = conditions(pipe) | |
| pipe.text_encoder = None | |
| torch.cuda.empty_cache() | |
| block = tr.layers[ck["start"]] | |
| if ck["full"]: | |
| block.load_state_dict(ck["state"]) | |
| else: | |
| inject_lora(block, ck["rank"]) | |
| block.to("cuda") | |
| block.load_state_dict(ck["state"], strict=False) | |
| if not ck["full"] and not a.no_merge: | |
| merge_lora(block) | |
| for l in block.proj.values(): | |
| for p in l.parameters(): | |
| p.requires_grad_(False) | |
| drop_blocks(tr, list(range(ck["start"] + 1, ck["start"] + ck["drop"] + 1))) | |
| images = sample(pipe, cond, size=a.size, steps=a.sample_steps, seed=a.seed) | |
| run_dir = os.path.join(LAB, "runs", tag) | |
| os.makedirs(run_dir, exist_ok=True) | |
| for i, im in enumerate(images): | |
| im.save(os.path.join(run_dir, f"{i:02d}.png")) | |
| grid(images, os.path.join(LAB, "runs", f"{tag}.png")) | |
| res = {"tag": tag, "blocks": len(tr.layers), | |
| "params_b": round(sum(p.numel() for p in tr.parameters()) / 1e9, 3), | |
| "size": a.size, "steps": a.steps, "seed": a.seed, | |
| "peak_vram_gib": round(torch.cuda.max_memory_allocated() / 2 ** 30, 2)} | |
| base_dir = os.path.join(LAB, "runs", "base") | |
| if os.path.isdir(base_dir): | |
| base_imgs = [Image.open(os.path.join(base_dir, f"{i:02d}.png")) for i in range(len(images))] | |
| m = Metrics() | |
| lp = m.lpips_vs(images, base_imgs) | |
| res["lpips_vs_base"] = None if lp is None else round(lp, 4) | |
| res["clip_score"] = round(m.clip_score(images, EVAL[:len(images)]), 2) | |
| with open(os.path.join(LAB, "results.jsonl"), "a") as f: | |
| f.write(json.dumps(res, ensure_ascii=False) + "\n") | |
| print(json.dumps(res, ensure_ascii=False), flush=True) | |
| def apply_spec(tr, tags): | |
| """Влить LoRA всех слияний в веса и удалить поглощённые блоки. Возвращает список удалённых.""" | |
| drops = set() | |
| for tag in tags: | |
| ck = torch.load(os.path.join(LAB, "lora", f"{tag}.pt"), map_location="cpu", weights_only=False) | |
| block = tr.layers[ck["start"]] | |
| if ck["full"]: | |
| block.load_state_dict(ck["state"]) | |
| else: | |
| inject_lora(block, ck["rank"]) | |
| block.to("cuda") | |
| block.load_state_dict(ck["state"], strict=False) | |
| merge_lora(block) | |
| drops |= set(range(ck["start"] + 1, ck["start"] + ck["drop"] + 1)) | |
| drop_blocks(tr, sorted(drops)) | |
| return sorted(drops) | |
| def cmd_combo(a): | |
| """Собрать все слияния в одну модель: каждый LoRA влит в свои веса, удалённые блоки — вон.""" | |
| tags = [t for t in a.tags.split(",") if t] | |
| pipe = build() | |
| tr = pipe.transformer.eval() | |
| cond = conditions(pipe) | |
| pipe.text_encoder = None | |
| torch.cuda.empty_cache() | |
| drops = set(apply_spec(tr, tags)) | |
| tag = a.tag or "combo" | |
| images = sample(pipe, cond, size=a.size, steps=a.sample_steps, seed=a.seed) | |
| run_dir = os.path.join(LAB, "runs", tag) | |
| os.makedirs(run_dir, exist_ok=True) | |
| for i, im in enumerate(images): | |
| im.save(os.path.join(run_dir, f"{i:02d}.png")) | |
| grid(images, os.path.join(LAB, "runs", f"{tag}.png")) | |
| res = {"tag": tag, "blocks": len(tr.layers), "dropped": sorted(drops), | |
| "params_b": round(sum(p.numel() for p in tr.parameters()) / 1e9, 3), | |
| "size": a.size, "steps": a.sample_steps, "seed": a.seed, | |
| "peak_vram_gib": round(torch.cuda.max_memory_allocated() / 2 ** 30, 2)} | |
| base_dir = os.path.join(LAB, "runs", "base") | |
| if os.path.isdir(base_dir): | |
| base_imgs = [Image.open(os.path.join(base_dir, f"{i:02d}.png")) for i in range(len(images))] | |
| m = Metrics() | |
| lp = m.lpips_vs(images, base_imgs) | |
| res["lpips_vs_base"] = None if lp is None else round(lp, 4) | |
| res["clip_score"] = round(m.clip_score(images, EVAL[:len(images)]), 2) | |
| with open(os.path.join(LAB, "results.jsonl"), "a") as f: | |
| f.write(json.dumps(res, ensure_ascii=False) + "\n") | |
| print(json.dumps(res, ensure_ascii=False), flush=True) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("cmd", choices=["cache", "train", "eval", "combo"]) | |
| ap.add_argument("--tags", default="", help="для combo: через запятую") | |
| ap.add_argument("--start", type=int, default=3, help="блок, который остаётся и получает LoRA") | |
| ap.add_argument("--drop", type=int, default=2, help="сколько блоков после него удалить") | |
| ap.add_argument("--samples", type=int, default=300) | |
| ap.add_argument("--warm", type=int, default=2, help="шагов Эйлера перед записью состояния") | |
| ap.add_argument("--size", type=int, default=512) | |
| ap.add_argument("--steps", type=int, default=3000, help="шаги обучения") | |
| ap.add_argument("--sample-steps", type=int, default=20, help="шаги сэмплинга на eval") | |
| ap.add_argument("--seed", type=int, default=1234) | |
| ap.add_argument("--rank", type=int, default=32) | |
| ap.add_argument("--lr", type=float, default=1e-3) | |
| ap.add_argument("--batch", type=int, default=4) | |
| ap.add_argument("--cos-w", type=float, default=0.1) | |
| ap.add_argument("--full", action="store_true", help="учить весь блок, не только LoRA") | |
| ap.add_argument("--tag", default=None) | |
| ap.add_argument("--base-tags", default="", help="сначала влить эти слияния в веса модели") | |
| ap.add_argument("--no-merge", action="store_true", help="не вливать LoRA в веса на eval") | |
| a = ap.parse_args() | |
| {"cache": cmd_cache, "train": cmd_train, "eval": cmd_eval, "combo": cmd_combo}[a.cmd](a) | |
| if __name__ == "__main__": | |
| main() | |