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
File size: 13,892 Bytes
2e26e31 | 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 245 246 247 248 249 250 251 252 253 254 255 256 257 | #!/usr/bin/env python3
"""Схема адаптера и его загрузка — в одном месте.
Раньше класс адаптера был скопирован в train_adapter.py и test_klein_adapter.py
с комментарием «схемы обязаны совпадать»: любая правка в одном файле ломала
второй молча, потому что веса грузятся по именам и совпадают по shape.
Теперь обучение, A/B-тест, экспорт в safetensors и пример с klein ходят сюда.
Схема: MLP поточечно (каждый токен независимо)
in -> [norm] -> hidden -> GELU(tanh) -> ... -> out
norm="ln-per-layer" — каждый срез слоя ТЕ нормируется отдельно, затем склейка
(нормы скрытых состояний растут с глубиной в 40 раз: 13 у слоя 2, 557 у слоя 27,
поэтому без пер-слойной нормировки ранние слои не влияют на выход).
"""
import torch
import torch.nn as nn
class PerLayerNorm(nn.Module):
"""Нормировка каждого среза слоя отдельно + общая нормировка склейки.
Срезы учителя имеют разный масштаб (у Qwen3.5-9B норма растёт с глубиной в десятки раз),
поэтому без пер-слойной нормировки ранние слои почти не влияют на выход.
"""
def __init__(self, in_dim, n_slices):
super().__init__()
assert in_dim % n_slices == 0, f"{in_dim} не делится на {n_slices}"
self.n, self.d = n_slices, in_dim // n_slices
self.per = nn.LayerNorm(self.d, elementwise_affine=False)
self.all = nn.LayerNorm(in_dim)
def forward(self, x):
b, l, _ = x.shape
return self.all(self.per(x.view(b, l, self.n, self.d)).reshape(b, l, -1))
def build_adapter(in_dim, out_dim, hidden=4096, layers=2, norm="none", n_slices=1,
attention=0, attn_dim=1024, attn_heads=8, max_len=256,
mixer=0, mixer_heads=8, mixer_ffn=2):
"""MLP-проектор: `layers` линейных слоёв, `layers-1` GELU(tanh) между ними.
attention>0 — добавить residual-ветку self-attention на `attention` блоков
(выход ветки инициализирован нулями, поэтому на старте функция та же, что у чистого MLP).
"""
if layers < 2:
raise SystemExit("нужно хотя бы 2 линейных слоя")
mods = []
if norm == "ln":
mods.append(nn.LayerNorm(in_dim))
elif norm == "ln-per-layer":
mods.append(PerLayerNorm(in_dim, n_slices))
elif norm == "rms":
mods.append(nn.RMSNorm(in_dim))
elif norm != "none":
raise SystemExit(f"неизвестный --norm: {norm}")
mods += [nn.Linear(in_dim, hidden), nn.GELU(approximate="tanh")]
for _ in range(layers - 2):
mods += [nn.Linear(hidden, hidden), nn.GELU(approximate="tanh")]
mods.append(nn.Linear(hidden, out_dim))
mix = (SliceMixer(n_slices, in_dim // n_slices, out_dim, mixer_heads, mixer, mixer_ffn)
if mixer else None)
attn = (AttnMixer(in_dim, out_dim, attn_dim, attn_heads, attention, max_len)
if attention else None)
if mix is not None or attn is not None:
return AttnAdapter(mods, attn, mix)
return nn.Sequential(*mods)
class AttnBlock(nn.Module):
"""Pre-norm self-attention + FFN. Без причинности: текст не автогенеративный,
а DiT всё равно видит всю последовательность целиком."""
def __init__(self, d_model, n_heads, ffn_mult=4):
super().__init__()
self.n1 = nn.LayerNorm(d_model)
self.attn = nn.MultiheadAttention(d_model, n_heads, batch_first=True)
self.n2 = nn.LayerNorm(d_model)
self.ffn = nn.Sequential(nn.Linear(d_model, d_model * ffn_mult),
nn.GELU(approximate="tanh"),
nn.Linear(d_model * ffn_mult, d_model))
def forward(self, x, key_padding_mask=None):
h = self.n1(x)
x = x + self.attn(h, h, h, need_weights=False, key_padding_mask=key_padding_mask)[0]
return x + self.ffn(self.n2(x))
class AttnMixer(nn.Module):
"""Ветка внимания: своя нормировка -> проекция в d_model -> N блоков -> проекция в out_dim.
Последняя проекция инициализирована нулями: на старте ветка не добавляет ничего (см. AttnAdapter).
"""
def __init__(self, in_dim, out_dim, d_model=1024, n_heads=8, blocks=2, max_len=256):
super().__init__()
self.norm = nn.LayerNorm(in_dim)
self.inp = nn.Linear(in_dim, d_model)
self.pos = nn.Parameter(torch.zeros(1, max_len, d_model))
self.blocks = nn.ModuleList([AttnBlock(d_model, n_heads) for _ in range(blocks)])
self.out = nn.Linear(d_model, out_dim)
nn.init.zeros_(self.out.weight)
nn.init.zeros_(self.out.bias)
def forward(self, x, key_padding_mask=None):
h = self.inp(self.norm(x))
h = h + self.pos[:, :h.shape[1]]
for b in self.blocks:
h = b(h, key_padding_mask)
return self.out(h)
class SliceMixer(nn.Module):
"""Attention ПО ОСИ СРЕЗОВ (как txtfusion Krea 2) + добавка к выходу адаптера.
Порядок как у Krea: срезы нормируются поштучно (нормы растут с глубиной), идут
последовательностью длины K через 2 блока self-attention, затем `Linear(K->1)`
схлопывает ось — как `projector` в Krea.
Отличие от Krea — точка инъекции: там fusion стоит на входе ДиТ'a, у нас обученная МЛП
слишком чувствительна к входу (добавка 0.4% к входу меняет выход в разы — проверено),
поэтому результат добавляется к ВЫХОДУ (zero-init), как ветка внимания по токенам.
"""
def __init__(self, n_slices, d, out_dim, n_heads=8, blocks=2, ffn_mult=2):
super().__init__()
self.n, self.d = n_slices, d
self.per = nn.LayerNorm(d, elementwise_affine=False)
self.blocks = nn.ModuleList([AttnBlock(d, n_heads, ffn_mult) for _ in range(blocks)])
self.proj = nn.Linear(n_slices, 1, bias=False)
self.out = nn.Linear(d, out_dim)
nn.init.zeros_(self.out.weight)
nn.init.zeros_(self.out.bias)
def forward(self, x):
b, l, _ = x.shape
h = self.per(x.view(b, l, self.n, self.d)).reshape(b * l, self.n, self.d)
for blk in self.blocks:
h = blk(h)
h = self.proj(h.permute(0, 2, 1)).squeeze(-1)
return self.out(h).reshape(b, l, -1)
class AttnAdapter(nn.Module):
"""Поточечная MLP (self.mlp) + residual-ветки: по токенам (self.attn) и по срезам (self.mixer).
Обе ветки инициализированы нулём и добавляются к **выходу** MLP: на вход добавлять
нельзя — обученная MLP разгоняет крошечную добавку в разы (см. SliceMixer).
Ключи MLP получают префикс "mlp.", ветки — "attn." и "mixer."; чекпоинт без
префиксов грузится с переносом (см. train_adapter).
"""
def __init__(self, mods, attn=None, mixer=None):
super().__init__()
self.mlp = nn.Sequential(*mods)
self.attn = attn
self.mixer = mixer
def forward(self, x, mask=None):
"""mask: (B, L) bool, True = реальный токен. В attention уходит key_padding_mask = ~mask,
иначе (как было в klein до v14) на длине 256 внимание смотрит в паддинг."""
out = self.mlp(x)
if self.attn is not None:
kpm = None if mask is None else ~mask.bool()
out = out + self.attn(x, kpm)
if self.mixer is not None:
out = out + self.mixer(x)
return out
def infer_arch(state_dict):
"""Размеры и тип нормировки прямо из весов — чтобы примеру не нужны были флаги.
Нормировка опознаётся по ключам: `0.per.weight` — ln-per-layer, `0.weight`+`0.bias`
(1-D) — LayerNorm, `0.weight` (1-D) без bias — RMSNorm, `0.weight` (2-D) — нормировки нет.
"""
mlp = {k[len("mlp."):]: v for k, v in state_dict.items() if k.startswith("mlp.")}
if not mlp: # старый поточечный чекпоинт: без префикса
mlp = {k: v for k, v in state_dict.items() if not k.startswith("attn.")}
linears = sorted((int(k.split(".")[0]), k) for k, v in mlp.items()
if k.endswith(".weight") and v.dim() == 2 and k.split(".")[0].isdigit())
if not linears:
raise SystemExit("в весах нет линейных слоёв")
first, last = mlp[linears[0][1]], mlp[linears[-1][1]]
if "0.per.weight" in mlp or "0.all.weight" in mlp:
norm = "ln-per-layer"
in_dim = mlp["0.all.weight" if "0.all.weight" in mlp else "0.per.weight"].shape[0]
elif any(k.startswith("0.") and v.dim() == 1 for k, v in mlp.items()):
norm = "ln" if "0.bias" in mlp else "rms"
in_dim = first.shape[1]
else:
norm = "none"
in_dim = first.shape[1]
attention = len({k.split(".")[2] for k in state_dict if k.startswith("attn.blocks.")})
mixer = len({k.split(".")[2] for k in state_dict if k.startswith("mixer.blocks.")})
arch = {"in_dim": in_dim, "out_dim": last.shape[0], "hidden": first.shape[0],
"proj_layers": len(linears), "norm": norm, "attention": attention, "mixer": mixer}
if attention:
arch["attn_dim"] = state_dict["attn.out.weight"].shape[1]
# таблица позиций ветки: длина входа на инференсе может быть больше, чем при обучении,
# но собрать адаптер надо ровно под сохранённые веса, иначе load_state_dict падает
arch["attn_max_len"] = state_dict["attn.pos"].shape[1]
return arch
def load_adapter(path, device="cuda", dtype=None, n_slices=None):
"""Веса -> (adapter в eval, meta). Понимает .pt (голые веса или полный чекпоинт) и .safetensors.
n_slices (число слоёв ТЕ в склейке) нельзя вытащить из весов: у PerLayerNorm
нормировка без обучаемых параметров. Поэтому он берётся из meta, а если её нет —
из аргумента; при norm="ln-per-layer" и отсутствии обоих это ошибка, а не догадка.
"""
meta = {}
if path.endswith(".safetensors"):
from safetensors.torch import load_file
from safetensors import safe_open
sd = load_file(path)
with safe_open(path, framework="pt") as f:
meta = dict(f.metadata() or {})
else:
sd = torch.load(path, map_location="cpu", weights_only=False)
if isinstance(sd, dict) and "state_dict" in sd: # полный чекпоинт обучения
meta = {k: v for k, v in sd.items() if k != "state_dict"}
sd = sd["state_dict"]
arch = infer_arch(sd)
if meta.get("student_layers"):
layers = [int(x) for x in str(meta["student_layers"]).split(",")]
n_slices = n_slices or len(layers)
if n_slices is None:
if arch["norm"] == "ln-per-layer":
raise SystemExit("нужен n_slices: в весах его нет, а norm=ln-per-layer без него "
"загрузится с неверной формой и будет считать мусор")
n_slices = 1
adapter = build_adapter(arch["in_dim"], arch["out_dim"], arch["hidden"],
arch["proj_layers"], arch["norm"], n_slices=n_slices,
attention=arch["attention"],
attn_dim=arch.get("attn_dim", 1024),
attn_heads=int(meta.get("attn_heads", 8)),
max_len=arch.get("attn_max_len", 256),
mixer=arch.get("mixer", 0),
mixer_heads=int(meta.get("mixer_heads", 8)))
adapter.load_state_dict(sd)
if dtype is not None:
adapter = adapter.to(dtype)
adapter = adapter.to(device).eval()
meta.update(arch | {"n_slices": n_slices})
return adapter, meta
def save_safetensors(adapter, path, meta=None):
"""Сохранить адаптер в safetensors вместе с метаданными (строки, как требует формат)."""
from safetensors.torch import save_file
md = {k: ("".join(map(str, v)) if isinstance(v, (list, tuple)) else str(v))
for k, v in (meta or {}).items()}
save_file({k: v.contiguous() for k, v in adapter.state_dict().items()}, path, metadata=md)
|