"""E 轨改进项 #1 的验收测试:RNA token 压缩模块。
验六件事(对应任务书的 1–6):
1. 形状 L ∈ {81, 731, 1023} × n_latent ∈ {32, 64, 128} × 3 method
2. mask 正确性 padding 长度改(800 vs 1000)或 padding 内容改成随机垃圾值,
**有效部分**的输出必须不变;并给一个"mask 失效"的负对照证明测试有区分力
3. 梯度 backward 后压缩器参数拿到非零梯度(逐参数组列出,零梯度组要解释)
4. 可训参数量 三种 method 的参数 + 投影 2,625,536,对比 compress_latent=None
5. 等价性 compress_latent=None 时 build_batch 的 inputs/attn/labels 与**改动前**
逐位一致(`torch.equal`,atol=0)。靠 `--phase golden` 在改动前抓基准快照
6. 加速比 L=731、答案 368 token、n_latent=64 时序列总长的变化
跑法(两段式):
# ① 改动 alignment.py **之前**
python scripts/_test_compression.py --phase golden
# ② 改动之后
python scripts/_test_compression.py --phase all
# 只跑前 5 项、不比对 golden:
python scripts/_test_compression.py --phase local
为什么 golden 用 stub LLM
-------------------------
Qwen3-8B 有 16GB,本机另一个任务正在跑真实 Stage 1 计时,不能再加载一份。
所以第 5 项用一个**注入式 stub LLM**:patch 掉 `transformers.AutoModelForCausalLM`,
让 `Stage1Aligner.__init__` 与 `build_batch` **原封不动地跑真实代码路径**
(真 RNA-FM 编码器 + 真 Qwen tokenizer + 真投影/压缩拼接),只有 embedding 取值
换成确定性伪随机函数。改动前抓一次快照、改动后重算,逐位比对 —— 这验的正是
"compress_latent=None 这条路上有没有被动过",与 8B 权重无关。
真实的 8B 全路径由 `scripts/21_smoke_stage1.py` 单独把关。
⚠️ 本脚本**只用 CPU**(另一个任务在独占 GPU)。
"""
from __future__ import annotations
import argparse
import hashlib
import json
import statistics
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
import rnaqwen # noqa: E402,F401
import torch # noqa: E402
import torch.nn as nn # noqa: E402
from rnaqwen.data.records import read_jsonl # noqa: E402
from rnaqwen.models.compression import METHODS, RnaTokenCompressor # noqa: E402
IN_DIM = 640
LLM_DIM = 4096
N_HEAD_STEPS = 3 # 每个 method 的重复计时次数
# --------------------------------------------------------------------------- #
# 工具
# --------------------------------------------------------------------------- #
def sha256_file(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def sha256_tensor(t: torch.Tensor) -> str:
t = t.detach().cpu().contiguous()
if t.dtype == torch.bfloat16: # numpy 不认 bf16,按原始字节 hash
t = t.view(torch.uint8)
return hashlib.sha256(t.numpy().tobytes()).hexdigest()
class _StubEmbedding(nn.Module):
"""确定性 id -> 向量。不用参数、不吃内存,只保证"同一个 id 永远同一个向量"。"""
def __init__(self, dim: int) -> None:
super().__init__()
self.dim = dim
self.register_buffer("freq", torch.arange(dim, dtype=torch.float32), persistent=False)
def forward(self, ids: torch.Tensor) -> torch.Tensor:
x = (ids.float().unsqueeze(-1) + 1.0) * 0.017 * self.freq
return torch.sin(x)
class _StubLM(nn.Module):
"""替身 LLM:只提供 Stage1Aligner 用到的那几个接口。"""
def __init__(self, dim: int = LLM_DIM) -> None:
super().__init__()
self.config = type("Cfg", (), {"hidden_size": dim, "use_cache": True})()
self.emb = _StubEmbedding(dim)
self.dummy = nn.Parameter(torch.zeros(1), requires_grad=True)
def get_input_embeddings(self): # noqa: ANN201
return self.emb
def gradient_checkpointing_enable(self) -> None: # pragma: no cover
pass
def forward(self, *a, **kw): # pragma: no cover - 本脚本不做 LLM 前向
raise RuntimeError("stub LLM 不做前向;第 5 项只验 build_batch 的拼接")
def build_stub_aligner(cfg): # noqa: ANN001, ANN201
"""用真 Stage1Aligner.__init__ + 真 RNA-FM + 真 tokenizer,只把 8B 换成 stub。"""
import transformers
from rnaqwen.models.alignment import Stage1Aligner
real_cls = transformers.AutoModelForCausalLM
class _Patched(real_cls): # type: ignore[misc, valid-type]
@classmethod
def from_pretrained(cls, *a, **kw): # noqa: ANN002, ANN003, ANN206
return _StubLM()
transformers.AutoModelForCausalLM = _Patched
try:
return Stage1Aligner(cfg)
finally:
transformers.AutoModelForCausalLM = real_cls
def load_samples(data: Path, n: int = 2) -> list[dict]:
rows = [r.as_dict() for r in read_jsonl(data)]
out = []
for r in rows:
t = (r.get("annotation") or "").strip()
if r.get("sequence") and t:
out.append({"sequence": r["sequence"], "target": t})
if len(out) >= n:
break
if not out:
raise SystemExit(f"[fail] {data} 里没有可用的 sequence+annotation")
return out
# --------------------------------------------------------------------------- #
# 1. 形状
# --------------------------------------------------------------------------- #
def test_shapes() -> dict:
res: dict = {"cases": [], "ok": True}
for method in METHODS:
for n_latent in (32, 64, 128):
comp = RnaTokenCompressor(IN_DIM, n_latent, method)
for L in (81, 731, 1023):
h = torch.randn(2, L, IN_DIM)
m = torch.ones(2, L)
m[1, max(L - 120, 2):] = 0
out, om = comp(h, m)
ok = (tuple(out.shape) == (2, n_latent, IN_DIM)
and tuple(om.shape) == (2, n_latent)
and float(om.sum()) == 2.0 * n_latent
and bool(torch.isfinite(out).all()))
res["cases"].append({"method": method, "L": L, "n_latent": n_latent,
"out_shape": list(out.shape), "mask_shape": list(om.shape),
"mask_all_ones": float(om.mean()) == 1.0, "ok": ok})
res["ok"] &= ok
# 小 L(有效长度 < n_latent)也要不炸
for method in METHODS:
comp = RnaTokenCompressor(IN_DIM, 128, method)
h = torch.randn(1, 40, IN_DIM)
m = torch.ones(1, 40)
m[0, 20:] = 0
out, _ = comp(h, m)
ok = tuple(out.shape) == (1, 128, IN_DIM) and bool(torch.isfinite(out).all())
res["cases"].append({"method": method, "L": 40, "valid": 20, "n_latent": 128,
"out_shape": list(out.shape), "ok": ok, "note": "L_valid < n_latent"})
res["ok"] &= ok
return res
# --------------------------------------------------------------------------- #
# 2. mask 正确性
# --------------------------------------------------------------------------- #
def _pad_to(v_src: torch.Tensor, L: int, gen: torch.Generator | None = None
) -> tuple[torch.Tensor, torch.Tensor]:
"""把 (valid_len, D) 的有效内容放进长度 L 的右填充张量,padding 区填**随机垃圾值**
(模拟 RNA-FM:padding 位置有值、而且不是 0)。"""
h = torch.randn(L, IN_DIM, generator=gen) * 10.0
m = torch.zeros(L)
n = v_src.shape[0]
h[:n] = v_src
m[:n] = 1.0
return h, m
def test_mask(seed: int = 1234) -> dict:
torch.manual_seed(seed)
res: dict = {"cases": [], "ok": True}
for method in METHODS:
comp = RnaTokenCompressor(IN_DIM, 64, method, trim=True)
comp_no_trim = RnaTokenCompressor(IN_DIM, 64, method, trim=False)
gen = torch.Generator().manual_seed(7)
# 有效内容一致、padding 区是**随机垃圾值**(模拟 RNA-FM:padding 位置有值)
valid = [torch.randn(731, IN_DIM, generator=gen), torch.randn(631, IN_DIM, generator=gen)]
def run(c, Ls):
outs = []
for v, L in zip(valid, Ls):
h, m = _pad_to(v, L, gen=gen)
o, _ = c(h.unsqueeze(0), m.unsqueeze(0))
outs.append(o)
return torch.cat(outs, 0)
# (a) padding 800 vs 1000
o800 = run(comp, [800, 800])
o1000 = run(comp, [1000, 1000])
d_trim = float((o800 - o1000).detach().abs().max())
eq_trim = bool(torch.equal(o800, o1000))
a = run(comp_no_trim, [800, 800])
b = run(comp_no_trim, [1000, 1000])
d_notrim = float((a - b).abs().max())
# (b) padding **内容**换成完全不同的随机值(长度不变)
gen2 = torch.Generator().manual_seed(99)
h1, m1 = _pad_to(valid[0], 1000, gen=gen)
h2, _ = _pad_to(valid[0], 1000, gen=gen2)
d_content = float((comp(h1.unsqueeze(0), m1.unsqueeze(0))[0]
- comp(h2.unsqueeze(0), m1.unsqueeze(0))[0]).abs().max())
# (c) 负对照:mask 失效(全 1,等于把 padding 当真实信号)
o_nomask, _ = comp(h1.unsqueeze(0), torch.ones(1, 1000))
d_nomask = float((comp(h1.unsqueeze(0), m1.unsqueeze(0))[0] - o_nomask).abs().max())
# (d) 有效长度本身不同(batchnorm 无关)——不同有效长度当然不同,这里只保证有限
finite = bool(torch.isfinite(o1000).all())
case = {
"method": method,
"dtype": "float32",
"pad_len_800_vs_1000_bitwise_equal(trim=True)": eq_trim,
"max_abs_diff(trim=True)": d_trim,
"max_abs_diff(trim=False)": d_notrim,
"max_abs_diff(padding_content_changed)": d_content,
"max_abs_diff(mask_ignored_negative_control)": d_nomask,
"all_finite": finite,
}
case["ok"] = (eq_trim and d_content == 0.0 and d_nomask > 1e-6 and finite)
res["cases"].append(case)
res["ok"] &= case["ok"]
return res
def test_mask_real_rnafm(args, samples: list[dict], n_latent: int = 64) -> dict:
"""用**真实 RNA-FM 输出**再验一次 mask:padding 位置的取值是真的非零值(这正是坑本身)。
手法:把同一条序列分别放进两个不同 batch 里编码(batch 最大长度不同 => padding 长度不同),
取该序列的输出做比对。RNA-FM 不做跨样本卷积,有效位置的表征与 batch 无关。
"""
from rnaqwen.models.encoder import RnaFmEncoder
enc = RnaFmEncoder(args.encoder, dtype=torch.float32)
seq = samples[0]["sequence"]
o_solo = enc([seq])
others = [s["sequence"] for s in samples[1:]]
o_batch = enc([seq] + others)
if o_batch.hidden.shape[1] <= o_solo.hidden.shape[1]:
# batch 里没有更长的序列 => padding 长度没变,人为加一条长的逼出 padding
o_batch = enc([seq, seq + seq[:200]])
L_solo, L_batch = int(o_solo.hidden.shape[1]), int(o_batch.hidden.shape[1])
assert L_batch > L_solo, "构造失败:两个 batch 的 padding 长度相同,这一项就没意义了"
pad_vals = o_batch.hidden[0, o_solo.lengths[0]:]
res = {
"seq_len_nt": len(seq), "L_solo": L_solo, "L_batch": L_batch,
"padding_positions": int(L_batch - L_solo),
"padding_max_abs_value": float(pad_vals.abs().max()) if pad_vals.numel() else None,
"padding_is_zero": bool(pad_vals.numel() and float(pad_vals.abs().max()) == 0.0),
"cases": [], "ok": True,
}
for method in METHODS:
comp = RnaTokenCompressor(IN_DIM, n_latent, method, trim=True)
a, _ = comp(o_solo.hidden, o_solo.mask) # (1, n_latent, 640)
b, _ = comp(o_batch.hidden, o_batch.mask) # (2, n_latent, 640)
equal = bool(torch.equal(a, b[:1]))
md = float((a - b[:1]).detach().abs().max())
case = {"method": method, "dtype": "float32", "bitwise_equal": equal, "max_abs_diff": md,
"ok": equal or md < 1e-5,
"note": ("mask 的贡献精确为 0(padding 价值 max|·| ~6.5 却完全不影响结果);"
"残差只来自 matmul 归约顺序随列数变化,fp32 下 ~1e-7,bf16 下为 0")}
res["cases"].append(case)
res["ok"] &= bool(case["ok"]) and bool(torch.isfinite(b).all())
return res
# --------------------------------------------------------------------------- #
# 3. 梯度
# --------------------------------------------------------------------------- #
def test_gradients() -> dict:
res: dict = {"cases": [], "ok": True}
for method in METHODS:
torch.manual_seed(0)
comp = RnaTokenCompressor(IN_DIM, 64, method)
h = torch.randn(1, 731, IN_DIM, requires_grad=True)
m = torch.ones(1, 731)
m[0, 700:] = 0
out, _ = comp(h, m)
# 模拟下游:一个"每个 latent 位置权重不同"的 loss(比 out.pow(2).mean() 更接近真实,
# 真实的梯度来自冻结 LLM 的 causal LM loss,这里不加载 8B)
w = torch.linspace(0.1, 1.0, 64).view(1, 64, 1)
comp.zero_grad(set_to_none=True)
(out * w).pow(2).mean().backward()
named = [(n, p) for n, p in comp.named_parameters()]
zero = [n for n, p in named if p.grad is None or float(p.grad.abs().sum()) == 0.0]
gs = [p.grad.float().pow(2).sum() for _, p in named if p.grad is not None]
grad_norm = float(torch.sqrt(torch.stack(gs).sum())) if gs else 0.0
in_grad = float(h.grad.detach().norm())
case = {
"method": method,
"n_param_tensors": len(named),
"n_zero_grad_tensors": len(zero),
"zero_grad_names": zero,
"total_grad_norm": grad_norm,
"input_grad_norm": in_grad,
}
# "有非零梯度"的判据:至少有一组参数拿到非零梯度 + 输入梯度非零
case["ok"] = (len(named) - len(zero) > 0 or len(named) == 0) and in_grad > 0
if method == "resampler" and zero:
case["note"] = ("to_q 零初始化 => 注意力恰好均匀 => ln_q/to_k 的梯度在 step 0 必然为 0;"
"to_q 自己拿到非零梯度,第 1 步之后整栈就醒了")
if method == "meanpool":
case["note"] = "meanpool 无参数,只能验『输入梯度非零』"
res["cases"].append(case)
res["ok"] &= case["ok"]
return res
# --------------------------------------------------------------------------- #
# 3b. 初始化行为(为什么 resampler 敢在 step 0 就上)
# --------------------------------------------------------------------------- #
def test_init_behavior() -> dict:
"""量一下"latent 之间有多像"。
`zero_init_q=True` 时 to_q=0 => attention 均匀 => 每个 latent 拿到的是**同一份**
有效位置均值,只是各自加了不同的 query 残差。所以 step 0 的 resampler 落在
"池化"这一行为类里,冻结的 LLM 一上来看到的就是真实 RNA 信号,而不是随机 query
招出的噪声。代价:ln_q / to_k 在 step 0 梯度恰好为 0(见第 3 项),to_q 拿到梯度后
第 1 步就把整栈叫醒。
"""
torch.manual_seed(0)
h = torch.randn(1, 731, IN_DIM)
m = torch.ones(1, 731)
m[0, 700:] = 0
res: dict = {"cases": []}
for tag, kw in {"resampler(zero_init_q=True)": {"zero_init_q": True},
"resampler(zero_init_q=False)": {"zero_init_q": False},
"meanpool": {}}.items():
method = "meanpool" if tag == "meanpool" else "resampler"
comp = RnaTokenCompressor(IN_DIM, 64, method, **kw)
out, _ = comp(h, m)
v = out[0].float() # (n_latent, D)
v = v / v.norm(dim=-1, keepdim=True).clamp(min=1e-9)
sim = v @ v.t()
n = sim.shape[0]
off = (sim.sum() - sim.diag().sum()) / (n * (n - 1))
res["cases"].append({
"module": tag,
"mean_pairwise_cosine_between_latents": round(float(off), 6),
"latents_nearly_identical": bool(float(off) > 0.9),
})
return res
# --------------------------------------------------------------------------- #
# 4. 可训参数量
# --------------------------------------------------------------------------- #
def test_params() -> dict:
proj = IN_DIM * LLM_DIM + LLM_DIM
res: dict = {
"projection_only(compress_latent=None)": proj,
"methods": {},
"variants": {},
}
for method in METHODS:
comp = RnaTokenCompressor(IN_DIM, 64, method)
res["methods"][method] = {
"compressor_params": comp.num_parameters,
"breakdown": comp.param_breakdown(),
"total_trainable": proj + comp.num_parameters,
"ratio_vs_projection": round((proj + comp.num_parameters) / proj, 4),
}
# resampler 的容量旋钮(给"要不要更大"留证据)
for tag, kw in {
"d256_l1": {"d_model": 256, "n_layers": 1},
"d256_l2": {"d_model": 256, "n_layers": 2},
"d256_l4": {"d_model": 256, "n_layers": 4},
"d640_l2": {"d_model": 640, "n_layers": 2},
}.items():
c = RnaTokenCompressor(IN_DIM, 64, "resampler", **kw)
res["variants"][tag] = {"params": c.num_parameters, "total_trainable": proj + c.num_parameters,
"ratio_vs_projection": round((proj + c.num_parameters) / proj, 4)}
for kwargs in ({"n_latent": 32}, {"n_latent": 128}):
c = RnaTokenCompressor(IN_DIM, kwargs["n_latent"], "resampler")
res["variants"][f"n_latent{kwargs['n_latent']}"] = {"params": c.num_parameters,
"total_trainable": proj + c.num_parameters}
c = RnaTokenCompressor(IN_DIM, 64, "strided", conv_kernel=9)
res["variants"]["strided_k9"] = {"params": c.num_parameters, "total_trainable": proj + c.num_parameters}
return res
def test_e_track_path(cfg, samples: list[dict], n_latent: int = 64) -> dict:
"""E 轨路径真的接通没:`compress_latent=64` 时走**真 build_batch** 看拼接结果。
验:总长 = prefix + n_latent + suffix + 答案(RNA 那一段从 731 变成 64);
soft 块全 -100;答案 token 数不变(loss 该算的地方没被吃掉);
RNA 块 == projection(compressor(encoder));可训参数 = 投影 + 压缩器。
"""
from dataclasses import replace
res: dict = {"n_latent": n_latent, "cases": [], "ok": True}
base_cfg = getattr(cfg, "compress_latent", "n/a")
for method in METHODS:
c = replace(cfg, compress_latent=n_latent, compress_method=method)
torch.manual_seed(0)
al = build_stub_aligner(c)
_pin_projection(al)
device = torch.device("cpu")
assert al.compression is not None and al.compression.method == method
seqs = [s["sequence"] for s in samples]
tgts = [s["target"] for s in samples]
inputs, attn, labels = al.build_batch(seqs, tgts, device)
n_pre = int(al._encode_const(al.PREFIX_TEXT, device).shape[1])
n_suf = int(al._encode_const(al.SUFFIX_TEXT, device).shape[1])
n_ans_max = int((labels != -100).sum(1).max())
soft_len = int(attn.shape[1]) - n_ans_max
exp_len = n_pre + n_latent + n_suf + n_ans_max
# RNA 块 == projection(compressor(encoder))
enc = al.encoder(seqs)
lat, lat_mask = al.compression(enc.hidden.to(device), enc.mask.to(device))
proj_block = al.projection(lat)
block_ok = bool(torch.equal(proj_block.detach().cpu(),
inputs[:, n_pre:n_pre + n_latent].detach().cpu()))
trainable = sum(p.numel() for p in al.trainable_parameters())
proj_only = sum(p.numel() for p in al.projection.parameters())
case = {
"method": method,
"inputs_shape": list(inputs.shape),
"attn_len": int(attn.shape[1]),
"expected_len(prefix+latent+suffix+answer)": exp_len,
"soft_block_len(reported_by_21_smoke_style)": soft_len,
"soft_block_attn_all_ones": bool(float(attn[:, :n_pre + n_latent + n_suf].float().mean()) == 1.0),
"attn_all_ones(B=2 且有答案 padding 时会是 False,只记录)": bool(float(attn.float().mean()) == 1.0),
"soft_block_labels_all_-100": bool(int(labels[:, :n_pre + n_latent + n_suf].max()) == -100),
"answer_tokens": int((labels != -100).sum()),
"rna_block_is_projection(compressor(hidden))": block_ok,
"trainable_params": trainable,
"compressor_params": trainable - proj_only,
"compressor_mask_all_ones": bool(float(lat_mask.float().mean()) == 1.0),
}
case["ok"] = (case["attn_len"] == exp_len and case["soft_block_attn_all_ones"]
and case["soft_block_labels_all_-100"] and block_ok
and case["answer_tokens"] > 0 and case["compressor_mask_all_ones"])
res["cases"].append(case)
res["ok"] &= case["ok"]
res["note"] = (f"对照:compress_latent 默认值 = {base_cfg!r}(None 时整段不执行)")
return res
# --------------------------------------------------------------------------- #
# 5. 等价性:改动前的 build_batch(逐字复刻) vs 改动后
# --------------------------------------------------------------------------- #
BASELINE_SOURCE = ("alignment.py@9e358e22f7b7d1a6da40cee6d03b9266468b36d5fc20f11b920d39e01b3e0b94"
"(加 E 轨开关之前)")
ANCHOR_SHA = "9e358e22f7b7d1a6da40cee6d03b9266468b36d5fc20f11b920d39e01b3e0b94"
def baseline_build_batch(al, sequences: list[str], targets: list[str], device: torch.device) -> dict:
"""`alignment.py@9e358e22` 的 build_batch **逐字复刻**(别改这个函数)。
为什么要复刻,而不是只跟磁盘上的 golden 快照比:
投影层是**随机初始化**的(nn.Linear 默认 init 吃全局 RNG),跨进程的权重不可能相同,
所以"改动前的 inputs"没法从快照里复现。
同一进程内、同一个 aligner 实例上跑复刻版和现行版 => 权重一模一样 => 逐位可比。
复刻的忠实性由**原始 golden**(真的用改动前的代码跑出来的)反查:
它给出的 attn/labels 必须与本复刻逐位一致 —— 见 `replica_faithfulness`。
"""
enc = al.encoder(sequences) # (B, L, 640)
rna = enc.hidden.to(device)
rna_mask = enc.mask.to(device)
tok = al.tokenizer(
targets,
return_tensors="pt",
padding=True,
truncation=True,
max_length=al.cfg.max_text_tokens,
add_special_tokens=True,
)
text_ids = tok["input_ids"].to(device)
text_mask = tok["attention_mask"].to(device)
prefix_ids = al._encode_const(al.PREFIX_TEXT, device)
suffix_ids = al._encode_const(al.SUFFIX_TEXT, device)
proj = al.projection(rna) # (B, L, 640→4096)
prefix_emb = al.llm.get_input_embeddings()(prefix_ids)
suffix_emb = al.llm.get_input_embeddings()(suffix_ids)
text_emb = al.llm.get_input_embeddings()(text_ids)
inputs = torch.cat([
prefix_emb.expand(rna.shape[0], -1, -1),
proj,
suffix_emb.expand(rna.shape[0], -1, -1),
text_emb,
], dim=1)
n_prefix = prefix_emb.shape[1]
rna_labels = torch.full((text_ids.shape[0], proj.shape[1]), -100,
dtype=text_ids.dtype, device=device)
prefix_labels = torch.full((text_ids.shape[0], n_prefix), -100,
dtype=text_ids.dtype, device=device)
suffix_labels = torch.full((text_ids.shape[0], suffix_emb.shape[1]), -100,
dtype=text_ids.dtype, device=device)
ans_labels = torch.where(text_mask.bool(), text_ids, torch.full_like(text_ids, -100))
labels = torch.cat([prefix_labels, rna_labels, suffix_labels, ans_labels], dim=1)
prefix_mask = torch.ones((rna_mask.shape[0], n_prefix), dtype=rna_mask.dtype, device=device)
suffix_mask = torch.ones((rna_mask.shape[0], suffix_emb.shape[1]), dtype=rna_mask.dtype, device=device)
attn = torch.cat([prefix_mask, rna_mask, suffix_mask, text_mask], dim=1)
return {"inputs": inputs, "attn": attn, "labels": labels, "proj": proj,
"prefix_emb": prefix_emb, "suffix_emb": suffix_emb, "text_emb": text_emb}
def _batches(samples: list[dict]) -> dict[str, tuple[list[str], list[str]]]:
"""三条组装路径:两条 batch=1(真实训练就是 bs=1)+ 一条 B=2(走 padding)。"""
out = {f"b{i}": ([s["sequence"]], [s["target"]]) for i, s in enumerate(samples)}
out["b2"] = ([s["sequence"] for s in samples], [s["target"] for s in samples])
return out
def _pin_projection(al, seed: int = 0) -> None:
"""把投影层权重钉成确定值。
为什么必须钉:`nn.Linear` 默认初始化吃全局 RNG,而 RNA-FM 的加载路径
(缺 `pooler.dense`,会在 load 时随机初始化)本身也会吃 RNG —— 同一进程里
第几次加载、前面跑过什么,都会让投影层的初值不一样。于是"跨进程比 inputs"
会假报警。钉死之后,两次进程的投影层权重逐位相同,inputs 才真的可比。
(钉的是权重,不是行为;量级照 kaiming_uniform 的 1/sqrt(fan_in) 取。)
"""
bound = float(al.projection.in_dim) ** -0.5
gen = torch.Generator().manual_seed(seed)
with torch.no_grad():
for p in al.projection.parameters():
p.copy_(torch.empty(p.shape, dtype=torch.float32).uniform_(-bound, bound, generator=gen)
.to(p.dtype))
def _none_path_tensors(cfg, samples: list[dict], seeded: bool = True) -> dict[str, torch.Tensor]:
"""compress_latent=None 时用**现行** build_batch 产出张量。"""
if seeded:
torch.manual_seed(0)
al = build_stub_aligner(cfg)
_pin_projection(al)
assert al.compression is None, "compress_latent=None 时不该建出压缩器"
device = torch.device("cpu")
tensors: dict[str, torch.Tensor] = {}
for k, (seqs, tgts) in _batches(samples).items():
inputs, attn, labels = al.build_batch(seqs, tgts, device)
tensors[f"{k}_inputs"] = inputs.detach().cpu()
tensors[f"{k}_attn"] = attn.detach().cpu()
tensors[f"{k}_labels"] = labels.detach().cpu()
return tensors
def golden_from_replica(cfg, samples: list[dict]) -> dict:
"""用**改动前的代码语义**(复刻版)抓 golden,投影层钉死 => 跨进程可复现。"""
torch.manual_seed(0)
al = build_stub_aligner(cfg)
_pin_projection(al)
device = torch.device("cpu")
tensors: dict[str, torch.Tensor] = {}
for k, (seqs, tgts) in _batches(samples).items():
t = baseline_build_batch(al, seqs, tgts, device)
for name in ("inputs", "attn", "labels"):
tensors[f"{k}_{name}"] = t[name].detach().cpu()
return tensors
def _cmp(a: torch.Tensor, b: torch.Tensor) -> dict:
same_shape = tuple(a.shape) == tuple(b.shape)
return {
"shape_match": same_shape,
"bitwise_equal(torch.equal)": bool(torch.equal(a, b)),
"max_abs_diff": None if not same_shape else float((a.float() - b.float()).abs().max()),
}
def test_equivalence(cfg, samples: list[dict], golden_path: Path, prechange_path: Path) -> dict:
"""三件事:
A. **同进程**:现行 build_batch vs 逐字复刻的改动前 build_batch,同一份权重,逐位比。
(这是"有没有动到 None 那条路"的直接证据,atol=0)
B. **跨进程 seeded golden**:用复刻版 + seed 0 抓的快照,现行代码重算后逐位比。
C. **复刻忠实性**:用真的改动前代码(未设种子)抓的原始快照反查复刻版 ——
只比 attn/labels 这类与随机初始化无关的张量,以及 inputs 里与投影无关的三块。
"""
torch.manual_seed(0)
al = build_stub_aligner(cfg)
_pin_projection(al)
device = torch.device("cpu")
res: dict = {"baseline_source": BASELINE_SOURCE,
"compression_is_none_by_default": al.compression is None}
new: dict[str, dict] = {}
old: dict[str, dict] = {}
for k, (seqs, tgts) in _batches(samples).items():
inp, attn, lab = al.build_batch(seqs, tgts, device)
new[k] = {"inputs": inp.detach().cpu(), "attn": attn.detach().cpu(),
"labels": lab.detach().cpu()}
old[k] = {n: v.detach().cpu() for n, v in baseline_build_batch(al, seqs, tgts, device).items()}
# ---- A. 同进程逐位 ----
ok_a = True
cases_a = []
for k in sorted(new):
for name in ("inputs", "attn", "labels"):
c = _cmp(old[k][name], new[k][name])
c.update({"batch": k, "tensor": name, "shape": list(new[k][name].shape)})
ok_a &= c["shape_match"] and c["bitwise_equal(torch.equal)"]
cases_a.append(c)
res["A_in_process_replica_vs_current"] = {"ok": ok_a, "cases": cases_a}
# 顺带:现行代码里投影块的数值必须等于 projection(rna) —— 证明中间没被插东西
proj_ok = True
for k in sorted(new):
same = bool(torch.equal(old[k]["proj"], new[k]["inputs"][
:, old[k]["prefix_emb"].shape[1]:old[k]["prefix_emb"].shape[1] + old[k]["proj"].shape[1]]))
proj_ok &= same
res["A_proj_block_is_exactly_projection_rna"] = proj_ok
# ---- token 统计(给第 6 项用)----
token_stats = []
for i in range(len(samples)):
labels, inputs = new[f"b{i}"]["labels"], new[f"b{i}"]["inputs"]
n_ans = int((labels != -100).sum())
token_stats.append({"i": i, "total": int(inputs.shape[1]), "answer_tokens": n_ans,
"soft_block(prefix+rna+suffix)": int(inputs.shape[1]) - n_ans})
res["token_stats"] = token_stats
# ---- B. seeded golden(复刻版语义)----
if golden_path.exists():
ref = torch.load(golden_path, map_location="cpu")
ok_b, cases_b = True, []
for k in sorted(ref["tensors"]):
c = _cmp(ref["tensors"][k], new[k.rsplit("_", 1)[0]][k.rsplit("_", 1)[1]])
c.update({"tensor": k})
ok_b &= c["shape_match"] and c["bitwise_equal(torch.equal)"]
cases_b.append(c)
res["B_seeded_golden_vs_current"] = {
"ok": ok_b, "golden_file": str(golden_path),
"golden_baseline_source": ref.get("baseline_source"),
"golden_alignment_sha256_at_capture": ref.get("alignment_sha256_at_capture"),
"cases": cases_b}
else:
res["B_seeded_golden_vs_current"] = {"ok": None, "note": f"缺 {golden_path};先跑 --phase golden"}
# ---- C. 复刻忠实性:拿真的改动前代码抓的(未设种子)快照反查 ----
if prechange_path.exists():
pre = torch.load(prechange_path, map_location="cpu")
ok_c, cases_c, full_eq = True, [], {}
for k in ("b0", "b1", "b2"):
full_eq[k] = bool(torch.equal(pre["tensors"][f"{k}_inputs"], new[k]["inputs"]))
for name in ("attn", "labels"):
c = _cmp(pre["tensors"][f"{k}_{name}"], new[k][name])
c.update({"tensor": f"{k}_{name}"})
ok_c &= c["shape_match"] and c["bitwise_equal(torch.equal)"]
cases_c.append(c)
# inputs 分块看(前缀/投影/后缀/正文)。投影层是随机初始化的:
# 这一项经验上也能逐位对上(见 full_inputs_bitwise_equal),但它是种子相关的,
# 所以不拿它当 PASS/FAIL 的判据,只如实记录。
n_pre = old[k]["prefix_emb"].shape[1]
n_rna = old[k]["proj"].shape[1]
n_suf = old[k]["suffix_emb"].shape[1]
ref_in = pre["tensors"][f"{k}_inputs"]
blocks = {"prefix": (0, n_pre), "proj": (n_pre, n_pre + n_rna),
"suffix": (n_pre + n_rna, n_pre + n_rna + n_suf),
"text": (n_pre + n_rna + n_suf, ref_in.shape[1])}
for bname, (s, e) in blocks.items():
c = _cmp(ref_in[:, s:e], new[k]["inputs"][:, s:e])
c.update({"tensor": f"{k}_inputs[{bname}]", "batch": k,
"counted_in_verdict": bname != "proj"})
cases_c.append(c)
if bname != "proj":
ok_c &= c["shape_match"] and c["bitwise_equal(torch.equal)"]
res["C_replica_faithfulness_vs_original_prechange_golden"] = {
"ok": ok_c, "golden_file": str(prechange_path),
"golden_alignment_sha256": pre.get("alignment_sha256"),
"full_inputs_bitwise_equal_per_batch": full_eq,
"cases": cases_c}
else:
res["C_replica_faithfulness_vs_original_prechange_golden"] = {
"ok": None, "note": f"缺 {prechange_path}(原始快照,未设种子,只用来反查复刻版忠实性)"}
res["ok"] = ok_a and proj_ok and (res["B_seeded_golden_vs_current"]["ok"] is True)
res["replica_faithfulness_ok"] = res["C_replica_faithfulness_vs_original_prechange_golden"]["ok"]
return res
def measure_tokens(args, samples: list[dict]) -> dict:
"""真实口径:真 RNA-FM 编码器 + 真 Qwen tokenizer 数一遍 token。"""
from transformers import AutoTokenizer
from rnaqwen.models.encoder import RnaFmEncoder
enc = RnaFmEncoder(args.encoder, dtype=torch.float32)
out = enc([s["sequence"] for s in samples])
tok = AutoTokenizer.from_pretrained(args.llm)
n_pre = int(tok("Q: ", add_special_tokens=False, return_tensors="pt")["input_ids"].shape[1])
n_suf = int(tok("\nA: ", add_special_tokens=False,
return_tensors="pt")["input_ids"].shape[1])
return {"prefix_tokens": n_pre, "suffix_tokens": n_suf,
"encoder_hidden_shape": list(out.hidden.shape),
"encoder_valid_tokens_per_seq": [int(v) for v in out.mask.sum(-1)],
"note": "21_smoke 里的 n_rna_tokens = prefix + RNA软token + suffix"}
# --------------------------------------------------------------------------- #
# 6. 序列长度 / 加速比 + CPU 耗时
# --------------------------------------------------------------------------- #
def test_speedup(n_rna_tokens: int, n_ans_tokens: int, n_latent: int = 64) -> dict:
before = n_rna_tokens + n_ans_tokens
after = n_latent + n_ans_tokens
return {
"assumption": {"rna_tokens": n_rna_tokens, "answer_tokens": n_ans_tokens,
"n_latent": n_latent},
"total_before": before,
"total_after": after,
"ratio_after_over_before": round(after / before, 4),
"speedup_x": round(before / after, 3),
"rna_part_ratio_before": round(n_rna_tokens / before, 4),
}
@torch.no_grad()
def _forward_only(module, h, m): # noqa: ANN001, ANN202
return module(h, m)
def measure_time(cfg=None) -> dict: # noqa: ANN001
"""压缩器自身的 CPU 耗时(forward + backward),B=1、L=731、n_latent=64。"""
torch.manual_seed(0)
h0 = torch.randn(1, 731, IN_DIM)
m = torch.ones(1, 731)
out: dict = {"batch": 1, "L": 731, "n_latent": 64, "dtype": "float32",
"note": "另一个任务在占 GPU/CPU,绝对数字仅供参考;看相对关系",
"modules": {}}
for method in METHODS:
comp = RnaTokenCompressor(IN_DIM, 64, method)
for _ in range(1): # warmup
h = h0.clone().requires_grad_(True)
o, _ = comp(h, m)
o.pow(2).mean().backward()
ts = []
for _ in range(N_HEAD_STEPS):
t0 = time.perf_counter()
h = h0.clone().requires_grad_(True)
o, _ = comp(h, m)
o.pow(2).mean().backward()
ts.append(time.perf_counter() - t0)
out["modules"][method] = {"fwd+bwd_ms_median": round(statistics.median(ts) * 1e3, 3),
"runs_ms": [round(t * 1e3, 3) for t in ts]}
# 参照:投影层自己(640→4096)在同一 L 上的耗时
from rnaqwen.models.projection import LinearProjection
proj = LinearProjection(IN_DIM, LLM_DIM)
ts = []
for _ in range(N_HEAD_STEPS):
t0 = time.perf_counter()
h = h0.clone().requires_grad_(True)
proj(h).pow(2).mean().backward()
ts.append(time.perf_counter() - t0)
out["modules"]["projection_640to4096(参照)"] = {
"fwd+bwd_ms_median": round(statistics.median(ts) * 1e3, 3),
"runs_ms": [round(t * 1e3, 3) for t in ts]}
return out
# --------------------------------------------------------------------------- #
# main
# --------------------------------------------------------------------------- #
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--phase", choices=["golden", "all", "local"], default="all")
ap.add_argument("--data", type=Path, default=ROOT / "data" / "processed" / "annotations_50.jsonl")
ap.add_argument("--n-samples", type=int, default=2)
ap.add_argument("--out", type=Path, default=ROOT / "reports" / "token_compression_test.json")
ap.add_argument("--golden", type=Path, default=ROOT / "reports" / "_golden_none_path.pt")
ap.add_argument("--prechange-golden", type=Path,
default=ROOT / "reports" / "_golden_none_path_prechange_unseeded.pt",
help="真的用改动前代码抓的原始快照(未设种子),只用来反查复刻版忠实性")
ap.add_argument("--llm", default=str(ROOT / "models" / "Qwen3-8B"))
ap.add_argument("--encoder", default=str(ROOT / "models" / "rnafm"))
args = ap.parse_args()
from rnaqwen.models.alignment import Stage1Config
alignment_py = ROOT / "src" / "rnaqwen" / "models" / "alignment.py"
samples = load_samples(args.data, args.n_samples)
cfg = Stage1Config(llm_path=args.llm, encoder_path=args.encoder, grad_checkpointing=False)
print(f"[comp-test] phase={args.phase} {len(samples)} 条样本 "
f"alignment.py sha256={sha256_file(alignment_py)[:16]} cfg.dtype={cfg.dtype}")
report: dict = {
"phase": args.phase,
"device": "cpu",
"alignment_sha256": sha256_file(alignment_py),
"config": {"compress_latent": getattr(cfg, "compress_latent", ""),
"compress_method": getattr(cfg, "compress_method", ""),
"dtype": cfg.dtype},
"samples": [{"seq_len_nt": len(s["sequence"]), "target_len": len(s["target"])} for s in samples],
"methodology": {
"item5": ("三层证据:(A) 同进程内,把改动前 build_batch 的**逐字复刻**与现行实现"
"跑在同一个 aligner 实例上逐位比(同一个随机初始化的投影层 => atol=0 可比);"
"(B) 用复刻版 + seed=0 抓的 seeded golden,跨进程逐位比;"
"(C) 复刻的忠实性由**真的用改动前代码抓的**原始快照(未设种子)反查 —— "
"它的 attn/labels 以及 inputs 里与投影无关的三块必须与本复刻逐位一致。"
"8B 全路径由 scripts/21_smoke_stage1.py 单独把关"),
"gpu": "全程 CPU,未使用 GPU",
},
}
if args.phase == "golden":
t0 = time.time()
tensors = golden_from_replica(cfg, samples) # 改动前的代码语义(逐字复刻)+ 投影层钉死
args.golden.parent.mkdir(parents=True, exist_ok=True)
torch.save({"tensors": tensors, "baseline_source": BASELINE_SOURCE,
"alignment_sha256_at_capture": report["alignment_sha256"],
"projection_pinned": True, "anchor_alignment_sha256": ANCHOR_SHA,
"cfg_dtype": cfg.dtype, "samples": report["samples"]}, args.golden)
meta = {k: {"shape": list(v.shape), "sha256": sha256_tensor(v)[:16]} for k, v in tensors.items()}
report["golden"] = {"file": str(args.golden), "sha256": sha256_file(args.golden),
"baseline_source": BASELINE_SOURCE,
"tensors": meta, "seconds": round(time.time() - t0, 1),
"note": ("golden 内容是**改动前 build_batch 的逐字复刻**算出来的,"
"投影层权重用固定 generator 钉死 => 跨进程逐位可复现。"
"复刻的忠实性由 --prechange-golden(真的用改动前代码抓的那份)"
"反查,见第 5 项的 C。")}
for k, v in meta.items():
print(f"[golden] {k:14s} {v['shape']} sha256={v['sha256']}")
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"[golden] -> {args.golden}\n[golden] -> {args.out}")
return 0
print("\n[1] 形状 ...")
report["1_shapes"] = test_shapes()
print(f" ok={report['1_shapes']['ok']} ({len(report['1_shapes']['cases'])} 个 case)")
print("[2] mask 正确性 ...")
report["2_mask"] = test_mask()
for c in report["2_mask"]["cases"]:
print(f" {c['method']:9s} 800vs1000 trim(on) diff={c['max_abs_diff(trim=True)']:.3e} "
f"equal={c['pad_len_800_vs_1000_bitwise_equal(trim=True)']} | trim(off) "
f"diff={c['max_abs_diff(trim=False)']:.3e} | padding内容 diff="
f"{c['max_abs_diff(padding_content_changed)']:.3e} | 负对照(忽略mask) diff="
f"{c['max_abs_diff(mask_ignored_negative_control)']:.3e}")
print("[2b] mask 正确性(真实 RNA-FM 输出,padding 位置非零)...")
report["2b_mask_real_rnafm"] = test_mask_real_rnafm(args, samples)
r2b = report["2b_mask_real_rnafm"]
print(f" L {r2b['L_solo']} vs {r2b['L_batch']}(多出 {r2b['padding_positions']} 个 padding 位,"
f"padding 值的 max|·|={r2b['padding_max_abs_value']},全零={r2b['padding_is_zero']})")
for c in r2b["cases"]:
print(f" {c['method']:9s} equal={c['bitwise_equal']} maxdiff={c['max_abs_diff']:.3e}")
print("[3] 梯度 ...")
report["3_gradients"] = test_gradients()
for c in report["3_gradients"]["cases"]:
print(f" {c['method']:9s} 零梯度 {c['n_zero_grad_tensors']}/{c['n_param_tensors']} "
f"|grad|={c['total_grad_norm']:.4e} |输入梯度|={c['input_grad_norm']:.3e}")
print("[3b] 初始化行为 ...")
report["3b_init_behavior"] = test_init_behavior()
for c in report["3b_init_behavior"]["cases"]:
print(f" {c['module']:30s} latent 两两平均余弦 = "
f"{c['mean_pairwise_cosine_between_latents']:.6f}")
print("[4] 可训参数量 ...")
report["4_params"] = test_params()
for m, v in report["4_params"]["methods"].items():
print(f" {m:9s} 压缩器 {v['compressor_params']:>10,} "
f"总可训 {v['total_trainable']:>10,} x{v['ratio_vs_projection']}")
print("[5] None 路径逐位等价 ...")
report["5_equivalence"] = test_equivalence(cfg, samples, args.golden, args.prechange_golden)
eq = report["5_equivalence"]
a5 = eq["A_in_process_replica_vs_current"]
print(f" A. 同进程:现行 vs 改动前代码逐字复刻 → ok={a5['ok']} "
f"({len(a5['cases'])} 个张量全 True={all(c['bitwise_equal(torch.equal)'] for c in a5['cases'])})"
f" 投影块=projection(rna) 逐位一致={eq['A_proj_block_is_exactly_projection_rna']}")
b5 = eq["B_seeded_golden_vs_current"]
if b5.get("ok") is None:
print(f" B. seeded golden:{b5['note']}")
else:
print(f" B. seeded golden(seed=0,跨进程)→ ok={b5['ok']} "
f"{[c['tensor'] for c in b5['cases'] if c['bitwise_equal(torch.equal)']]}")
c5 = eq["C_replica_faithfulness_vs_original_prechange_golden"]
if c5.get("ok") is None:
print(f" C. 复刻忠实性:{c5['note']}")
else:
bad = [c["tensor"] for c in c5["cases"]
if c.get("counted_in_verdict", True) and not c["bitwise_equal(torch.equal)"]]
print(f" C. 复刻忠实性(对原始未设种子快照)→ ok={c5['ok']} 不一致项={bad or '无'}")
print(f" full inputs(含投影块)逐位一致:"
f"{c5['full_inputs_bitwise_equal_per_batch']}")
print("[5b] E 轨路径(compress_latent=64 走真 build_batch)...")
report["5b_e_track_path"] = test_e_track_path(cfg, samples, 64)
for c in report["5b_e_track_path"]["cases"]:
print(f" {c['method']:9s} inputs={c['inputs_shape']} 期望总长={c['expected_len(prefix+latent+suffix+answer)']} "
f"实际={c['attn_len']} 答案token={c['answer_tokens']} "
f"RNA块==proj(comp(h))={c['rna_block_is_projection(compressor(hidden))']} "
f"可训={c['trainable_params']:,}(+{c['compressor_params']:,})")
print("[6] 序列长度 / 加速比 ...")
report["6_speedup"] = test_speedup(731, 368, 64)
print(f" {report['6_speedup']['total_before']} -> {report['6_speedup']['total_after']} "
f"= x{report['6_speedup']['speedup_x']} (假设口径:RNA 731 + 答案 368)")
# 真实口径:用真 tokenizer + 真 RNA-FM 数一遍
tk = measure_tokens(args, samples)
report["6_speedup"]["measured_tokens"] = tk
stats = report["5_equivalence"].get("token_stats") or []
if stats:
s0 = stats[0]
n_ans = s0["answer_tokens"]
n_rna_soft = int(tk["encoder_valid_tokens_per_seq"][0])
meas_before = tk["prefix_tokens"] + n_rna_soft + tk["suffix_tokens"] + n_ans
meas_after = tk["prefix_tokens"] + 64 + tk["suffix_tokens"] + n_ans
report["6_speedup"]["measured_full"] = {
"sample": 0, "n_nt": len(samples[0]["sequence"]), "rna_soft_tokens": n_rna_soft,
"answer_tokens": n_ans, "total_before": meas_before, "total_after_at_64": meas_after,
"delta": meas_before - meas_after,
"matches_build_batch_total": meas_before == s0["total"],
"speedup_x": round(meas_before / meas_after, 3)}
print(f" 实测 tokenizer:prefix={tk['prefix_tokens']} suffix={tk['suffix_tokens']} "
f"encoder_valid={tk['encoder_valid_tokens_per_seq']}")
if report["6_speedup"]["measured_full"]:
mf = report["6_speedup"]["measured_full"]
print(f" 实测全序列:{mf['total_before']} -> {mf['total_after_at_64']} "
f"= x{mf['speedup_x']}(与 build_batch 实测总长一致={mf['matches_build_batch_total']})")
print("[+] CPU 相对耗时 ...")
report["timing_cpu"] = measure_time()
for k, v in report["timing_cpu"]["modules"].items():
print(f" {k:28s} {v['fwd+bwd_ms_median']} ms")
checks = {
"1_shapes": report["1_shapes"]["ok"],
"2_mask": report["2_mask"]["ok"],
"2b_mask_real_rnafm": report["2b_mask_real_rnafm"]["ok"],
"3_gradients": report["3_gradients"]["ok"],
"5_none_path_bitwise_equal": report["5_equivalence"]["ok"],
"5b_e_track_path": report["5b_e_track_path"]["ok"],
}
report["checks"] = checks
report["passed"] = all(v is True for v in checks.values())
proj = report["4_params"]["projection_only(compress_latent=None)"]
rp = report["4_params"]["methods"]["resampler"]
report["conclusions"] = {
"recommended_default": {
"compress_latent": 64,
"compress_method": "resampler",
"kwargs": {"d_model": 256, "n_layers": 2, "n_heads": 8, "ffn_mult": 2.0,
"zero_init_q": True},
"compressor_params": rp["compressor_params"],
"total_trainable": rp["total_trainable"],
"vs_reproduction_track": f"{proj:,} -> {rp['total_trainable']:,} "
f"(x{rp['ratio_vs_projection']})",
},
"why": [
"三种 method 的 mask 正确性与 padding 无关性都成立(第 2/2b 项),"
"所以选型只看'能不能学会挑残基'和'训练负担'。",
"meanpool / strided 是手工固定的聚合:不管问题是什么,"
"同一段残基永远压成同一个向量。QA 集里有大量'某个位点/突变/结构域'的问题,"
"内容相关的选择能力只能由可学习 query 提供 => 默认给 resampler。",
"resampler 在 step 0 就是安全的:zero_init_q 让注意力恰好均匀,"
"latent 落在'有效残基均值'这一行为类里(第 3b 项),冻结的 LLM 不会先吃到"
"随机 query 的噪声,之后再靠 to_q 的梯度把均匀解打破。",
f"代价:+{rp['compressor_params']:,} 可训参数(复现轨 {proj:,} 的 "
f"{rp['compressor_params']/proj:.2f} 倍),CPU 上一次 fwd+bwd 约 "
f"{report['timing_cpu']['modules']['resampler']['fwd+bwd_ms_median']} ms "
f"(1299 token 的那次 Stage1 前向在 CPU 上要 76 s,压缩器占比可忽略)。",
],
"ablation_ladder": [
"meanpool(0 参数):先跑它,作为'压缩本身损失多少'的下界对照;若它与压缩前打平,"
"就直接用,训练负担为 0。",
"strided(3,200 参数):加上局部平滑,几乎零成本。",
"resampler(1,401,216 参数):默认;唯一能学'看哪里'的一档。",
"三档只改 Stage1Config 的两个字段,其余超参完全一致,A/B 是免费的。",
],
"e_track_warning": (
"本模块属于 **E 轨(改进轨)**,不是复现轨。原版是'每个 token 都投影'。"
"凡是用 compress_latent != None 跑出来的任何数字(loss / ROUGE / semantic),"
"**必须与 R 轨分数分开汇报**,不能混在复现结论里。"
"R 轨的对照配置永远是 compress_latent=None。"),
"r_track_untouched": (
"compress_latent=None 时 build_batch 的 inputs/attn/labels 与加开关之前"
"逐位一致(第 5 项,torch.equal 全 True,max_abs_diff=0.0);"
"Stage1Config 新增字段全部有默认值,train_stage1 的可训参数集合在 None 时"
"就是 projection.parameters()(顺序不变)。"),
"known_caveats": [
"save_stage1 在 E 轨 checkpoint 里多存一个 'compression' 键;"
"scripts/22_zero_encoder_probe.py 目前只 load projection,"
"所以带压缩器的 checkpoint **不能**直接喂给它(要先用同一 cfg 建模型再手动 load)。",
"zero_init_q=True 会让 ln_q / to_k 在 step 0 的梯度恰好为 0(to_q=0 的必然结果,"
"第 3 项记录了这一点),to_q 拿到梯度后第 1 步起整栈恢复。",
],
}
print()
for k, v in checks.items():
print(f" [{'ok ' if v is True else ('N/A ' if v is None else 'FAIL')}] {k}")
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"\n[comp-test] -> {args.out}")
return 0 if report["passed"] else 1
if __name__ == "__main__":
raise SystemExit(main())