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 adapter/src/moss_lib.py from AiArtLab/mossling: direct link, hf CLI and curl.
- Browser
- Download file 13.5 kB
-
https://huggingface.co/AiArtLab/mossling/resolve/main/adapter/src/moss_lib.py
- Command line
-
hf download hf://AiArtLab/mossling/adapter/src/moss_lib.py
-
curl -L -o moss_lib.py https://huggingface.co/AiArtLab/mossling/resolve/main/adapter/src/moss_lib.py
13.5 kB
| #!/usr/bin/env python3 | |
| """Всё про MOSS-Image в одном месте: шаблон промпта, учитель (Qwen3.5-9B), ученик (0.8B), пайплайн. | |
| Задача та же, что в docs/qwen-image-2.1-adapter.md: заменить текстовый энкодер 15.6 ГБ | |
| (Qwen3.5-VL-9B, `language_model`, hidden 4096) на замороженную 0.8B + адаптер. Но здесь | |
| проще и одновременно строже: | |
| * токенизатор у учителя и ученика ОДИН (248 077 токенов, совпадают и строки, и id), | |
| поэтому общая сегментация не нужна — id одни и те же; | |
| * цель — `last_hidden_state`, то есть ПОСТ-норм выход слоя 32 (Qwen3_5TextModel сам | |
| применяет `norm`). Это не то же самое, что в 2.1 (там был пред-норм выход), и именно | |
| это видит DiT: text_embedder.proj 4096 -> 3072; | |
| * учитель и ученик — одно семейство (Qwen3.5 hybrid linear attention), срезы ученика | |
| берутся после его full-attention слоёв (3,7,11,15,19,23) -> hs[4,8,12,16,20,24]; | |
| * пайплайн принимает `prompt_embeds`/`negative_prompt_embeds` официально, monkeypatch | |
| не нужен (но метод `from_pretrained(text_encoder=None)` НЕ пропускает загрузку TE — | |
| пайплайн собираем вручную, см. build_pipeline). | |
| """ | |
| import json | |
| import os | |
| import sys | |
| import torch | |
| MOSS_DIR = os.environ.get("MOSS_DIR", "/home/recoilme/moss") | |
| STUDENT = os.environ.get("MOSS_STUDENT", "Qwen/Qwen3.5-0.8B") | |
| # раскладка папки модели: ученик — в text_encoder/, оригинальный 9B — рядом под явным именем | |
| STUDENT_SUBDIR = "text_encoder" | |
| TEACHER_SUBDIRS = ("text_encoder_qwen3.5-9b", "text_encoder") | |
| # после полных attention-слоёв ученика (0-based 3,7,11,15,19,23) -> hidden_states[4,8,...] | |
| LAYERS = (4, 8, 12, 16, 20, 24) | |
| # ---------------------------------------------------------------- токены и шаблон | |
| def load_tokenizer(moss_dir=None): | |
| """Токенизатор учителя = токенизатор ученика, берём из папки модели.""" | |
| from transformers import AutoTokenizer | |
| return AutoTokenizer.from_pretrained(os.path.join(moss_dir or MOSS_DIR, "tokenizer")) | |
| def render(tok, text): | |
| """Ровно то, что делает `MOSSImagePipeline.encode_prompt` (сырая строка, не id).""" | |
| return tok.apply_chat_template([{"role": "user", "content": text}], tokenize=False, | |
| add_generation_prompt=True, enable_thinking=True) | |
| def encode(tok, texts, max_len=4096): | |
| """list[str] -> list[list[int]]: шаблон + обрезка хвоста, как в пайплайне (truncation).""" | |
| return [tok(render(tok, t), truncation=True, max_length=max_len)["input_ids"] for t in texts] | |
| # ---------------------------------------------------------------- модели | |
| def _has_config(path): | |
| return os.path.isfile(os.path.join(path, "config.json")) | |
| def teacher_dir(moss_dir=None): | |
| """Папка оригинального текстового энкодера 9B (не удаляем и не переименовываем в text_encoder).""" | |
| root = moss_dir or MOSS_DIR | |
| for name in TEACHER_SUBDIRS: | |
| path = os.path.join(root, name) | |
| if _has_config(path): | |
| return path | |
| return os.path.join(root, TEACHER_SUBDIRS[0]) | |
| def student_dir(moss_dir=None): | |
| """Ученик: локальная копия в папке модели (`text_encoder/`), иначе HF-репозиторий.""" | |
| local = os.path.join(moss_dir or MOSS_DIR, STUDENT_SUBDIR) | |
| return local if _has_config(local) else STUDENT | |
| def load_text_model(path, dtype=torch.bfloat16, load_8bit=False, device="cuda"): | |
| """Qwen3.5 в любой раскладке — класс берём из `config.json`. | |
| Две раскладки одного и того же семейства: `Qwen3_5Model` (ключи `language_model.*` — так лежит | |
| текстовый энкодер MOSS) и `Qwen3_5ForConditionalGeneration` (ключи `model.*` — родной чекпойнт | |
| Qwen3.5-0.8B). Угадывать нельзя: не та раскладка грузится без ошибки, но со случайными весами | |
| и только предупреждением в логе — поэтому смотрим architectures и падаем, если не узнали. | |
| """ | |
| import transformers | |
| from transformers import AutoConfig | |
| arch = (AutoConfig.from_pretrained(path, local_files_only=os.path.isdir(path)).architectures or [""])[0] | |
| classes = {"Qwen3_5Model": transformers.Qwen3_5Model, | |
| "Qwen3_5ForConditionalGeneration": transformers.Qwen3_5ForConditionalGeneration} | |
| if arch not in classes: | |
| raise SystemExit(f"{path}: не знаю, как грузить architectures={arch!r}") | |
| local = {"local_files_only": os.path.isdir(path)} | |
| if load_8bit: | |
| from transformers import BitsAndBytesConfig | |
| return classes[arch].from_pretrained( | |
| path, quantization_config=BitsAndBytesConfig(load_in_8bit=True), | |
| device_map={"": 0}, **local).eval() | |
| return classes[arch].from_pretrained(path, dtype=dtype, **local).to(device).eval() | |
| def text_stack(model): | |
| """Текстовая стопка без vision-tower: у MOSS TE это `language_model`, у 0.8B — `model.language_model`.""" | |
| for name in ("language_model", "model.language_model"): | |
| obj = model | |
| for part in name.split("."): | |
| obj = getattr(obj, part, None) | |
| if obj is None: | |
| break | |
| if obj is not None and hasattr(obj, "norm"): | |
| return obj | |
| raise SystemExit("не нашёл текстовую модель с .norm") | |
| class Teacher: | |
| """Qwen3.5-VL-9B (текстовая часть): цель = post-norm `last_hidden_state`, (B,L,4096).""" | |
| def __init__(self, path=None, device="cuda", dtype=torch.bfloat16, load_8bit=False): | |
| path = path or teacher_dir() | |
| self.base = load_text_model(path, dtype=dtype, load_8bit=load_8bit, device=device) | |
| self.text = text_stack(self.base) | |
| self.quant = "8bit" if load_8bit else str(dtype).split(".")[-1] | |
| cfg = self.text.config | |
| print(f"учитель : {sum(p.numel() for p in self.base.parameters()) / 1e9:.2f}B, " | |
| f"слоёв {cfg.num_hidden_layers}, hidden {cfg.hidden_size}, {self.quant}, " | |
| f"VRAM {torch.cuda.memory_allocated() / 2**30:.1f} ГиБ", flush=True) | |
| def last_hidden(self, ids, mask=None): | |
| """(B,L) -> (B,L,4096) float32, пост-норм выход последнего слоя.""" | |
| return self.text(input_ids=ids, attention_mask=mask, use_cache=False).last_hidden_state.float() | |
| def load_teacher(device="cuda", prefer_8bit=False, path=None): | |
| """Учитель целиком, а если не влезает в карту — 8 бит. Возвращает (teacher, режим). | |
| После неудачной попытки надо отпустить traceback: он держит кадр с недогруженной моделью, | |
| и без gc/empty_cache вторая попытка падает на уже занятой памяти (проверено). | |
| """ | |
| order = [True, False] if prefer_8bit else [False, True] | |
| errors = [] | |
| for load_8bit in order: | |
| name = "8-бит" if load_8bit else "bf16" | |
| try: | |
| return Teacher(path=path, device=device, load_8bit=load_8bit), \ | |
| ("8bit" if load_8bit else "bf16") | |
| except (torch.OutOfMemoryError, RuntimeError) as exc: | |
| if "out of memory" not in str(exc).lower(): | |
| raise | |
| free_gib = torch.cuda.mem_get_info()[0] / 2**30 | |
| print(f" {name} не влез ({free_gib:.1f} ГиБ свободно) — пробую дальше", flush=True) | |
| errors.append(f"{name}: {str(exc).splitlines()[0][:100]}") | |
| del exc | |
| empty_cache() | |
| raise SystemExit("учитель не грузится: " + " | ".join(errors)) | |
| class Student: | |
| """Qwen3.5-0.8B (заморожен): срезы hidden_states -> (B, L, K*hidden).""" | |
| def __init__(self, path=None, layers=LAYERS, device="cuda", dtype=torch.bfloat16): | |
| path = path or student_dir() | |
| self.path = path | |
| self.base = load_text_model(path, dtype=dtype, device=device) | |
| self.base.requires_grad_(False) | |
| self.text = text_stack(self.base) | |
| if self.text.config.hidden_size >= 4096: | |
| raise SystemExit(f"{path}: hidden {self.text.config.hidden_size} — это учитель, а не " | |
| f"ученик (у Qwen3.5-0.8B hidden 1024)") | |
| self.layers = list(layers) | |
| self.device = device | |
| cfg = self.text.config | |
| print(f"ученик : {sum(p.numel() for p in self.base.parameters()) / 1e9:.2f}B, " | |
| f"слоёв {cfg.num_hidden_layers}, hidden {cfg.hidden_size}, срезы {self.layers}", | |
| flush=True) | |
| def in_dim(self): | |
| return len(self.layers) * self.text.config.hidden_size | |
| def hidden(self, ids, mask=None): | |
| out = self.text(input_ids=ids, attention_mask=mask, output_hidden_states=True, use_cache=False) | |
| hs = out.hidden_states | |
| assert hs is not None and len(hs) > max(self.layers), f"скрытых состояний {len(hs)}" | |
| x = torch.stack([hs[i] for i in self.layers], dim=1) # (B, K, L, d) | |
| return x.permute(0, 2, 1, 3).reshape(x.shape[0], x.shape[2], -1).float() | |
| # ---------------------------------------------------------------- инференс | |
| def build_pipeline(moss_dir=None, dit_dtype=torch.bfloat16, vae_dtype=torch.float32): | |
| """MOSSImagePipeline без текстового энкодера. | |
| `DiffusionPipeline.from_pretrained(..., text_encoder=None)` здесь НЕ работает: text_encoder | |
| объявлен в __init__ с дефолтом None, поэтому diffusers не считает его «переданным» и лезет | |
| грузить 15.6 ГБ. Собираем компоненты руками — их три, и все локальные. | |
| """ | |
| moss_dir = moss_dir or MOSS_DIR | |
| if moss_dir not in sys.path: | |
| sys.path.insert(0, moss_dir) | |
| from diffusers import AutoencoderKLFlux2 | |
| from pipeline_moss_image import MOSSImagePipeline, MOSSImageScheduler, MOSSImageTransformer | |
| transformer = MOSSImageTransformer.from_pretrained( | |
| os.path.join(moss_dir, "transformer"), torch_dtype=dit_dtype, local_files_only=True) | |
| vae = AutoencoderKLFlux2.from_pretrained( | |
| os.path.join(moss_dir, "vae"), torch_dtype=vae_dtype, local_files_only=True) | |
| cfg = json.load(open(os.path.join(moss_dir, "scheduler", "scheduler_config.json"))) | |
| keys = ("num_train_timesteps", "shift", "training_shift") | |
| scheduler = MOSSImageScheduler(**{k: cfg[k] for k in keys if k in cfg}) | |
| return MOSSImagePipeline(transformer.eval(), scheduler, text_encoder=None, tokenizer=None, | |
| vae=vae.eval()) | |
| def condition(adapter, student, tok, texts, max_len=4096): | |
| """list[str] -> list[(embeds [L,4096] bf16, mask [L] bool)]: по одному промпту без паддинга. | |
| Пайплайн кодирует каждый промпт отдельно (`unpadded individual forwards`), поэтому и здесь | |
| так же: длину маскирует сам DiT, а не токенизатор. | |
| """ | |
| device = next(adapter.parameters()).device | |
| out = [] | |
| for ids in encode(tok, texts, max_len): | |
| si = torch.tensor([ids], dtype=torch.long, device=device) | |
| mask = torch.ones_like(si, dtype=torch.bool) | |
| y = adapter(student.hidden(si, mask), mask) | |
| out.append((y[0].to(torch.bfloat16).cpu(), mask[0].cpu())) | |
| return out | |
| def generate(pipe, pos, neg, size=1024, steps=30, seed=1234, guidance=3.0, device="cuda"): | |
| """pos/neg: (embeds [L,4096], mask [L]) -> PIL. CFG включён всегда (uncond_skip_layers).""" | |
| emb, mask = pos | |
| nemb, nmask = neg | |
| gen = torch.Generator(device).manual_seed(seed) | |
| return pipe(prompt_embeds=emb[None].to(device), prompt_attention_mask=mask[None].to(device), | |
| negative_prompt_embeds=nemb[None].to(device), | |
| negative_prompt_attention_mask=nmask[None].to(device), | |
| height=size, width=size, num_inference_steps=steps, guidance_scale=guidance, | |
| generator=gen, output_type="pil").images[0] | |
| def empty_cache(): | |
| """Вернуть освобождённую память карты. Сами объекты надо удалить у себя (`del`): `del` внутри | |
| функции снимает только локальную ссылку, объект остаётся жив, пока на него смотрит вызывающий.""" | |
| import gc | |
| gc.collect() | |
| torch.cuda.empty_cache() | |