Buckets:

redmingwei's picture
download
raw
16.9 kB
#!/usr/bin/env python3
"""exp102_cache.py — exp102(②③ 条件生成线:**连续条件 + 冻结 SD1.5**)的**数据缓存**一步。
## 这一步在做什么
把 exp102 训练/生成所需的**两样重资产**预先算好、落盘到 `/domains`:
1. **EEG 条件**(`--step eeg`):用**冻结**的 exp93b encoder(`encoder_best.pt`)把
THINGS-EEG **train split** 的 EEG 编成 **1024 维条件向量**(每被试 × 每图 × 每试次一条)。
2. **VAE 潜变量**(`--step vae`):用 **SD1.5 自带的** VAE 把 train split 的 **16,540 张**照片
编成 `(4, 64, 64)` 潜变量(缩放系数用 SD1.5 的 `scaling_factor` = 0.18215)。
## 为什么这么写(算力与内存的账)
- **只训 adapter**(§四十四·五)⇒ 训练步里**不能**现编 VAE 潜变量:
512×512 的一次 VAE 编码 ≈ 一次 UNet 前向的 1/5 成本 ⇒ 现编等于白烧 ~20% 算力
⇒ **必须预缓存**。
- ⚠️ **潜变量必须用 SD1.5 自带的 VAE**:底模被换掉 ⇒ 潜变量分布就变了 ⇒ 训练目标对不上。
(探针里试过的 `sd-vae-ft-mse` **只是探测用**,本轮**不用**。)
- EEG 原文件是**每被试 41.7 GB**(16540×80×63×250 fp16)⇒ **不能**整份读进内存
⇒ `torch.load(mmap=True)` + **逐试次切片**,峰值内存只有 1 个试次(~0.5 GB)。
- **必须能分片并行**(平台同时只跑 2 个 Job)⇒ `--shard i --n-shards N`:
- `--step eeg`:按**被试**分片(每个被试的读盘量最大 ⇒ 按被试切最均衡);
- `--step vae`:按**图像行号**分片(VAE 编码成本均匀 ⇒ 按行号切最均衡)。
## 行号约定(**一条事实只写一处**)
VAE 潜变量的**行号 = EEG 的行号 = train.pt 的行号 = 「概念升序 × 每概念 10 张」**。
行 ↔ 照片的对应**以 `train.pt` 的 `img` 字段为准**(它直接编码了这个配对),
文件系统只用来**定位文件**。
## 用法
```powershell
# 云端(GPU;eeg 与 vae 各分 2 片 ⇒ 4 个 Job,同时最多 2 个)
python -u /domains/scripts/exp102_cache.py --step eeg --shard 0 --n-shards 2 \
--out-dir /domains/runs/illusionbrain-align/exp102
python -u /domains/scripts/exp102_cache.py --step vae --shard 0 --n-shards 2 \
--out-dir /domains/runs/illusionbrain-align/exp102
```
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from pathlib import Path
import numpy as np
for _v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"):
os.environ.setdefault(_v, "1")
EXP_DIR = Path(__file__).resolve().parent
if str(EXP_DIR) not in sys.path:
sys.path.insert(0, str(EXP_DIR))
TRAIN_PT = Path("/training/eeg/things-eeg/Preprocessed_data_250Hz_whiten")
IMAGE_SET = Path("/training/eeg/things-eeg/Image_set")
ENCODER_CKPT = "/domains/runs/illusionbrain-align/exp93b/encoder_best.pt"
DEFAULT_OUT = Path("/domains/runs/illusionbrain-align/exp102")
TRAIN_SUBJECTS = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
def log(*a):
print(*a, flush=True)
# ---------------------------------------------------------------------------
# 读取 train.pt(**mmap**:41.7 GB 的文件不能整份读进内存)
# ---------------------------------------------------------------------------
def load_train_pt(subject):
import torch
fp = TRAIN_PT / f"sub-{subject:02d}" / "train.pt"
if not fp.exists():
raise FileNotFoundError(f"缺 {fp}")
try:
return torch.load(fp, map_location="cpu", mmap=True, weights_only=False)
except TypeError: # 老版 torch 没有 mmap 参数
return torch.load(fp, map_location="cpu", weights_only=False)
def _as_2d_strings(cand, n_img):
"""把 train.pt 的 'img' / 'label' 字段统一成 (n_img, k) 的字符串/整数数组。"""
import torch
if cand is None:
return None
if isinstance(cand, torch.Tensor):
a = cand.numpy()
else:
a = np.asarray(cand)
return a
def img_paths_of(obj, n_img):
"""train.pt 的 'img' 字段 → 每行一条图像相对路径(**列 0**;兼容 (n,k)/(n,) 两种排法)。"""
a = _as_2d_strings(obj.get("img") if isinstance(obj, dict) else None, n_img)
if a is None:
return None
if a.ndim >= 2:
a = a[:, 0]
flat = [str(x) for x in np.asarray(a).reshape(-1)]
if len(flat) < n_img:
return None
return flat[:n_img]
def concept_of_path(p):
"""'train_images/00001_aardvark/aardvark_01b.jpg' → 0(概念号减一)。"""
try:
return int(p.split("/")[1][:5]) - 1
except Exception:
return -1
def resolve_image_index():
"""**自发现** Image_set 里的真实文件 ⇒ (概念目录, 文件名) → 绝对路径。
⚠️⚠️ **为什么要自发现(这是一个真坑,2026-10-27)**:
`train.pt` 的 `img` 字段写的是 **`train_images/...`**,而 `Image_set` 下真实的 split 目录
是 **`training_images/`**(探针实测顶层 = `.DS_Store` / `test_images` / `training_images`)
⇒ ⇒ 直接拼路径 ⇒ **100% 读不到** ⇒ 而旧版代码**静默用灰占位图顶替** ⇒
⇒ ⇒ **潜变量全是灰图的** ⇒ 训练看着「正常跑完」,其实全废
⇒ ⇒ ⇒ **两条硬规矩**:① 路径**不信任**上游字符串 ⇒ 自己 rglob 建索引;
② **缺图即报错**(不许占位图顶替)⇒ 让问题当场炸,而不是变成一场静默的假实验。
⇒ 索引键 = (概念目录名, 文件名) ⇒ 对前缀差异免疫。
"""
idx = {}
by_dir = {}
for p in IMAGE_SET.rglob("*.jpg"):
idx[(p.parent.name, p.name)] = p
by_dir.setdefault(p.parent.name, []).append(p)
for p in IMAGE_SET.rglob("*.png"):
idx.setdefault((p.parent.name, p.name), p)
by_dir.setdefault(p.parent.name, []).append(p)
return idx, by_dir
def resolve_paths(paths, idx):
"""把 train.pt 的相对路径解析成**真实绝对路径**;解析不到 ⇒ 直接抛(不许占位)。"""
out = []
missing = []
for rel in paths:
parts = str(rel).split("/")
cand = idx.get((parts[-2], parts[-1]))
if cand is None:
# 退路:只按文件名找(同一张图在别处)
hits = [v for k, v in idx.items() if k[1] == parts[-1]]
cand = hits[0] if len(hits) == 1 else None
if cand is None:
missing.append(str(rel))
else:
out.append(cand)
if missing:
raise RuntimeError(
f"**{len(missing)}/{len(paths)} 张图解析不到真实文件**"
f"(例:{missing[:3]})⇒ 拒绝用占位图顶替"
f"(旧版就是在这里静默把 8270 张全变成灰图 ⇒ 训练全废)")
return out
def master_paths(subjects=TRAIN_SUBJECTS):
"""→ (paths list, source str):**行号 = EEG 行号**的图像路径表。"""
for sid in subjects:
obj = load_train_pt(sid)
n_img = int(obj["eeg"].shape[0])
paths = img_paths_of(obj, n_img)
if paths:
return paths, f"train.pt sub-{sid:02d}.img"
# 兜底:文件系统(⚠️ 此时**行内**的照片顺序未必与 EEG 行序一致 ⇒ 只算兜底)
jpgs = sorted(p for p in IMAGE_SET.rglob("*.jpg") if "train" in str(p))
rel = [str(p.relative_to(IMAGE_SET)) for p in jpgs]
return rel, "文件系统(兜底:行内顺序未验证)"
def pick_reps(n_rep, k):
"""从 n_rep 个试次里**均匀取 k 个**(确定性 ⇒ 分片可复现)。"""
k = max(1, min(int(k), int(n_rep)))
if k == 1:
return [0]
return sorted(set(int(x) for x in np.linspace(0, n_rep - 1, k).round()))
def _shard_of(items, shard, n_shards):
"""按 round-robin 分片(负载均衡 ⇒ 每个分片的总量差不多)。"""
return [it for i, it in enumerate(items) if i % int(n_shards) == int(shard)]
# ---------------------------------------------------------------------------
# 步骤 ①:EEG 条件
# ---------------------------------------------------------------------------
def eeg_step(args):
import torch
import exp93b_contrastive as E93B
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
out_dir = Path(args.out_dir)
cache_dir = out_dir / "cache"
cache_dir.mkdir(parents=True, exist_ok=True)
ck = torch.load(args.encoder_ckpt, map_location="cpu", weights_only=False)
model = E93B.build_model(n_ch=ck["n_ch"], n_times=ck["n_times"], out_dim=ck["out_dim"])
model.load_state_dict(ck["state_dict"])
model = model.to(device).eval()
log(f"[编码器] exp93b encoder_best.pt(epoch={ck.get('epoch')},**冻结**)")
subjects = _shard_of(list(args.subjects), args.shard, args.n_shards)
log(f"[分片] shard {args.shard}/{args.n_shards} ⇒ 被试 {subjects}")
t0 = time.time()
for sid in subjects:
fp = cache_dir / f"eeg_cond_sub{sid:02d}.pt"
if fp.exists() and not args.overwrite:
log(f" ✓ sub-{sid:02d} 已有缓存 ⇒ 跳过")
continue
obj = load_train_pt(sid)
eeg = obj["eeg"]
n_img, n_rep = int(eeg.shape[0]), int(eeg.shape[1])
reps = pick_reps(n_rep, args.n_reps)
conds = []
for r in reps:
rows = np.asarray(eeg[:, r]) # (n_img, 63, 250) fp16
z = E93B.encode_all(model, rows, device, args.batch).astype(np.float16)
conds.append(z)
del rows
arr = np.stack(conds, axis=1) # (n_img, n_reps, 1024)
torch.save({"cond": torch.from_numpy(arr), "reps": reps, "subject": sid,
"n_img": n_img, "n_rep_total": n_rep,
"encoder": "exp93b encoder_best.pt(冻结)"},
fp)
log(f" ✓ sub-{sid:02d}: {n_img} 图 × {len(reps)} 试次 {reps} "
f"⇒ {tuple(arr.shape)}({time.time()-t0:.0f}s)")
return 0
# ---------------------------------------------------------------------------
# 步骤 ②:VAE 潜变量
# ---------------------------------------------------------------------------
def vae_step(args):
import torch
from PIL import Image
import exp102_train as E102T
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
out_dir = Path(args.out_dir)
cache_dir = out_dir / "cache"
cache_dir.mkdir(parents=True, exist_ok=True)
paths, source = master_paths(tuple(args.subjects))
n_img = len(paths)
log(f"[路径] {n_img} 张训练图(来源:{source})")
# 行内顺序自检:概念号必须是 i//10(与 exp93b 的 P1 预检同一条)
cons = np.array([concept_of_path(p) for p in paths])
ok_block = bool(np.array_equal(cons, np.arange(n_img) // 10))
log(f" 行序自检:概念号 == i//10 ⇒ {ok_block}"
f"(否则说明路径表与 EEG 行序对不上 ⇒ 停)")
if not ok_block:
raise RuntimeError("图像路径表与 EEG 行序对不上(概念块顺序不对)⇒ 不产出缓存")
lo = n_img * args.shard // args.n_shards
hi = n_img * (args.shard + 1) // args.n_shards
sel = list(range(lo, hi))
log(f"[分片] shard {args.shard}/{args.n_shards} ⇒ 行 [{lo}, {hi})({len(sel)} 张)")
vae = E102T.load_sd_component("vae", dtype=torch.float16, device=device).eval()
try:
vae.enable_slicing() # 512×512 的 VAE encoder 很吃显存 ⇒ 切片更稳
except Exception: # noqa: BLE001
pass
scale = float(vae.config.scaling_factor)
log(f"[VAE] SD1.5 自带 VAE|scaling_factor={scale}|{args.res}×{args.res}"
f"|batch={args.vae_batch}")
# ---- **自发现**真实文件路径(⚠️ train.pt 的 'train_images/' 前缀 ≠ 真实的 'training_images/')----
idx, _by_dir = resolve_image_index()
log(f"[路径] 自发现索引:{len(idx)} 个图片文件")
abs_paths = resolve_paths(paths, idx) # 解析不到 ⇒ 直接抛(**不许占位图**)
log(f"[路径] ✓ 全部 {len(abs_paths)} 张都解析到真实文件(缺一张就停)")
lat_all = []
idx_all = []
miss = 0
t0 = time.time()
# ---- 分段落盘(⚠️ 2026-10-27:平台会中途取消长 Job ⇒ 必须**中间落盘 + 可续跑**)----
cache_dir = out_dir / "cache"
part_files = sorted(cache_dir.glob(f"vae_latents_{args.shard:02d}_p*.pt"))
done_idx = set()
for pf in part_files:
try:
obj = torch.load(pf, map_location="cpu", weights_only=False)
done_idx.update(int(i) for i in obj["idx"])
log(f" [续跑] 已有分片 {pf.name}:{len(obj['idx'])} 行")
except Exception as e: # noqa: BLE001
log(f" [续跑] 分片 {pf.name} 读不出({type(e).__name__})⇒ 忽略")
if done_idx:
log(f" [续跑] 已完成 {len(done_idx)} 张 ⇒ 跳过它们")
def flush_part(idx_chunk, lat_chunk, start_idx):
fp = cache_dir / f"vae_latents_{args.shard:02d}_p{int(start_idx):06d}.pt"
torch.save({"idx": list(idx_chunk), "latents": torch.cat(lat_chunk, 0),
"paths": [str(abs_paths[i]) for i in idx_chunk],
"resolution": args.res, "scaling_factor": scale}, fp)
log(f" ✓ 写分片 {fp.name}:{len(idx_chunk)} 行")
part_k = len(part_files)
for b in range(0, len(sel), args.vae_batch):
chunk = [i for i in sel[b:b + args.vae_batch] if i not in done_idx]
if not chunk:
continue
imgs = []
for i in chunk:
p = abs_paths[i]
try:
im = Image.open(p).convert("RGB")
except Exception as e: # noqa: BLE001
# ⚠️ **不许**用灰占位图顶替:旧版就是在这里静默把 8270 张全变成灰图
raise RuntimeError(f"读图失败 {p}: {type(e).__name__}: {e}")
im = im.resize((args.res, args.res), Image.BICUBIC)
arr = np.asarray(im, dtype=np.float32) / 127.5 - 1.0
imgs.append(torch.from_numpy(arr).permute(2, 0, 1))
x = torch.stack(imgs).to(device=device, dtype=torch.float16)
with torch.no_grad():
# ⚠️ diffusers 0.42 里 `DiagonalGaussianDistribution.mode` 是**方法**
# (早期版本是属性)⇒ 写成「能调就调」⇒ 跨版本都稳。
# 用 mode(均值)而不是 sample ⇒ **潜变量是确定的** ⇒ 同一张图永远同一个 latent。
dist = vae.encode(x).latent_dist
mode = dist.mode() if callable(dist.mode) else dist.mode
lat = mode * scale
lat_all.append(lat.cpu().half())
idx_all.append(chunk)
n_done = sum(len(c) for c in idx_all)
if n_done % args.flush_every < args.vae_batch:
# 按**起始行号**命名分片 ⇒ 续跑时不会与旧分片重名 ⇒ 不会产生重复行
flush_part([i for c in idx_all for i in c], lat_all, idx_all[0][0])
lat_all, idx_all = [], []
part_k += 1
if (b // args.vae_batch) % 20 == 0:
log(f" {n_done}/{len(sel)}|{time.time()-t0:.0f}s")
if idx_all:
flush_part([i for c in idx_all for i in c], lat_all, idx_all[0][0])
log(f" ✓ 本分片完成|缺图 {miss} 张|{time.time()-t0:.0f}s")
return 0
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--step", choices=["eeg", "vae"], required=True)
ap.add_argument("--shard", type=int, default=0)
ap.add_argument("--n-shards", type=int, default=1)
ap.add_argument("--subjects", type=int, nargs="+", default=list(TRAIN_SUBJECTS))
ap.add_argument("--n-reps", type=int, default=4,
help="每个被试每个概念缓存几个试次(§四十四·五:全部可用 rep;"
"受读盘时间限制,取均匀的 k 个)")
ap.add_argument("--batch", type=int, default=256)
ap.add_argument("--vae-batch", type=int, default=4,
help="VAE 编码的批大小(512×512 很吃显存 ⇒ 别用 --batch 那个大值)")
ap.add_argument("--flush-every", type=int, default=1500,
help="每编码多少张就落一个分片(平台会中途取消 Job ⇒ 必须中间落盘)")
ap.add_argument("--res", type=int, default=512)
ap.add_argument("--encoder-ckpt", default=ENCODER_CKPT)
ap.add_argument("--out-dir", default=str(DEFAULT_OUT))
ap.add_argument("--overwrite", action="store_true")
args = ap.parse_args()
if args.step == "eeg":
return eeg_step(args)
return vae_step(args)
if __name__ == "__main__":
sys.exit(main())

Xet Storage Details

Size:
16.9 kB
·
Xet hash:
446de4e47e802e55931fe6a2157b16dbb284a41e32b6fd005577d380cd27b991

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.