mossling / adapter /src /cache_targets.py
recoilme's picture
Mossling: MOSS-Image pruned to a 16 GB card (alpha)
2e26e31 verified
Raw History Blame Contribute Delete
9.23 kB
#!/usr/bin/env python3
"""Шаг 1: цели учителя (Qwen3.5-9B) на диск.
Зачем отдельным шагом: 15.63 ГиБ весов учителя при 16 ГиБ карты не живут вместе ни с чем, а цель
(пост-норм `last_hidden_state` слоя 32) считается один раз на промпт и дальше не меняется.
Кэш позволяет перебирать гиперпараметры адаптера часами без единого прогона учителя.
Формат кэша (shard_XXXXX.pt, torch.save, грузится с weights_only=True):
{"items": [{"ids": int32[L], "y": bf16[L, 4096]}, ...], "texts": [...], "kinds": [...]}
Пробы и val — те же поля отдельными файлами, плюс meta.json с параметрами и статистикой норм.
python3 src/cache_targets.py --prompts-dir ~/dataset --out cache/teacher --len 256
# продолжить прерванный прогон: те же аргументы, готовые шарды пропускаются
"""
import argparse
import json
import os
import sys
import time
import numpy as np
import torch
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from moss_lib import MOSS_DIR, encode, load_teacher, load_tokenizer # noqa: E402
from prompts import NEGATIVES, PROBE, load_prompts # noqa: E402
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def save_items(path, items, texts, kinds):
torch.save({"items": items, "texts": texts, "kinds": kinds}, path)
def load_items(path):
return torch.load(path, map_location="cpu", weights_only=True)
def main():
ap = argparse.ArgumentParser(description="цели учителя MOSS (Qwen3.5-9B) -> кэш на диске")
ap.add_argument("--moss-dir", default=MOSS_DIR)
ap.add_argument("--teacher-dir", default=None, help="папка учителя (по умолчанию <moss-dir>/text_encoder)")
ap.add_argument("--prompts-dir", default=os.path.expanduser("~/dataset"))
ap.add_argument("--out", default=os.path.join(ROOT, "cache", "teacher"))
ap.add_argument("--limit", type=int, default=0, help="0 — весь пул")
ap.add_argument("--len", type=int, default=256, help="макс. длина последовательности (обрезка хвоста)")
ap.add_argument("--shard", type=int, default=500, help="промптов в шарде")
ap.add_argument("--val-size", type=int, default=128, help="сколько промптов отложить на val")
ap.add_argument("--batch", type=int, default=1,
help="промптов за один forward; 1 — бит в бит как в пайплайне, >1 меняет "
"цель на ~0.5%% (разные блоки ядер, bf16)")
ap.add_argument("--prefer-8bit", action="store_true", help="сразу 8 бит, без попытки bf16")
ap.add_argument("--seed", type=int, default=0)
args = ap.parse_args()
tok = load_tokenizer(args.moss_dir)
pool = load_prompts(args.prompts_dir, limit=args.limit, seed=args.seed)
if len(pool) < args.val_size + 1:
raise SystemExit(f"пул слишком мал: {len(pool)}")
# спец-промпты (пустой + негативы) отдельным файлом, val — хвост пула
special_texts = [""] + list(NEGATIVES)
special_kinds = ["empty"] + ["neg"] * len(NEGATIVES)
val_texts = pool[:args.val_size]
train_texts = pool[args.val_size:]
probe_texts = [t for _, t in PROBE]
print(f"кэш: {len(train_texts)} обучающих + special {len(special_texts)} + val {len(val_texts)} "
f"+ probe {len(probe_texts)}, len={args.len}", flush=True)
t0 = time.time()
ids = encode(tok, special_texts + train_texts + val_texts + probe_texts, args.len)
n_special, n_train, n_val = len(special_texts), len(train_texts), len(val_texts)
print(f"токенизация: {time.time() - t0:.1f} c, медиана длины "
f"{int(np.median([len(r) for r in ids[n_special:n_special + n_train]]))} из {args.len}",
flush=True)
teacher, mode = load_teacher(prefer_8bit=args.prefer_8bit, path=args.teacher_dir)
def targets(rows):
"""list[list[int]] -> list[(ids, y)]: батч с правым паддингом, цель обрезается по длине.
Паддинг справа + маска: реальные токены от него не зависят (внимание причинное), это
проверяется selfcheck-ом ниже.
"""
out = []
for s in range(0, len(rows), args.batch):
chunk = rows[s:s + args.batch]
length = max(len(r) for r in chunk)
idb = torch.full((len(chunk), length), tok.pad_token_id, dtype=torch.long)
mb = torch.zeros((len(chunk), length), dtype=torch.long)
for i, r in enumerate(chunk):
idb[i, :len(r)] = torch.tensor(r)
mb[i, :len(r)] = 1
y = teacher.last_hidden(idb.to("cuda"), mb.to("cuda"))
if not torch.isfinite(y).all(): # 8-бит считает в fp16: ловим переполнение сразу
raise SystemExit("в цели NaN/inf — учитель посчитал в fp16 и переполнился")
for i, r in enumerate(chunk):
out.append((torch.tensor(r, dtype=torch.int32),
y[i, :len(r)].to(torch.bfloat16).cpu()))
return out
if args.batch > 1: # паддинг в батче меняет только округление bf16
check_rows = ids[:4]
single = torch.cat([y for r in check_rows for _, y in targets([r])]).float()
batched = torch.cat([y for _, y in targets(check_rows)]).float()
rel = (single - batched).abs().max().item() / single.norm(dim=-1).mean().item()
print(f" selfcheck: батч меняет цель на {rel * 100:.2f}% (округление bf16, ждём <1%)", flush=True)
if rel > 0.01:
raise SystemExit("батч с паддингом меняет цель сильнее округления — считай --batch 1")
os.makedirs(args.out, exist_ok=True)
special = [{"ids": r, "y": y} for r, y in targets(ids[:n_special])]
save_items(os.path.join(args.out, "special.pt"), special, special_texts, special_kinds)
probe = [{"ids": r, "y": y} for r, y in targets(ids[n_special + n_train + n_val:])]
save_items(os.path.join(args.out, "probe.pt"), probe, probe_texts, [n for n, _ in PROBE])
val = [{"ids": r, "y": y} for r, y in targets(ids[n_special + n_train:n_special + n_train + n_val])]
save_items(os.path.join(args.out, "val.pt"), val, val_texts, ["val"] * n_val)
norms, shards, tok_total, done, t0 = [], [], 0, 0, time.time()
for s in range(0, n_train, args.shard):
path = os.path.join(args.out, f"shard_{s // args.shard:05d}.pt")
shards.append(os.path.basename(path))
if os.path.exists(path): # возобновление прерванного прогона
data = load_items(path)
items = data["items"]
done += len(items)
else:
chunk = list(range(s, min(s + args.shard, n_train)))
items = [{"ids": r, "y": y} for r, y in targets([ids[n_special + i] for i in chunk])]
save_items(path, items, [train_texts[i] for i in chunk], ["pool"] * len(chunk))
done += len(items)
el = time.time() - t0
t0 = time.time()
print(f" {done}/{n_train} промптов, {el:.0f} c на шард, "
f"осталось ~{el * (n_train - done) / max(1, len(items)) / 60:.0f} мин", flush=True)
norms += [it["y"].float().norm(dim=-1).numpy() for it in items]
tok_total += int(sum(it["ids"].numel() for it in items))
norm = np.concatenate(norms)
meta = {"moss_dir": args.moss_dir, "prompts_dir": args.prompts_dir, "teacher_mode": mode,
"len": args.len, "batch": args.batch,
"n_train": n_train, "n_val": n_val, "n_probe": len(probe_texts),
"n_special": n_special, "shards": shards, "tokens": tok_total,
"norms": {f"p{q}": float(np.percentile(norm, q)) for q in (1, 10, 50, 90, 99)},
"norm_mean": float(norm.mean()), "norm_floor": float(np.median(norm))}
with open(os.path.join(args.out, "meta.json"), "w") as f:
json.dump(meta, f, indent=1)
raw = sum(os.path.getsize(os.path.join(args.out, s)) for s in shards)
print(f"готово: {args.out}, {n_train} промптов, {tok_total / 1e6:.1f}M токенов, "
f"{raw / 2**30:.1f} ГиБ; норма цели медиана {meta['norm_floor']:.0f} "
f"(p1 {meta['norms']['p1']:.0f}, p99 {meta['norms']['p99']:.0f}); учитель {mode}", flush=True)
if __name__ == "__main__":
main()