138 lines
6.0 KiB
Python
138 lines
6.0 KiB
Python
|
|
"""Train the landmark router on REAL RAG data (LongbenchSeg), distilling to the model's own attention.
|
||
|
|
|
||
|
|
Fixes the synthetic-capitals distribution gap. No gold-block labels needed: the target at each answer
|
||
|
|
position is the ORACLE top-1 block = argmax of full-block attention mass there (the model's own
|
||
|
|
retrieval signal). Router = linear over per-(layer,head) landmark attention -> predict that block.
|
||
|
|
|
||
|
|
Run: source env.sh && HF_HOME=/projects/bdjx/hxia3/hf_cache_proj PYTHONPATH=. \
|
||
|
|
python scripts/train_router_real.py --model hf_release/model --save-router checkpoints/qwen3-14b-router-real.pt
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import random
|
||
|
|
|
||
|
|
import torch
|
||
|
|
import torch.nn as nn
|
||
|
|
import torch.nn.functional as F
|
||
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||
|
|
|
||
|
|
import scripts.distill_lora as D # load_data (real LongbenchSeg/SemanticSeg), masks
|
||
|
|
import scripts.e2e_nll as N # landmarks_content, mask_for
|
||
|
|
|
||
|
|
|
||
|
|
@torch.no_grad()
|
||
|
|
def collect(model, examples, summary_n, dev, max_tokens=1100):
|
||
|
|
"""Per example -> (feat [A,nb,L*H], oracle_top1 [A]). feat = per-(layer,head) attention to each
|
||
|
|
block's SUMMARY = its last `summary_n` content tokens (mean). summary_n=1 -> old single-landmark."""
|
||
|
|
data = []
|
||
|
|
for ids, blk, sink, kl0, ce0, src in examples:
|
||
|
|
if len(ids) > max_tokens:
|
||
|
|
continue
|
||
|
|
nb = max(blk) + 1
|
||
|
|
if nb < 3:
|
||
|
|
continue
|
||
|
|
n = len(ids)
|
||
|
|
rows = {i: set(range(nb)) for i in range(n - kl0)}
|
||
|
|
mask = N.mask_for(blk, sink, kl0, nb, rows, dev)
|
||
|
|
out = model(input_ids=torch.tensor([ids], device=dev), attention_mask=mask,
|
||
|
|
output_attentions=True)
|
||
|
|
atts = torch.stack(out.attentions, 0)[:, 0].float() # [L,H,n,n]
|
||
|
|
_, isc = N.landmarks_content(blk, sink, nb)
|
||
|
|
bpos = {b: [] for b in range(nb)}
|
||
|
|
content = torch.zeros(nb, n, device=dev)
|
||
|
|
for j, c in enumerate(isc):
|
||
|
|
if c:
|
||
|
|
content[blk[j], j] = 1.0; bpos[blk[j]].append(j)
|
||
|
|
a0 = ce0
|
||
|
|
if a0 >= n - 1:
|
||
|
|
continue
|
||
|
|
A = n - a0
|
||
|
|
# feat[t, b] = mean over block b's last-summary_n content tokens of attn, per (layer,head)
|
||
|
|
feat = torch.zeros(A, nb, atts.shape[0] * atts.shape[1])
|
||
|
|
for b in range(nb):
|
||
|
|
cols = torch.tensor(bpos[b][-summary_n:], device=dev)
|
||
|
|
v = atts[:, :, a0:n, :][:, :, :, cols].mean(-1) # [L,H,A]
|
||
|
|
feat[:, b, :] = v.permute(2, 0, 1).reshape(A, -1).cpu()
|
||
|
|
full_mass = (atts.mean(dim=(0, 1))[a0:n] @ content.T)
|
||
|
|
data.append((feat, full_mass.argmax(-1).cpu()))
|
||
|
|
return data
|
||
|
|
|
||
|
|
|
||
|
|
def coverage(scorer, data, ks, nb_cap):
|
||
|
|
cov = {k: 0.0 for k in ks}; tot = 0
|
||
|
|
for feat, tgt in data:
|
||
|
|
sc = scorer(feat)
|
||
|
|
rank = sc.argsort(-1, descending=True)
|
||
|
|
for k in ks:
|
||
|
|
cov[k] += (rank[:, :k] == tgt.view(-1, 1)).any(-1).float().sum().item()
|
||
|
|
tot += feat.shape[0]
|
||
|
|
return {k: cov[k] / tot for k in ks}
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
ap.add_argument("--model", required=True)
|
||
|
|
ap.add_argument("--n-examples", type=int, default=350)
|
||
|
|
ap.add_argument("--max-len", type=int, default=1100)
|
||
|
|
ap.add_argument("--epochs", type=int, default=400)
|
||
|
|
ap.add_argument("--wd", type=float, default=1e-3)
|
||
|
|
ap.add_argument("--summary-tokens", type=int, default=8, help="block summary = last N content tokens")
|
||
|
|
ap.add_argument("--seed", type=int, default=0)
|
||
|
|
ap.add_argument("--save-router", default=None)
|
||
|
|
args = ap.parse_args()
|
||
|
|
dev = "cuda"
|
||
|
|
|
||
|
|
print(f"loading {args.model} ...")
|
||
|
|
tok = AutoTokenizer.from_pretrained(args.model)
|
||
|
|
model = AutoModelForCausalLM.from_pretrained(
|
||
|
|
args.model, dtype=torch.bfloat16, attn_implementation="eager").to(dev).eval()
|
||
|
|
sink_ids = tok("\n", add_special_tokens=False)["input_ids"] * 4
|
||
|
|
|
||
|
|
print("loading real RAG data (LongbenchSeg) ...")
|
||
|
|
ex = D.load_data(tok, sink_ids, args.max_len, args.n_examples, args.seed)
|
||
|
|
ex = [e for e in ex if e[5] == "lb"] # QA (query+answer) examples
|
||
|
|
random.Random(1).shuffle(ex)
|
||
|
|
print(f" {len(ex)} QA examples")
|
||
|
|
print(f"collecting features (summary = last {args.summary_tokens} tokens) + oracle targets ...")
|
||
|
|
data = collect(model, ex, args.summary_tokens, dev, args.max_len)
|
||
|
|
print(f" usable: {len(data)} examples, {sum(f.shape[0] for f,_ in data)} answer positions")
|
||
|
|
n_eval = max(20, len(data) // 5)
|
||
|
|
ev, tr = data[:n_eval], data[n_eval:]
|
||
|
|
LH = tr[0][0].shape[-1]; nb_max = max(f.shape[1] for f, _ in data)
|
||
|
|
print(f" feat dim L*H={LH} | train {len(tr)} / eval {len(ev)}")
|
||
|
|
|
||
|
|
router = nn.Linear(LH, 1).to(dev)
|
||
|
|
opt = torch.optim.Adam(router.parameters(), lr=1e-3, weight_decay=args.wd)
|
||
|
|
Xtr = [(f.to(dev), t.to(dev)) for f, t in tr]
|
||
|
|
for epq in range(1, args.epochs + 1):
|
||
|
|
opt.zero_grad(); tl = 0.0
|
||
|
|
for feat, tgt in Xtr:
|
||
|
|
sc = router(feat).squeeze(-1)
|
||
|
|
loss = F.cross_entropy(sc, tgt); loss.backward(); tl += loss.item()
|
||
|
|
opt.step()
|
||
|
|
if epq % 100 == 0 or epq == 1:
|
||
|
|
print(f"epoch {epq:4d} | CE {tl/len(Xtr):.4f}")
|
||
|
|
|
||
|
|
ks = [1, 2, 3]
|
||
|
|
with torch.no_grad():
|
||
|
|
rt = lambda f: router(f.to(dev)).squeeze(-1).cpu()
|
||
|
|
mean_lm = lambda f: f.mean(-1)
|
||
|
|
ev_r = coverage(rt, ev, ks, nb_max)
|
||
|
|
ev_m = coverage(mean_lm, ev, ks, nb_max)
|
||
|
|
print(f"\nheld-out coverage of the model's own oracle top-1 block (real LongBench QA):")
|
||
|
|
print(f"{'k':>3} | {'untrained mean-lm':>17} | {'trained router (eval)':>21}")
|
||
|
|
for k in ks:
|
||
|
|
print(f"{k:>3} | {ev_m[k]:>17.3f} | {ev_r[k]:>21.3f}")
|
||
|
|
|
||
|
|
if args.save_router:
|
||
|
|
torch.save({"state_dict": router.state_dict(), "in_dim": LH,
|
||
|
|
"summary_tokens": args.summary_tokens,
|
||
|
|
"note": f"linear over per-(layer,head) attn to block's last-{args.summary_tokens} tokens; real LongBench QA"},
|
||
|
|
args.save_router)
|
||
|
|
print(f"saved router -> {args.save_router} (summary_tokens={args.summary_tokens})")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|