Feature Extraction
Transformers
TensorBoard
Safetensors
English
captionbert_v2
sentence-similarity
consensus-distillation
geometric-deep-learning
amoe
custom_code
Instructions to use AbstractPhil/captionbert-8192-v2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use AbstractPhil/captionbert-8192-v2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="AbstractPhil/captionbert-8192-v2", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("AbstractPhil/captionbert-8192-v2", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Create pretrain_trainer.py
Browse files- trainers/pretrain_trainer.py +1167 -0
trainers/pretrain_trainer.py
ADDED
|
@@ -0,0 +1,1167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# CAPTIONBERT-8192-v2 β CONSENSUS DISTILLATION AT CC12M SCALE
|
| 3 |
+
#
|
| 4 |
+
# Once ModernBert's teacher captions are repaired the system will have the full 36m.
|
| 5 |
+
#
|
| 6 |
+
# v2 vs the shipped 500k model, per Phil's 2026-07-31 guidance:
|
| 7 |
+
# - NO ALIGNMENT BANK. v1's bank was additive and experimental; measured on real
|
| 8 |
+
# embeddings its expert-consistency block varied 0.2% across samples and took
|
| 9 |
+
# 0.23% of geo_proj energy while anchor distances took 98.7%. Banks in this
|
| 10 |
+
# format are content extensions β an AMOE-LORA is the right carrier, attached
|
| 11 |
+
# as a separate finetune pass on the prefitted core. Not here.
|
| 12 |
+
# - LEGROOM. d 384->512, 6L->12L, ff 1536->2048, heads 6->8. 26.0M -> 58.3M
|
| 13 |
+
# (0.53x bert-base, so the compression story survives). Sized for many
|
| 14 |
+
# overlapping sources at ~36M features/teacher, not one 500k census.
|
| 15 |
+
# - CHAMPION OBJECTIVE. InfoNCE + per-sample MSE against the consensus β the
|
| 16 |
+
# consensus_nce_mse form that won the CC12M vision matrix on every task gauge,
|
| 17 |
+
# both seeds. NO shipped rotation needed here: that line aligns to a running
|
| 18 |
+
# mean (frame free), this one aligns to a REFERENCE MEMBER (bert), so the frame
|
| 19 |
+
# is pinned by construction. A frame-fit gauge runs anyway to confirm it.
|
| 20 |
+
# - CULL-PROOF. Colab kills the VM every 24h and takes local disk with it.
|
| 21 |
+
# Full state (model/opt/sched/scaler/step/epoch/chunk-order/RNG) checkpoints on
|
| 22 |
+
# a TIME cadence, and pushes to HF so a cull costs minutes, not the run.
|
| 23 |
+
# - FULL TENSORBOARD. per-step losses + lr + grad-norm, per-eval gauges
|
| 24 |
+
# (mimicry, cos, isotropy, effective rank, CV), histograms, and the alignment
|
| 25 |
+
# report as text.
|
| 26 |
+
#
|
| 27 |
+
# STAGES (each resumable, each gated) β carried from the cc12m pipeline:
|
| 28 |
+
# 0 PARITY which caption field was embedded + row alignment. Hard gate.
|
| 29 |
+
# 1 FIT one global whitened-Procrustes map per expert -> bert, stratified
|
| 30 |
+
# random fit, reported OUT-OF-SAMPLE on held-out chunks.
|
| 31 |
+
# 2 TARGETS per-chunk consensus -> fp16, ledgered, expert shards deleted after.
|
| 32 |
+
# 3 TRAIN streams (captions, consensus) pairs, dynamic padding.
|
| 33 |
+
#
|
| 34 |
+
# Colab-cell-safe. HF_TOKEN from Colab secrets (key icon) or env.
|
| 35 |
+
# ============================================================================
|
| 36 |
+
|
| 37 |
+
import gc, json, math, os, random, sys, time, subprocess, shutil
|
| 38 |
+
from dataclasses import dataclass, asdict
|
| 39 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 40 |
+
|
| 41 |
+
for _p in ("datasets", "transformers", "huggingface_hub", "tensorboard", "safetensors"):
|
| 42 |
+
try:
|
| 43 |
+
__import__(_p)
|
| 44 |
+
except ImportError:
|
| 45 |
+
subprocess.run([sys.executable, "-m", "pip", "install", "-q", _p], check=False)
|
| 46 |
+
|
| 47 |
+
# Variable-length batches fragment the caching allocator badly; this is the
|
| 48 |
+
# documented mitigation and must be set BEFORE torch initialises CUDA.
|
| 49 |
+
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
| 50 |
+
|
| 51 |
+
import numpy as np
|
| 52 |
+
import torch
|
| 53 |
+
import torch.nn as nn
|
| 54 |
+
import torch.nn.functional as F
|
| 55 |
+
from huggingface_hub import hf_hub_download, HfApi, create_repo
|
| 56 |
+
from torch.utils.tensorboard import SummaryWriter
|
| 57 |
+
|
| 58 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 62 |
+
# BASE CONFIG
|
| 63 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 64 |
+
|
| 65 |
+
@dataclass
|
| 66 |
+
class BaseConfig:
|
| 67 |
+
run_name: str = "captionbert-8192-v2"
|
| 68 |
+
|
| 69 |
+
# ββ sources ββ (list so overlapping datasets can be added later)
|
| 70 |
+
sources: Tuple[Dict[str, Any], ...] = (
|
| 71 |
+
{"repo": "AbstractPhil/conceptual-captions-12m-webdataset-berts",
|
| 72 |
+
"n_chunks": 66, "chunk_rows": 500_000,
|
| 73 |
+
"missing": {"modern": (5, 7, 8, 21, 25, 26, 28, 32, 38, 46)}},
|
| 74 |
+
)
|
| 75 |
+
experts: Tuple[str, ...] = ("bert", "modern", "roberta", "albert", "distil")
|
| 76 |
+
ref_expert: str = "bert"
|
| 77 |
+
ref_hf_name: str = "google-bert/bert-base-uncased"
|
| 78 |
+
require_all_experts: bool = True
|
| 79 |
+
caption_field: Optional[str] = None
|
| 80 |
+
caption_field_candidates: Tuple[str, ...] = (
|
| 81 |
+
"caption_llava", "caption", "caption_llava_short")
|
| 82 |
+
|
| 83 |
+
work_dir: str = "/content/cbv2"
|
| 84 |
+
keep_expert_shards: bool = False
|
| 85 |
+
|
| 86 |
+
# ββ hardware allowance (Colab Pro+ / RTX 6000 Pro, measured 2026-07-31) ββ
|
| 87 |
+
# disk 235.7GB (~176 free) | RAM 176.9GB | GPU 95.6GB | 401.5 units @ 8.9/h = 45.1h
|
| 88 |
+
# The expert shards are 507GB β 2.1x the WHOLE DISK. They are streamed one chunk
|
| 89 |
+
# at a time and deleted; only the 43GB consensus is kept.
|
| 90 |
+
disk_floor_gb: float = 25.0 # abort a chunk if free disk drops below
|
| 91 |
+
ram_resident: bool = True # hold tokens+targets in RAM (48.8GB)
|
| 92 |
+
preflight: bool = True
|
| 93 |
+
|
| 94 |
+
# ββ backup (Colab culls at 24h; local disk dies with the VM) ββ
|
| 95 |
+
hf_repo: str = "AbstractPhil/captionbert-8192-v2"
|
| 96 |
+
targets_repo: str = "AbstractPhil/captionbert-8192-v2-consensus"
|
| 97 |
+
push_targets: bool = True # 43GB; re-derivable only from a 507GB pull
|
| 98 |
+
hf_push: bool = True
|
| 99 |
+
push_every_min: float = 30.0
|
| 100 |
+
keep_local_ckpts: int = 3
|
| 101 |
+
|
| 102 |
+
# ββ stage 0 ββ
|
| 103 |
+
parity_chunk: int = 0
|
| 104 |
+
parity_n: int = 64
|
| 105 |
+
parity_min_cos: float = 0.999
|
| 106 |
+
|
| 107 |
+
# ββ stage 1 ββ
|
| 108 |
+
fit_chunks: Tuple[int, ...] = (0, 11, 22, 33, 44, 55)
|
| 109 |
+
fit_rows_per_chunk: int = 4000 # 24k vs d=768 -> N/d = 31
|
| 110 |
+
holdout_chunks: Tuple[int, ...] = (60, 61)
|
| 111 |
+
fit_seed: int = 0
|
| 112 |
+
|
| 113 |
+
# ββ student (LEGROOM) ββ
|
| 114 |
+
d_model: int = 512 # was 384
|
| 115 |
+
n_heads: int = 8 # was 6
|
| 116 |
+
n_layers: int = 12 # was 6
|
| 117 |
+
d_ff: int = 2048 # was 1536
|
| 118 |
+
max_len: int = 8192 # name-bearing; costs 4.2M params
|
| 119 |
+
output_dim: int = 768 # consensus space = teacher dim
|
| 120 |
+
dropout: float = 0.1
|
| 121 |
+
pooling: str = "mean" # arm: "cls". teachers are mean-pooled
|
| 122 |
+
max_tokens: int = 256 # dynamic pad ceiling
|
| 123 |
+
# OOM FIX (2026-07-31, observed at B=2048): dynamic padding pads to the BATCH
|
| 124 |
+
# max, and with 2048 draws the max is essentially always the ceiling. The corpus
|
| 125 |
+
# mean is 48 tokens but every batch ran at L=256 -- attention memory goes as L^2,
|
| 126 |
+
# so 12 layers needed ~120 GB against 95 available.
|
| 127 |
+
# length_bucketing sorts within a shuffled window so a batch is length-
|
| 128 |
+
# homogeneous: L tracks the corpus mean (~48-64) instead of
|
| 129 |
+
# the ceiling. ~5x less memory AND ~5x less compute.
|
| 130 |
+
# grad_checkpointing bounds the worst case. The longest bucket IS a full batch
|
| 131 |
+
# at L=256; checkpointing puts that at ~19 GB instead of
|
| 132 |
+
# ~148 GB, for about 30% more compute.
|
| 133 |
+
length_bucketing: bool = True
|
| 134 |
+
bucket_window: int = 64 # batches per sort window
|
| 135 |
+
grad_checkpointing: bool = True
|
| 136 |
+
vram_probe: bool = True # forward+backward at worst case first
|
| 137 |
+
|
| 138 |
+
# ββ training (sized for 95.6GB GPU: batch size IS the InfoNCE negative count) ββ
|
| 139 |
+
epochs: int = 4 # 13.7k steps/ep at 2048 -> ~55k total
|
| 140 |
+
batch_size: int = 2048 # was 512; ~19GB activations, 4x negatives
|
| 141 |
+
lr: float = 6e-4 # sqrt-scaled from 3e-4 @ 512
|
| 142 |
+
min_lr: float = 1e-6
|
| 143 |
+
warmup_steps: int = 2000
|
| 144 |
+
grad_clip: float = 1.0
|
| 145 |
+
seed: int = 42
|
| 146 |
+
amp: bool = True
|
| 147 |
+
num_workers: int = 0 # RAM-resident: no workers needed
|
| 148 |
+
log_every: int = 50
|
| 149 |
+
eval_every: int = 1000
|
| 150 |
+
ckpt_every_min: float = 20.0 # TIME-based: culls are wall-clock
|
| 151 |
+
|
| 152 |
+
# ββ loss: the champion form ββ
|
| 153 |
+
nce_weight: float = 1.0
|
| 154 |
+
mse_weight: float = 1.0
|
| 155 |
+
nce_temperature: float = 0.07
|
| 156 |
+
cv_weight: float = 0.0 # arm: 0.1 reproduces the v1 stack
|
| 157 |
+
cv_target: float = 0.084
|
| 158 |
+
|
| 159 |
+
# ββ stages ββ
|
| 160 |
+
run_stage0: bool = True
|
| 161 |
+
run_stage1: bool = True
|
| 162 |
+
run_stage2: bool = True
|
| 163 |
+
run_stage3: bool = True
|
| 164 |
+
resume: bool = True
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
CFG = BaseConfig()
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 171 |
+
# HELPERS
|
| 172 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 173 |
+
|
| 174 |
+
def line(t=""):
|
| 175 |
+
print("β" * 78 if not t else f"ββ {t} " + "β" * max(0, 74 - len(t)))
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def paths(cfg) -> Dict[str, str]:
|
| 179 |
+
w = cfg.work_dir
|
| 180 |
+
d = {"root": w, "targets": f"{w}/targets", "maps": f"{w}/maps",
|
| 181 |
+
"ckpt": f"{w}/checkpoints", "tb": f"{w}/tensorboard", "shards": f"{w}/shards",
|
| 182 |
+
"config": f"{w}/config"}
|
| 183 |
+
for p in d.values():
|
| 184 |
+
os.makedirs(p, exist_ok=True)
|
| 185 |
+
return d
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def src0(cfg) -> Dict[str, Any]:
|
| 189 |
+
return cfg.sources[0]
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def usable_chunks(cfg) -> List[int]:
|
| 193 |
+
s = src0(cfg)
|
| 194 |
+
c = set(range(s["n_chunks"]))
|
| 195 |
+
if cfg.require_all_experts:
|
| 196 |
+
for miss in s.get("missing", {}).values():
|
| 197 |
+
c -= set(miss)
|
| 198 |
+
return sorted(c - set(cfg.holdout_chunks))
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def fetch(cfg, fname: str) -> str:
|
| 202 |
+
return hf_hub_download(src0(cfg)["repo"], fname, repo_type="dataset",
|
| 203 |
+
local_dir=paths(cfg)["shards"])
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def load_captions_chunk(cfg, c: int) -> List[str]:
|
| 207 |
+
raw = json.load(open(fetch(cfg, f"captions_{c:03d}.json")))
|
| 208 |
+
f = cfg.caption_field
|
| 209 |
+
if isinstance(raw, dict):
|
| 210 |
+
return list(raw[f])
|
| 211 |
+
if raw and isinstance(raw[0], dict):
|
| 212 |
+
return [r[f] for r in raw]
|
| 213 |
+
return list(raw)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def load_expert_chunk(cfg, expert: str, c: int) -> torch.Tensor:
|
| 217 |
+
return torch.load(fetch(cfg, f"{expert}_{c:03d}.pt"),
|
| 218 |
+
weights_only=True, map_location="cpu")
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def drop_shard(cfg, fname: str):
|
| 222 |
+
if cfg.keep_expert_shards:
|
| 223 |
+
return
|
| 224 |
+
p = os.path.join(paths(cfg)["shards"], fname)
|
| 225 |
+
if os.path.exists(p):
|
| 226 |
+
os.remove(p)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def free_gb(path: str) -> float:
|
| 230 |
+
st = os.statvfs(path)
|
| 231 |
+
return st.f_bavail * st.f_frsize / 1e9
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def purge_hf_cache(cfg):
|
| 235 |
+
"""
|
| 236 |
+
The expert shards total 507GB against a 235.7GB disk. hf_hub_download with
|
| 237 |
+
local_dir does not populate the global cache on modern hub versions, but a
|
| 238 |
+
stale HF_HOME cache or an older version WILL duplicate every shard and blow
|
| 239 |
+
the disk mid-run. Purge both, every chunk.
|
| 240 |
+
"""
|
| 241 |
+
for d in (os.path.join(paths(cfg)["shards"], ".cache"),
|
| 242 |
+
os.environ.get("HF_HUB_CACHE", ""),
|
| 243 |
+
os.path.expanduser("~/.cache/huggingface/hub")):
|
| 244 |
+
if d and os.path.isdir(d):
|
| 245 |
+
for entry in os.listdir(d):
|
| 246 |
+
if entry.startswith("datasets--"):
|
| 247 |
+
shutil.rmtree(os.path.join(d, entry), ignore_errors=True)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def preflight(cfg):
|
| 251 |
+
"""Hard-check the allowance before anything expensive starts."""
|
| 252 |
+
line("PREFLIGHT β disk / RAM / GPU vs the plan")
|
| 253 |
+
P = paths(cfg)
|
| 254 |
+
disk = free_gb(P["root"])
|
| 255 |
+
s = src0(cfg)
|
| 256 |
+
n_keep = len(usable_chunks(cfg)) + len(cfg.holdout_chunks)
|
| 257 |
+
rows = n_keep * s["chunk_rows"]
|
| 258 |
+
targets_gb = rows * cfg.output_dim * 2 / 1e9
|
| 259 |
+
transient_gb = len(cfg.experts) * 1.536
|
| 260 |
+
caps_gb = s["n_chunks"] * 0.120
|
| 261 |
+
need = targets_gb + caps_gb + transient_gb + 10.0
|
| 262 |
+
print(f" source on HF : {s['n_chunks'] * len(cfg.experts) * 1.536:.0f} GB expert shards "
|
| 263 |
+
f"(streamed one chunk at a time, deleted after)")
|
| 264 |
+
print(f" disk free : {disk:.1f} GB | stage-2 peak need β {need:.1f} GB "
|
| 265 |
+
f"(targets {targets_gb:.1f} + captions {caps_gb:.1f} + transient {transient_gb:.1f})")
|
| 266 |
+
if disk < need:
|
| 267 |
+
raise RuntimeError(
|
| 268 |
+
f"DISK: {disk:.1f} GB free, need β {need:.1f} GB. Free space, reduce chunks, "
|
| 269 |
+
f"or set push_targets=True and drop consensus locally after each push.")
|
| 270 |
+
try:
|
| 271 |
+
import psutil
|
| 272 |
+
ram = psutil.virtual_memory().total / 1e9
|
| 273 |
+
except Exception:
|
| 274 |
+
ram = float("nan")
|
| 275 |
+
ram_need = (rows * 100 * 2 + rows * 8 + rows * cfg.output_dim * 2) / 1e9
|
| 276 |
+
print(f" RAM total : {ram:.1f} GB | ram_resident need β {ram_need:.1f} GB "
|
| 277 |
+
f"(ragged tokens + offsets + fp16 targets)")
|
| 278 |
+
if cfg.ram_resident and ram == ram and ram_need > 0.7 * ram:
|
| 279 |
+
print(f" !! ram_resident wants {ram_need:.1f} GB of {ram:.1f}. "
|
| 280 |
+
f"Set ram_resident=False to stream per chunk from disk instead.")
|
| 281 |
+
if DEVICE == "cuda":
|
| 282 |
+
g = torch.cuda.get_device_properties(0).total_memory / 1e9
|
| 283 |
+
print(f" GPU : {torch.cuda.get_device_name()} {g:.1f} GB | "
|
| 284 |
+
f"batch {cfg.batch_size} -> {cfg.batch_size} InfoNCE negatives")
|
| 285 |
+
print(f" plan : {rows:,} rows, {rows // cfg.batch_size:,} steps/epoch "
|
| 286 |
+
f"x {cfg.epochs} = {rows // cfg.batch_size * cfg.epochs:,} steps")
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def effective_rank(x: torch.Tensor) -> float:
|
| 290 |
+
xc = (x - x.mean(0, keepdim=True)).double()
|
| 291 |
+
s2 = torch.linalg.svdvals(xc) ** 2
|
| 292 |
+
return float((s2.sum() ** 2 / (s2 ** 2).sum()).item())
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def hf_token() -> Optional[str]:
|
| 296 |
+
t = os.environ.get("HF_TOKEN")
|
| 297 |
+
if t:
|
| 298 |
+
return t
|
| 299 |
+
try:
|
| 300 |
+
from google.colab import userdata
|
| 301 |
+
return userdata.get("HF_TOKEN")
|
| 302 |
+
except Exception:
|
| 303 |
+
return None
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 307 |
+
# BACKUP β a Colab cull must cost minutes, not the run
|
| 308 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 309 |
+
|
| 310 |
+
class Backup:
|
| 311 |
+
def __init__(self, cfg):
|
| 312 |
+
self.cfg, self.api, self.ok, self.last = cfg, None, False, 0.0
|
| 313 |
+
if not cfg.hf_push:
|
| 314 |
+
return
|
| 315 |
+
tok = hf_token()
|
| 316 |
+
if not tok:
|
| 317 |
+
print(" [backup] no HF_TOKEN β LOCAL ONLY. A cull will lose the run.")
|
| 318 |
+
return
|
| 319 |
+
try:
|
| 320 |
+
create_repo(cfg.hf_repo, token=tok, exist_ok=True, private=True)
|
| 321 |
+
self.api = HfApi(token=tok)
|
| 322 |
+
self.ok = True
|
| 323 |
+
print(f" [backup] -> {cfg.hf_repo} (private)")
|
| 324 |
+
except Exception as e:
|
| 325 |
+
print(f" [backup] disabled: {type(e).__name__}: {str(e)[:100]}")
|
| 326 |
+
|
| 327 |
+
def push(self, force: bool = False, msg: str = "checkpoint"):
|
| 328 |
+
if not self.ok:
|
| 329 |
+
return
|
| 330 |
+
if not force and (time.time() - self.last) / 60 < self.cfg.push_every_min:
|
| 331 |
+
return
|
| 332 |
+
P = paths(self.cfg)
|
| 333 |
+
try:
|
| 334 |
+
for folder, dest in ((P["ckpt"], "checkpoints"), (P["tb"], "tensorboard"),
|
| 335 |
+
(P["maps"], "maps"), (P["config"], "config")):
|
| 336 |
+
if os.path.isdir(folder) and os.listdir(folder):
|
| 337 |
+
self.api.upload_folder(folder_path=folder, path_in_repo=dest,
|
| 338 |
+
repo_id=self.cfg.hf_repo,
|
| 339 |
+
commit_message=f"{msg} ({dest})")
|
| 340 |
+
self.last = time.time()
|
| 341 |
+
print(f" [backup] pushed ({msg})")
|
| 342 |
+
except Exception as e:
|
| 343 |
+
print(f" [backup] push failed: {type(e).__name__}: {str(e)[:100]}")
|
| 344 |
+
|
| 345 |
+
def pull_latest(self) -> Optional[str]:
|
| 346 |
+
"""Recover state.pt after a cull."""
|
| 347 |
+
if not self.ok:
|
| 348 |
+
return None
|
| 349 |
+
try:
|
| 350 |
+
p = hf_hub_download(self.cfg.hf_repo, "checkpoints/state.pt",
|
| 351 |
+
token=hf_token(), local_dir=paths(self.cfg)["root"])
|
| 352 |
+
print(f" [backup] recovered {p}")
|
| 353 |
+
return p
|
| 354 |
+
except Exception:
|
| 355 |
+
return None
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 359 |
+
# STAGE 0 β PARITY GATE
|
| 360 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 361 |
+
|
| 362 |
+
def stage0_parity(cfg) -> str:
|
| 363 |
+
"""
|
| 364 |
+
Which caption field was embedded, and is row i of <expert>_XXX.pt caption i?
|
| 365 |
+
The manifest names three fields and does not say which was used. If the stored
|
| 366 |
+
vectors came from caption_llava and the student trains on caption_llava_short,
|
| 367 |
+
every target is silently wrong. Re-embed with the real reference model, demand
|
| 368 |
+
cos ~ 1.0. Nothing downstream runs until this passes.
|
| 369 |
+
"""
|
| 370 |
+
from transformers import AutoModel, AutoTokenizer
|
| 371 |
+
line("STAGE 0 β PARITY GATE (caption field + row alignment)")
|
| 372 |
+
stored = load_expert_chunk(cfg, cfg.ref_expert, cfg.parity_chunk)[: cfg.parity_n].float()
|
| 373 |
+
raw = json.load(open(fetch(cfg, f"captions_{cfg.parity_chunk:03d}.json")))
|
| 374 |
+
if isinstance(raw, dict):
|
| 375 |
+
fields = {k: list(v)[: cfg.parity_n] for k, v in raw.items()
|
| 376 |
+
if k in cfg.caption_field_candidates}
|
| 377 |
+
elif raw and isinstance(raw[0], dict):
|
| 378 |
+
fields = {k: [r[k] for r in raw[: cfg.parity_n]]
|
| 379 |
+
for k in raw[0] if k in cfg.caption_field_candidates}
|
| 380 |
+
else:
|
| 381 |
+
fields = {"(flat)": list(raw[: cfg.parity_n])}
|
| 382 |
+
print(f" stored rows {tuple(stored.shape)} | fields {list(fields)}")
|
| 383 |
+
|
| 384 |
+
tok = AutoTokenizer.from_pretrained(cfg.ref_hf_name)
|
| 385 |
+
mdl = AutoModel.from_pretrained(cfg.ref_hf_name).to(DEVICE).eval()
|
| 386 |
+
best, best_cos = None, -1.0
|
| 387 |
+
for f, texts in fields.items():
|
| 388 |
+
with torch.no_grad():
|
| 389 |
+
inp = tok(list(texts), max_length=512, padding=True, truncation=True,
|
| 390 |
+
return_tensors="pt").to(DEVICE)
|
| 391 |
+
h = mdl(**inp).last_hidden_state
|
| 392 |
+
m = inp.attention_mask.unsqueeze(-1).float()
|
| 393 |
+
pooled = ((h * m).sum(1) / m.sum(1).clamp(min=1)).float().cpu()
|
| 394 |
+
cos = F.cosine_similarity(pooled, stored, dim=-1)
|
| 395 |
+
print(f" {f:22s} cos mean {cos.mean():.6f} min {cos.min():.6f}")
|
| 396 |
+
if cos.mean().item() > best_cos:
|
| 397 |
+
best, best_cos = f, cos.mean().item()
|
| 398 |
+
del mdl; gc.collect(); torch.cuda.empty_cache()
|
| 399 |
+
if best_cos < cfg.parity_min_cos:
|
| 400 |
+
raise RuntimeError(
|
| 401 |
+
f"PARITY GATE FAIL: best field '{best}' only reaches cos {best_cos:.6f} "
|
| 402 |
+
f"(need >= {cfg.parity_min_cos}). Either the field is not among "
|
| 403 |
+
f"{cfg.caption_field_candidates}, row order differs, or the extraction used "
|
| 404 |
+
f"different pooling/truncation. DO NOT SPEND GPU TIME until this resolves.")
|
| 405 |
+
print(f" GATE PASS: field = '{best}' at cos {best_cos:.6f}")
|
| 406 |
+
return best
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 410 |
+
# STAGE 1 β GLOBAL WHITENED PROCRUSTES (out-of-sample reported)
|
| 411 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 412 |
+
|
| 413 |
+
def symmetric_inv_sqrt(cov: torch.Tensor, eps: float = 1e-6) -> torch.Tensor:
|
| 414 |
+
ev, evec = torch.linalg.eigh(cov.double())
|
| 415 |
+
return (evec @ torch.diag(torch.clamp(ev, min=eps).rsqrt()) @ evec.T).float()
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
def fit_map(S: torch.Tensor, T: torch.Tensor) -> Dict[str, torch.Tensor]:
|
| 419 |
+
N = S.shape[0]
|
| 420 |
+
s_mean, t_mean = S.mean(0, keepdim=True), T.mean(0, keepdim=True)
|
| 421 |
+
Sc, Tc = S - s_mean, T - t_mean
|
| 422 |
+
s_w = symmetric_inv_sqrt((Sc.T @ Sc) / max(N - 1, 1))
|
| 423 |
+
t_w = symmetric_inv_sqrt((Tc.T @ Tc) / max(N - 1, 1))
|
| 424 |
+
U, _, Vt = torch.linalg.svd(
|
| 425 |
+
(F.normalize(Tc @ t_w, dim=-1).T @ F.normalize(Sc @ s_w, dim=-1)).double(),
|
| 426 |
+
full_matrices=False)
|
| 427 |
+
return {"rotation": (U @ Vt).float(), "source_mean": s_mean.squeeze(0),
|
| 428 |
+
"target_mean": t_mean.squeeze(0), "source_whitener": s_w,
|
| 429 |
+
"target_whitener": t_w, "target_unwhitener": torch.linalg.pinv(t_w)}
|
| 430 |
+
|
| 431 |
+
|
| 432 |
+
def apply_map(emb: torch.Tensor, a) -> torch.Tensor:
|
| 433 |
+
x = (emb.float() - a["source_mean"]) @ a["source_whitener"]
|
| 434 |
+
return (x @ a["rotation"].T) @ a["target_unwhitener"]
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
def score_map(S, T, a) -> Dict[str, float]:
|
| 438 |
+
Sw = F.normalize((S - a["source_mean"]) @ a["source_whitener"], dim=-1)
|
| 439 |
+
Tw = F.normalize((T - a["target_mean"]) @ a["target_whitener"], dim=-1)
|
| 440 |
+
cos = F.cosine_similarity(Sw @ a["rotation"].T, Tw, dim=-1).mean().item()
|
| 441 |
+
n = min(2000, S.shape[0])
|
| 442 |
+
sim = F.normalize(apply_map(S[:n], a), dim=-1) @ F.normalize(T[:n], dim=-1).T
|
| 443 |
+
return {"cos": cos, "r1": (sim.argmax(1) == torch.arange(n)).float().mean().item(),
|
| 444 |
+
"n": int(S.shape[0]), "chance": 1.0 / n}
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def stage1_fit(cfg, bk: "Backup"):
|
| 448 |
+
line("STAGE 1 β GLOBAL ALIGNMENT (stratified fit, OUT-OF-SAMPLE report)")
|
| 449 |
+
P = paths(cfg)
|
| 450 |
+
mp = f"{P['maps']}/alignment_maps.pt"
|
| 451 |
+
if os.path.exists(mp):
|
| 452 |
+
print(" maps exist, loading"); return torch.load(mp, weights_only=False)
|
| 453 |
+
|
| 454 |
+
g = torch.Generator().manual_seed(cfg.fit_seed)
|
| 455 |
+
fit = {e: [] for e in cfg.experts}
|
| 456 |
+
for c in cfg.fit_chunks:
|
| 457 |
+
idx = None
|
| 458 |
+
for e in cfg.experts:
|
| 459 |
+
X = load_expert_chunk(cfg, e, c)
|
| 460 |
+
if idx is None:
|
| 461 |
+
idx = torch.randperm(X.shape[0], generator=g)[: cfg.fit_rows_per_chunk]
|
| 462 |
+
fit[e].append(X[idx].float()); del X; gc.collect()
|
| 463 |
+
drop_shard(cfg, f"{e}_{c:03d}.pt")
|
| 464 |
+
print(f" fit chunk {c:03d}: {len(idx)} random rows")
|
| 465 |
+
fit = {e: torch.cat(v) for e, v in fit.items()}
|
| 466 |
+
N = fit[cfg.ref_expert].shape[0]
|
| 467 |
+
print(f" fit set {N} rows, d=768 -> N/d = {N/768:.1f}")
|
| 468 |
+
|
| 469 |
+
hold = {e: [] for e in cfg.experts}
|
| 470 |
+
for c in cfg.holdout_chunks:
|
| 471 |
+
for e in cfg.experts:
|
| 472 |
+
X = load_expert_chunk(cfg, e, c)
|
| 473 |
+
hold[e].append(X[: cfg.fit_rows_per_chunk].float()); del X; gc.collect()
|
| 474 |
+
hold = {e: torch.cat(v) for e, v in hold.items()}
|
| 475 |
+
|
| 476 |
+
maps, report, T = {}, {}, fit[cfg.ref_expert]
|
| 477 |
+
for e in cfg.experts:
|
| 478 |
+
a = fit_map(fit[e], T)
|
| 479 |
+
ins, oos = score_map(fit[e], T, a), score_map(hold[e], hold[cfg.ref_expert], a)
|
| 480 |
+
maps[e], report[e] = a, {"in_sample": ins, "out_of_sample": oos}
|
| 481 |
+
tag = " (ref: must read ~1.0)" if e == cfg.ref_expert else ""
|
| 482 |
+
print(f" {e:9s} cos in {ins['cos']:.4f} / OUT {oos['cos']:.4f} "
|
| 483 |
+
f"R@1 in {ins['r1']:.4f} / OUT {oos['r1']:.4f} "
|
| 484 |
+
f"(chance {oos['chance']:.5f}){tag}")
|
| 485 |
+
print(" READ THE 'OUT' COLUMN. A 768x768 rotation is 294,528 free parameters;")
|
| 486 |
+
print(" at low N/d the in-sample cosine reproduces strong numbers from nothing.")
|
| 487 |
+
torch.save(maps, mp)
|
| 488 |
+
json.dump(report, open(f"{P['maps']}/fit_report.json", "w"), indent=2)
|
| 489 |
+
bk.push(force=True, msg="stage1 alignment maps")
|
| 490 |
+
return maps
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 494 |
+
# STAGE 2 β CONSENSUS TARGETS
|
| 495 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 496 |
+
|
| 497 |
+
def stage2_targets(cfg, maps, bk: "Backup") -> List[int]:
|
| 498 |
+
line("STAGE 2 β CONSENSUS TARGETS (fp16, per chunk, resumable)")
|
| 499 |
+
P = paths(cfg)
|
| 500 |
+
lp = f"{P['targets']}/ledger.json"
|
| 501 |
+
ledger = json.load(open(lp)) if os.path.exists(lp) else {}
|
| 502 |
+
want = sorted(set(usable_chunks(cfg)) | set(cfg.holdout_chunks))
|
| 503 |
+
print(f" {len(want)} chunks with all {len(cfg.experts)} experts | "
|
| 504 |
+
f"streaming {len(want)*len(cfg.experts)*1.536:.0f} GB through "
|
| 505 |
+
f"{free_gb(P['root']):.0f} GB of free disk")
|
| 506 |
+
tapi = None
|
| 507 |
+
if cfg.push_targets and bk.ok:
|
| 508 |
+
try:
|
| 509 |
+
create_repo(cfg.targets_repo, token=hf_token(), exist_ok=True,
|
| 510 |
+
private=True, repo_type="dataset")
|
| 511 |
+
tapi = HfApi(token=hf_token())
|
| 512 |
+
print(f" targets -> {cfg.targets_repo} (dataset, private)")
|
| 513 |
+
except Exception as e:
|
| 514 |
+
print(f" target push disabled: {type(e).__name__}: {str(e)[:80]}")
|
| 515 |
+
for c in want:
|
| 516 |
+
k, out_p = f"{c:03d}", f"{P['targets']}/consensus_{c:03d}.pt"
|
| 517 |
+
if ledger.get(k) and os.path.exists(out_p):
|
| 518 |
+
continue
|
| 519 |
+
if free_gb(P["root"]) < cfg.disk_floor_gb:
|
| 520 |
+
raise RuntimeError(f"DISK FLOOR: {free_gb(P['root']):.1f} GB free at chunk {k}. "
|
| 521 |
+
f"Push and drop earlier consensus files, then resume.")
|
| 522 |
+
acc, n = None, None
|
| 523 |
+
for e in cfg.experts:
|
| 524 |
+
X = load_expert_chunk(cfg, e, c).float()
|
| 525 |
+
if n is None:
|
| 526 |
+
n = X.shape[0]
|
| 527 |
+
elif X.shape[0] != n:
|
| 528 |
+
raise RuntimeError(f"chunk {k}: {e} has {X.shape[0]} rows, expected {n}")
|
| 529 |
+
A = apply_map(X, maps[e])
|
| 530 |
+
acc = A if acc is None else acc + A
|
| 531 |
+
del X, A; gc.collect()
|
| 532 |
+
drop_shard(cfg, f"{e}_{c:03d}.pt")
|
| 533 |
+
purge_hf_cache(cfg)
|
| 534 |
+
cons = F.normalize(acc / len(cfg.experts), dim=-1).half()
|
| 535 |
+
torch.save(cons, out_p)
|
| 536 |
+
er = effective_rank(cons[:4000].float())
|
| 537 |
+
ledger[k] = {"rows": int(cons.shape[0]), "target_erank": er, "ts": time.time()}
|
| 538 |
+
json.dump(ledger, open(lp, "w"), indent=2)
|
| 539 |
+
if tapi is not None:
|
| 540 |
+
try:
|
| 541 |
+
tapi.upload_file(path_or_fileobj=out_p,
|
| 542 |
+
path_in_repo=f"consensus_{k}.pt",
|
| 543 |
+
repo_id=cfg.targets_repo, repo_type="dataset",
|
| 544 |
+
commit_message=f"consensus chunk {k}")
|
| 545 |
+
except Exception as ex:
|
| 546 |
+
print(f" target push failed for {k}: {str(ex)[:70]}")
|
| 547 |
+
print(f" chunk {k}: {cons.shape[0]} targets | TARGET erank {er:.1f}/768 | "
|
| 548 |
+
f"disk free {free_gb(P['root']):.0f} GB")
|
| 549 |
+
del acc, cons; gc.collect()
|
| 550 |
+
eranks = [v["target_erank"] for v in ledger.values() if "target_erank" in v]
|
| 551 |
+
if eranks:
|
| 552 |
+
print(f" consensus target erank: mean {np.mean(eranks):.1f} "
|
| 553 |
+
f"min {min(eranks):.1f} max {max(eranks):.1f} of 768")
|
| 554 |
+
print(" (v1's STUDENT read 23.6 β compare against this to tell 'student")
|
| 555 |
+
print(" collapsed' from 'student faithfully matched a low-rank target')")
|
| 556 |
+
bk.push(force=True, msg="stage2 target ledger")
|
| 557 |
+
return want
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 561 |
+
# STUDENT
|
| 562 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 563 |
+
|
| 564 |
+
class CaptionEncoder(nn.Module):
|
| 565 |
+
"""Standalone caption encoder. No experts at inference. No bank."""
|
| 566 |
+
|
| 567 |
+
def __init__(self, vocab_size=30522, max_len=8192, d_model=512, n_heads=8,
|
| 568 |
+
n_layers=12, d_ff=2048, output_dim=768, dropout=0.1,
|
| 569 |
+
pad_token_id=0, pooling="mean", grad_checkpointing=False):
|
| 570 |
+
super().__init__()
|
| 571 |
+
self.pad_token_id, self.pooling = pad_token_id, pooling
|
| 572 |
+
self.grad_checkpointing = grad_checkpointing
|
| 573 |
+
self.token_emb = nn.Embedding(vocab_size, d_model, padding_idx=pad_token_id)
|
| 574 |
+
self.pos_emb = nn.Embedding(max_len, d_model)
|
| 575 |
+
self.emb_norm = nn.LayerNorm(d_model)
|
| 576 |
+
self.emb_drop = nn.Dropout(dropout)
|
| 577 |
+
layer = nn.TransformerEncoderLayer(
|
| 578 |
+
d_model=d_model, nhead=n_heads, dim_feedforward=d_ff, dropout=dropout,
|
| 579 |
+
activation="gelu", batch_first=True, norm_first=True)
|
| 580 |
+
self.encoder = nn.TransformerEncoder(layer, num_layers=n_layers,
|
| 581 |
+
enable_nested_tensor=False)
|
| 582 |
+
self.output_proj = nn.Sequential(
|
| 583 |
+
nn.Linear(d_model, d_model), nn.GELU(), nn.LayerNorm(d_model),
|
| 584 |
+
nn.Linear(d_model, output_dim))
|
| 585 |
+
|
| 586 |
+
def forward(self, input_ids, attention_mask=None):
|
| 587 |
+
L = input_ids.shape[1]
|
| 588 |
+
pos = torch.arange(L, device=input_ids.device).unsqueeze(0)
|
| 589 |
+
x = self.emb_drop(self.emb_norm(self.token_emb(input_ids) + self.pos_emb(pos)))
|
| 590 |
+
kpm = (~attention_mask.bool()) if attention_mask is not None \
|
| 591 |
+
else (input_ids == self.pad_token_id)
|
| 592 |
+
if self.grad_checkpointing and self.training:
|
| 593 |
+
for layer in self.encoder.layers:
|
| 594 |
+
x = torch.utils.checkpoint.checkpoint(
|
| 595 |
+
layer, x, None, kpm, use_reentrant=False)
|
| 596 |
+
else:
|
| 597 |
+
x = self.encoder(x, src_key_padding_mask=kpm)
|
| 598 |
+
if self.pooling == "cls":
|
| 599 |
+
pooled = x[:, 0]
|
| 600 |
+
else:
|
| 601 |
+
m = (attention_mask.unsqueeze(-1).float() if attention_mask is not None
|
| 602 |
+
else (~kpm).unsqueeze(-1).float())
|
| 603 |
+
pooled = (x * m).sum(1) / m.sum(1).clamp(min=1)
|
| 604 |
+
return F.normalize(self.output_proj(pooled), dim=-1)
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 608 |
+
# LOSS / GAUGES
|
| 609 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 610 |
+
|
| 611 |
+
def infonce(a, b, temperature=0.07):
|
| 612 |
+
logits = (a @ b.T) / temperature
|
| 613 |
+
lab = torch.arange(logits.shape[0], device=logits.device)
|
| 614 |
+
loss = (F.cross_entropy(logits, lab) + F.cross_entropy(logits.T, lab)) / 2
|
| 615 |
+
with torch.no_grad():
|
| 616 |
+
acc = (logits.argmax(-1) == lab).float().mean().item()
|
| 617 |
+
return loss, acc
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
def cayley_menger_vol2(pts):
|
| 621 |
+
pts = pts.float()
|
| 622 |
+
d = pts.unsqueeze(-2) - pts.unsqueeze(-3)
|
| 623 |
+
d2 = (d * d).sum(-1)
|
| 624 |
+
B, V, _ = d2.shape
|
| 625 |
+
cm = torch.zeros(B, V + 1, V + 1, device=d2.device, dtype=torch.float32)
|
| 626 |
+
cm[:, 0, 1:] = 1; cm[:, 1:, 0] = 1; cm[:, 1:, 1:] = d2
|
| 627 |
+
f = math.factorial(V - 1)
|
| 628 |
+
return ((-1.0) ** V) / ((2.0 ** (V - 1)) * f * f) * torch.linalg.det(cm)
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
def cv_loss(emb, target=0.084, n_samples=16):
|
| 632 |
+
B = emb.shape[0]
|
| 633 |
+
if B < 5:
|
| 634 |
+
return torch.zeros((), device=emb.device)
|
| 635 |
+
s = torch.stack([torch.sqrt(F.relu(cayley_menger_vol2(
|
| 636 |
+
emb[torch.randperm(B, device=emb.device)[:5]].unsqueeze(0))[0]) + 1e-12)
|
| 637 |
+
for _ in range(n_samples)])
|
| 638 |
+
return (s.std() / (s.mean() + 1e-8) - target).abs()
|
| 639 |
+
|
| 640 |
+
|
| 641 |
+
@torch.no_grad()
|
| 642 |
+
def cv_metric(emb, n=200):
|
| 643 |
+
v = [float(torch.sqrt(F.relu(cayley_menger_vol2(
|
| 644 |
+
emb[torch.randperm(emb.shape[0], device=emb.device)[:5]].unsqueeze(0))[0])
|
| 645 |
+
+ 1e-12).item()) for _ in range(n)]
|
| 646 |
+
a = np.array([x for x in v if x > 0])
|
| 647 |
+
return float(a.std() / (a.mean() + 1e-8)) if len(a) >= 10 else 0.0
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
@torch.no_grad()
|
| 651 |
+
def frame_fit_gauge(E: torch.Tensor, T: torch.Tensor, n_pairs: int = 2500) -> Dict[str, float]:
|
| 652 |
+
"""
|
| 653 |
+
Standing rider: judge relational objectives with a frame fit or they read as false
|
| 654 |
+
floors. MSE anchors the frame here and the consensus aligns to a REFERENCE MEMBER,
|
| 655 |
+
so a rotation should buy ~nothing. If it buys a lot, the frame is NOT pinned and
|
| 656 |
+
this model needs a shipped rotation after all. Held-out split, fp64.
|
| 657 |
+
"""
|
| 658 |
+
N = E.shape[0]
|
| 659 |
+
k = min(n_pairs, N // 2)
|
| 660 |
+
if k < 64:
|
| 661 |
+
return {"skipped": True}
|
| 662 |
+
perm = torch.randperm(N, generator=torch.Generator().manual_seed(0))
|
| 663 |
+
fit_i, hold_i = perm[:k], perm[k:]
|
| 664 |
+
U, _, Vt = torch.linalg.svd(E[fit_i].double().T @ T[fit_i].double(), full_matrices=False)
|
| 665 |
+
Er = F.normalize((E.double() @ (U @ Vt)).float(), dim=-1)
|
| 666 |
+
m = min(2000, len(hold_i))
|
| 667 |
+
hi = hold_i[:m]
|
| 668 |
+
sim = Er[hi] @ T[hi].T
|
| 669 |
+
return {"r1_after_rotation": (sim.argmax(1) == torch.arange(m)).float().mean().item(),
|
| 670 |
+
"cos_after_rotation": F.cosine_similarity(Er[hi], T[hi], dim=-1).mean().item(),
|
| 671 |
+
"n_heldout": int(m)}
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 675 |
+
# DATA
|
| 676 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 677 |
+
|
| 678 |
+
class RamStore:
|
| 679 |
+
"""
|
| 680 |
+
Everything resident in system RAM: ragged uint16 tokens + fp16 targets.
|
| 681 |
+
|
| 682 |
+
On the Pro+ box this is 48.8 GB of 176.9 β so the training loop does ZERO disk
|
| 683 |
+
I/O and needs no DataLoader workers. Ragged storage (flat token buffer + offsets)
|
| 684 |
+
keeps dynamic padding available at ~5.6 GB instead of the 14 GB a fixed 256-token
|
| 685 |
+
matrix would cost, and captions average ~100 tokens against a 256 ceiling.
|
| 686 |
+
"""
|
| 687 |
+
|
| 688 |
+
def __init__(self, cfg, chunks: List[int], tokenizer, tag=""):
|
| 689 |
+
self.cfg, self.tok = cfg, tokenizer
|
| 690 |
+
self.pad = tokenizer.pad_token_id
|
| 691 |
+
flat, offs, tgts, total = [], [0], [], 0
|
| 692 |
+
for c in chunks:
|
| 693 |
+
caps = load_captions_chunk(cfg, c)
|
| 694 |
+
t = torch.load(f"{paths(cfg)['targets']}/consensus_{c:03d}.pt",
|
| 695 |
+
weights_only=True, map_location="cpu")
|
| 696 |
+
n = min(len(caps), t.shape[0])
|
| 697 |
+
caps, t = caps[:n], t[:n]
|
| 698 |
+
for i in range(0, n, 20000):
|
| 699 |
+
enc = tokenizer(caps[i:i + 20000], max_length=cfg.max_tokens,
|
| 700 |
+
truncation=True, padding=False)["input_ids"]
|
| 701 |
+
for ids in enc:
|
| 702 |
+
flat.append(np.asarray(ids, dtype=np.uint16))
|
| 703 |
+
total += len(ids)
|
| 704 |
+
offs.append(total)
|
| 705 |
+
tgts.append(t)
|
| 706 |
+
print(f" chunk {c:03d}: {n:,} rows | flat tokens {total/1e6:.1f}M")
|
| 707 |
+
del caps, t; gc.collect()
|
| 708 |
+
self.flat = np.concatenate(flat) if flat else np.zeros(0, np.uint16)
|
| 709 |
+
del flat; gc.collect()
|
| 710 |
+
self.offs = np.asarray(offs, dtype=np.int64)
|
| 711 |
+
self.tgt = torch.cat(tgts)
|
| 712 |
+
del tgts; gc.collect()
|
| 713 |
+
self.n = len(self.offs) - 1
|
| 714 |
+
self.lens = (self.offs[1:] - self.offs[:-1]).astype(np.int32)
|
| 715 |
+
gb = (self.flat.nbytes + self.offs.nbytes + self.tgt.numel() * 2) / 1e9
|
| 716 |
+
mean_len = total / max(self.n, 1)
|
| 717 |
+
q = np.percentile(self.lens, [50, 90, 99, 100]).astype(int)
|
| 718 |
+
print(f" RamStore{tag}: {self.n:,} rows | {gb:.1f} GB RAM | "
|
| 719 |
+
f"mean {mean_len:.0f} tokens (ceiling {cfg.max_tokens})")
|
| 720 |
+
print(f" length p50 {q[0]} | p90 {q[1]} | p99 {q[2]} | max {q[3]}"
|
| 721 |
+
f" -- unbucketed, a batch pads to the BATCH MAX, i.e. ~{q[3]}")
|
| 722 |
+
|
| 723 |
+
def plan_batches(self, batch_size, seed, window_batches=64, bucket=True):
|
| 724 |
+
"""
|
| 725 |
+
Deterministic batch plan for one epoch. Returns a list of index arrays.
|
| 726 |
+
|
| 727 |
+
With bucket=True: shuffle, cut into windows of window_batches*batch_size,
|
| 728 |
+
sort each window by length, slice into batches, then shuffle the BATCH ORDER.
|
| 729 |
+
Batches end up length-homogeneous (so padding is near-free) while batch
|
| 730 |
+
composition stays random across the window and the model never sees the
|
| 731 |
+
corpus in length order. Deterministic in (seed), so a resume mid-epoch
|
| 732 |
+
regenerates the identical plan and the stored batch index stays valid.
|
| 733 |
+
"""
|
| 734 |
+
rng = np.random.default_rng(seed)
|
| 735 |
+
perm = rng.permutation(self.n)
|
| 736 |
+
if not bucket:
|
| 737 |
+
n_full = self.n // batch_size
|
| 738 |
+
return [perm[i * batch_size:(i + 1) * batch_size] for i in range(n_full)]
|
| 739 |
+
W = batch_size * max(window_batches, 1)
|
| 740 |
+
batches = []
|
| 741 |
+
for i in range(0, self.n, W):
|
| 742 |
+
win = perm[i:i + W]
|
| 743 |
+
win = win[np.argsort(self.lens[win], kind="stable")]
|
| 744 |
+
for j in range(0, len(win) - batch_size + 1, batch_size):
|
| 745 |
+
batches.append(win[j:j + batch_size])
|
| 746 |
+
rng.shuffle(batches)
|
| 747 |
+
return batches
|
| 748 |
+
|
| 749 |
+
def __len__(self):
|
| 750 |
+
return self.n
|
| 751 |
+
|
| 752 |
+
def batch(self, idx: np.ndarray):
|
| 753 |
+
"""Gather a batch with DYNAMIC padding to the batch max."""
|
| 754 |
+
seqs = [self.flat[self.offs[i]:self.offs[i + 1]] for i in idx]
|
| 755 |
+
L = max(len(s) for s in seqs)
|
| 756 |
+
ids = np.full((len(seqs), L), self.pad, dtype=np.int64)
|
| 757 |
+
am = np.zeros((len(seqs), L), dtype=np.int64)
|
| 758 |
+
for r, s in enumerate(seqs):
|
| 759 |
+
ids[r, :len(s)] = s
|
| 760 |
+
am[r, :len(s)] = 1
|
| 761 |
+
return (torch.from_numpy(ids), torch.from_numpy(am),
|
| 762 |
+
self.tgt[torch.from_numpy(idx)])
|
| 763 |
+
|
| 764 |
+
|
| 765 |
+
class ChunkPairs(torch.utils.data.Dataset):
|
| 766 |
+
"""Disk-streaming fallback when ram_resident=False."""
|
| 767 |
+
def __init__(self, cfg, chunk, tokenizer):
|
| 768 |
+
self.caps = load_captions_chunk(cfg, chunk)
|
| 769 |
+
self.tgt = torch.load(f"{paths(cfg)['targets']}/consensus_{chunk:03d}.pt",
|
| 770 |
+
weights_only=True, map_location="cpu")
|
| 771 |
+
n = min(len(self.caps), self.tgt.shape[0])
|
| 772 |
+
self.caps, self.tgt = self.caps[:n], self.tgt[:n]
|
| 773 |
+
self.tok, self.max_tokens = tokenizer, cfg.max_tokens
|
| 774 |
+
|
| 775 |
+
def __len__(self):
|
| 776 |
+
return len(self.caps)
|
| 777 |
+
|
| 778 |
+
def __getitem__(self, i):
|
| 779 |
+
return self.caps[i], self.tgt[i]
|
| 780 |
+
|
| 781 |
+
def collate(self, batch):
|
| 782 |
+
texts, tg = zip(*batch)
|
| 783 |
+
enc = self.tok(list(texts), max_length=self.max_tokens, padding=True,
|
| 784 |
+
truncation=True, return_tensors="pt") # DYNAMIC
|
| 785 |
+
return enc["input_ids"], enc["attention_mask"], torch.stack(tg)
|
| 786 |
+
|
| 787 |
+
|
| 788 |
+
@torch.no_grad()
|
| 789 |
+
def evaluate(student, source, cap=5000, batch=512) -> Dict[str, float]:
|
| 790 |
+
student.eval()
|
| 791 |
+
E, T = [], []
|
| 792 |
+
if isinstance(source, RamStore):
|
| 793 |
+
for i in range(0, min(cap, len(source)), batch):
|
| 794 |
+
ids, am, tg = source.batch(np.arange(i, min(i + batch, len(source))))
|
| 795 |
+
E.append(student(ids.to(DEVICE), am.to(DEVICE)).float().cpu())
|
| 796 |
+
T.append(tg.float())
|
| 797 |
+
else:
|
| 798 |
+
for ids, am, tg in source:
|
| 799 |
+
E.append(student(ids.to(DEVICE), am.to(DEVICE)).float().cpu())
|
| 800 |
+
T.append(tg.float())
|
| 801 |
+
if sum(x.shape[0] for x in E) >= cap:
|
| 802 |
+
break
|
| 803 |
+
E, T = torch.cat(E), F.normalize(torch.cat(T), dim=-1)
|
| 804 |
+
n = min(2000, E.shape[0])
|
| 805 |
+
sim = E[:n] @ T[:n].T
|
| 806 |
+
ss = E[:n] @ E[:n].T
|
| 807 |
+
ss.fill_diagonal_(0)
|
| 808 |
+
out = {"mimicry_r1": (sim.argmax(1) == torch.arange(n)).float().mean().item(),
|
| 809 |
+
"cos_to_target": F.cosine_similarity(E, T, dim=-1).mean().item(),
|
| 810 |
+
"self_cos": ss.mean().item(),
|
| 811 |
+
"erank": effective_rank(E),
|
| 812 |
+
"cv": cv_metric(E[:2000].to(DEVICE)),
|
| 813 |
+
"n": int(E.shape[0])}
|
| 814 |
+
out.update({f"frame_{k}": v for k, v in frame_fit_gauge(E, T).items()})
|
| 815 |
+
student.train()
|
| 816 |
+
return out
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 820 |
+
# STAGE 3 β TRAIN (cull-proof)
|
| 821 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 822 |
+
|
| 823 |
+
def vram_probe(cfg, student):
|
| 824 |
+
"""
|
| 825 |
+
One forward+backward at the WORST case (full batch at the pad ceiling) before
|
| 826 |
+
any data is loaded. With bucketing the longest bucket really is a full batch at
|
| 827 |
+
max_tokens, so this is the case that decides whether the run survives -- and it
|
| 828 |
+
is far cheaper to discover here than 20 minutes into a RamStore build.
|
| 829 |
+
"""
|
| 830 |
+
if DEVICE != "cuda":
|
| 831 |
+
return
|
| 832 |
+
line("VRAM PROBE - worst-case batch before spending time on data")
|
| 833 |
+
torch.cuda.empty_cache(); torch.cuda.reset_peak_memory_stats()
|
| 834 |
+
total = torch.cuda.get_device_properties(0).total_memory / 1e9
|
| 835 |
+
ids = torch.randint(1, 30000, (cfg.batch_size, cfg.max_tokens), device=DEVICE)
|
| 836 |
+
am = torch.ones_like(ids)
|
| 837 |
+
tgt = F.normalize(torch.randn(cfg.batch_size, cfg.output_dim, device=DEVICE), dim=-1)
|
| 838 |
+
opt = torch.optim.Adam(student.parameters(), lr=1e-9)
|
| 839 |
+
try:
|
| 840 |
+
student.train()
|
| 841 |
+
with torch.amp.autocast("cuda", enabled=cfg.amp):
|
| 842 |
+
emb = student(ids, am)
|
| 843 |
+
emb = emb.float()
|
| 844 |
+
loss = infonce(emb, tgt, cfg.nce_temperature)[0] + F.mse_loss(emb, tgt)
|
| 845 |
+
loss.backward()
|
| 846 |
+
opt.zero_grad(set_to_none=True)
|
| 847 |
+
peak = torch.cuda.max_memory_allocated() / 1e9
|
| 848 |
+
print(f" batch {cfg.batch_size} x L {cfg.max_tokens} "
|
| 849 |
+
f"(checkpointing={cfg.grad_checkpointing}) -> peak {peak:.1f} GB "
|
| 850 |
+
f"of {total:.1f} GB")
|
| 851 |
+
if peak > 0.85 * total:
|
| 852 |
+
print(" !! within 15% of the limit. Reduce batch_size or max_tokens,")
|
| 853 |
+
print(" !! or set grad_checkpointing=True, before starting the run.")
|
| 854 |
+
else:
|
| 855 |
+
ok = (total - peak)
|
| 856 |
+
print(f" PASS - {ok:.1f} GB headroom")
|
| 857 |
+
except torch.cuda.OutOfMemoryError:
|
| 858 |
+
torch.cuda.empty_cache()
|
| 859 |
+
raise RuntimeError(
|
| 860 |
+
f"VRAM PROBE FAILED at batch {cfg.batch_size} x L {cfg.max_tokens} "
|
| 861 |
+
f"(checkpointing={cfg.grad_checkpointing}). Options, cheapest first: "
|
| 862 |
+
f"grad_checkpointing=True; lower max_tokens (corpus mean is ~48); "
|
| 863 |
+
f"halve batch_size (costs InfoNCE negatives). Nothing was loaded, so "
|
| 864 |
+
f"changing the config and re-running is quick.")
|
| 865 |
+
finally:
|
| 866 |
+
del ids, am, tgt, opt
|
| 867 |
+
torch.cuda.empty_cache(); torch.cuda.reset_peak_memory_stats()
|
| 868 |
+
|
| 869 |
+
|
| 870 |
+
def save_state(cfg, path, student, opt, sched, scaler, step, epoch, chunk_i, order, best):
|
| 871 |
+
torch.save({"model": student.state_dict(), "opt": opt.state_dict(),
|
| 872 |
+
"sched": sched.state_dict(), "scaler": scaler.state_dict(),
|
| 873 |
+
"step": step, "epoch": epoch, "chunk_i": chunk_i, "order": order,
|
| 874 |
+
"best": best, "config": asdict(cfg),
|
| 875 |
+
"rng": {"torch": torch.get_rng_state(), "np": np.random.get_state(),
|
| 876 |
+
"py": random.getstate()}}, path)
|
| 877 |
+
|
| 878 |
+
|
| 879 |
+
def stage3_train(cfg, chunks: List[int], bk: "Backup"):
|
| 880 |
+
from transformers import AutoTokenizer
|
| 881 |
+
line("STAGE 3 β TRAIN")
|
| 882 |
+
P = paths(cfg)
|
| 883 |
+
torch.manual_seed(cfg.seed); np.random.seed(cfg.seed); random.seed(cfg.seed)
|
| 884 |
+
tok = AutoTokenizer.from_pretrained(cfg.ref_hf_name)
|
| 885 |
+
json.dump(asdict(cfg), open(f"{P['config']}/config.json", "w"), indent=2, default=str)
|
| 886 |
+
|
| 887 |
+
student = CaptionEncoder(
|
| 888 |
+
vocab_size=tok.vocab_size, max_len=cfg.max_len, d_model=cfg.d_model,
|
| 889 |
+
n_heads=cfg.n_heads, n_layers=cfg.n_layers, d_ff=cfg.d_ff,
|
| 890 |
+
output_dim=cfg.output_dim, dropout=cfg.dropout,
|
| 891 |
+
pad_token_id=tok.pad_token_id, pooling=cfg.pooling,
|
| 892 |
+
grad_checkpointing=cfg.grad_checkpointing).to(DEVICE)
|
| 893 |
+
n_par = sum(p.numel() for p in student.parameters())
|
| 894 |
+
train_chunks = [c for c in chunks if c not in cfg.holdout_chunks]
|
| 895 |
+
rows = len(train_chunks) * src0(cfg)["chunk_rows"]
|
| 896 |
+
spe = rows // cfg.batch_size
|
| 897 |
+
total = spe * cfg.epochs
|
| 898 |
+
print(f" {cfg.run_name}: {n_par:,} params ({n_par/109_482_240:.2f}x bert-base)")
|
| 899 |
+
print(f" {cfg.n_layers}L {cfg.d_model}d {cfg.n_heads}h ff{cfg.d_ff} pool={cfg.pooling}")
|
| 900 |
+
print(f" {len(train_chunks)} chunks β {rows:,} rows | {spe:,} steps/ep x "
|
| 901 |
+
f"{cfg.epochs} = {total:,} steps @ batch {cfg.batch_size}")
|
| 902 |
+
print(f" loss = {cfg.nce_weight}*InfoNCE(T={cfg.nce_temperature}) + "
|
| 903 |
+
f"{cfg.mse_weight}*MSE + {cfg.cv_weight}*CV [champion consensus_nce_mse]")
|
| 904 |
+
|
| 905 |
+
if cfg.vram_probe:
|
| 906 |
+
vram_probe(cfg, student)
|
| 907 |
+
|
| 908 |
+
opt = torch.optim.Adam(student.parameters(), lr=cfg.lr) # pure Adam, no wd
|
| 909 |
+
sched = torch.optim.lr_scheduler.SequentialLR(
|
| 910 |
+
opt, [torch.optim.lr_scheduler.LinearLR(opt, 0.01, 1.0, cfg.warmup_steps),
|
| 911 |
+
torch.optim.lr_scheduler.CosineAnnealingLR(
|
| 912 |
+
opt, T_max=max(total - cfg.warmup_steps, 1), eta_min=cfg.min_lr)],
|
| 913 |
+
milestones=[cfg.warmup_steps])
|
| 914 |
+
scaler = torch.amp.GradScaler(enabled=cfg.amp and DEVICE == "cuda")
|
| 915 |
+
tb = SummaryWriter(log_dir=f"{P['tb']}/{cfg.run_name}")
|
| 916 |
+
tb.add_text("config", f"```json\n{json.dumps(asdict(cfg), indent=2, default=str)}\n```")
|
| 917 |
+
if os.path.exists(f"{P['maps']}/fit_report.json"):
|
| 918 |
+
tb.add_text("alignment/fit_report",
|
| 919 |
+
f"```json\n{open(f'{P['maps']}/fit_report.json').read()}\n```")
|
| 920 |
+
|
| 921 |
+
step, ep0, chunk_i0, best = 0, 0, 0, -1.0
|
| 922 |
+
order = None
|
| 923 |
+
sp = f"{P['ckpt']}/state.pt"
|
| 924 |
+
if cfg.resume:
|
| 925 |
+
if not os.path.exists(sp):
|
| 926 |
+
bk.pull_latest()
|
| 927 |
+
alt = f"{P['root']}/checkpoints/state.pt"
|
| 928 |
+
if os.path.exists(alt) and alt != sp:
|
| 929 |
+
shutil.copy(alt, sp)
|
| 930 |
+
if os.path.exists(sp):
|
| 931 |
+
st = torch.load(sp, weights_only=False, map_location=DEVICE)
|
| 932 |
+
student.load_state_dict(st["model"]); opt.load_state_dict(st["opt"])
|
| 933 |
+
sched.load_state_dict(st["sched"]); scaler.load_state_dict(st["scaler"])
|
| 934 |
+
step, ep0, chunk_i0, best = st["step"], st["epoch"], st["chunk_i"], st["best"]
|
| 935 |
+
order = st.get("order")
|
| 936 |
+
try:
|
| 937 |
+
torch.set_rng_state(st["rng"]["torch"].cpu())
|
| 938 |
+
np.random.set_state(st["rng"]["np"]); random.setstate(st["rng"]["py"])
|
| 939 |
+
except Exception:
|
| 940 |
+
pass
|
| 941 |
+
print(f" RESUMED at step {step:,} epoch {ep0+1} chunk_i {chunk_i0}")
|
| 942 |
+
|
| 943 |
+
print(" building val store...")
|
| 944 |
+
if cfg.ram_resident:
|
| 945 |
+
val_src = RamStore(cfg, [cfg.holdout_chunks[-1]], tok, tag=" [val]")
|
| 946 |
+
else:
|
| 947 |
+
vds = ChunkPairs(cfg, cfg.holdout_chunks[-1], tok)
|
| 948 |
+
val_src = torch.utils.data.DataLoader(
|
| 949 |
+
vds, batch_size=cfg.batch_size, shuffle=False,
|
| 950 |
+
num_workers=cfg.num_workers, collate_fn=vds.collate)
|
| 951 |
+
|
| 952 |
+
if cfg.ram_resident:
|
| 953 |
+
print(" building train store (one pass, then zero disk I/O)...")
|
| 954 |
+
train_src = RamStore(cfg, train_chunks, tok, tag=" [train]")
|
| 955 |
+
N = len(train_src)
|
| 956 |
+
spe = N // cfg.batch_size
|
| 957 |
+
total = spe * cfg.epochs
|
| 958 |
+
print(f" {N:,} rows resident | {spe:,} steps/ep x {cfg.epochs} = {total:,} steps")
|
| 959 |
+
|
| 960 |
+
t0 = last_ck = time.time()
|
| 961 |
+
for ep in range(ep0, cfg.epochs):
|
| 962 |
+
if cfg.ram_resident:
|
| 963 |
+
# deterministic bucketed plan; chunk_i doubles as the batch index, so a
|
| 964 |
+
# mid-epoch resume regenerates the identical plan and lands on the same batch
|
| 965 |
+
plan = train_src.plan_batches(cfg.batch_size, cfg.seed + ep,
|
| 966 |
+
cfg.bucket_window, cfg.length_bucketing)
|
| 967 |
+
if ep == ep0:
|
| 968 |
+
spe = len(plan); total = spe * cfg.epochs
|
| 969 |
+
bl = np.array([train_src.lens[b].max() for b in plan[:200]])
|
| 970 |
+
print(f" batch plan: {spe:,} batches/epoch | padded length "
|
| 971 |
+
f"p50 {int(np.percentile(bl,50))} p90 {int(np.percentile(bl,90))} "
|
| 972 |
+
f"max {int(bl.max())} (bucketing={cfg.length_bucketing})")
|
| 973 |
+
for ci in range(chunk_i0 if ep == ep0 else 0, len(plan)):
|
| 974 |
+
ids, am, tg = train_src.batch(plan[ci])
|
| 975 |
+
ids = ids.to(DEVICE, non_blocking=True)
|
| 976 |
+
am = am.to(DEVICE, non_blocking=True)
|
| 977 |
+
tgt = F.normalize(tg.to(DEVICE, non_blocking=True).float(), dim=-1)
|
| 978 |
+
with torch.amp.autocast("cuda", enabled=cfg.amp and DEVICE == "cuda"):
|
| 979 |
+
emb = student(ids, am)
|
| 980 |
+
emb = emb.float()
|
| 981 |
+
l_nce, acc = infonce(emb, tgt, cfg.nce_temperature)
|
| 982 |
+
l_mse = F.mse_loss(emb, tgt)
|
| 983 |
+
loss = cfg.nce_weight * l_nce + cfg.mse_weight * l_mse
|
| 984 |
+
l_cv = torch.zeros((), device=emb.device)
|
| 985 |
+
if cfg.cv_weight > 0:
|
| 986 |
+
l_cv = cv_loss(emb, cfg.cv_target)
|
| 987 |
+
loss = loss + cfg.cv_weight * l_cv
|
| 988 |
+
scaler.scale(loss).backward()
|
| 989 |
+
scaler.unscale_(opt)
|
| 990 |
+
gn = torch.nn.utils.clip_grad_norm_(student.parameters(), cfg.grad_clip)
|
| 991 |
+
scaler.step(opt); scaler.update()
|
| 992 |
+
opt.zero_grad(set_to_none=True); sched.step()
|
| 993 |
+
step += 1
|
| 994 |
+
|
| 995 |
+
if step % cfg.log_every == 0:
|
| 996 |
+
lr = opt.param_groups[0]["lr"]
|
| 997 |
+
tb.add_scalar("train/loss", loss.item(), step)
|
| 998 |
+
tb.add_scalar("train/nce", l_nce.item(), step)
|
| 999 |
+
tb.add_scalar("train/mse", l_mse.item(), step)
|
| 1000 |
+
tb.add_scalar("train/cv", float(l_cv), step)
|
| 1001 |
+
tb.add_scalar("train/batch_acc", acc, step)
|
| 1002 |
+
tb.add_scalar("train/lr", lr, step)
|
| 1003 |
+
tb.add_scalar("train/grad_norm", float(gn), step)
|
| 1004 |
+
tb.add_scalar("train/tokens_per_seq", ids.shape[1], step)
|
| 1005 |
+
print(f" e{ep+1} {step:>7,}/{total:,} loss {loss.item():.4f} "
|
| 1006 |
+
f"nce {l_nce.item():.4f} mse {l_mse.item():.5f} acc {acc:.3f} "
|
| 1007 |
+
f"lr {lr:.2e} L{ids.shape[1]} {(time.time()-t0)/60:.0f}m")
|
| 1008 |
+
|
| 1009 |
+
if step % cfg.eval_every == 0:
|
| 1010 |
+
m = evaluate(student, val_src)
|
| 1011 |
+
for k, v in m.items():
|
| 1012 |
+
if isinstance(v, (int, float)):
|
| 1013 |
+
tb.add_scalar(f"val/{k}", v, step)
|
| 1014 |
+
for nm, p in student.named_parameters():
|
| 1015 |
+
if p.grad is not None and ("output_proj" in nm or "token_emb" in nm):
|
| 1016 |
+
tb.add_histogram(f"grad/{nm}", p.grad, step)
|
| 1017 |
+
tb.add_histogram(f"weight/{nm}", p, step)
|
| 1018 |
+
print(f" VAL r1 {m['mimicry_r1']:.4f} cos {m['cos_to_target']:.4f} "
|
| 1019 |
+
f"self_cos {m['self_cos']:+.4f} erank {m['erank']:.1f} "
|
| 1020 |
+
f"cv {m['cv']:.4f} | frame r1 "
|
| 1021 |
+
f"{m.get('frame_r1_after_rotation', float('nan')):.4f}")
|
| 1022 |
+
if m["cos_to_target"] > best:
|
| 1023 |
+
best = m["cos_to_target"]
|
| 1024 |
+
save_state(cfg, f"{P['ckpt']}/best_state.pt", student, opt,
|
| 1025 |
+
sched, scaler, step, ep, ci, order, best)
|
| 1026 |
+
torch.save(student.state_dict(), f"{P['ckpt']}/best_model.pt")
|
| 1027 |
+
|
| 1028 |
+
if (time.time() - last_ck) / 60 >= cfg.ckpt_every_min:
|
| 1029 |
+
save_state(cfg, sp, student, opt, sched, scaler, step, ep, ci, order, best)
|
| 1030 |
+
torch.save(student.state_dict(), f"{P['ckpt']}/model_s{step}.pt")
|
| 1031 |
+
ck = sorted([f for f in os.listdir(P["ckpt"]) if f.startswith("model_s")],
|
| 1032 |
+
key=lambda f: int(f.split("_s")[1].split(".")[0]))
|
| 1033 |
+
for old in ck[:-cfg.keep_local_ckpts]:
|
| 1034 |
+
os.remove(os.path.join(P["ckpt"], old))
|
| 1035 |
+
tb.flush(); bk.push(msg=f"step {step}")
|
| 1036 |
+
last_ck = time.time()
|
| 1037 |
+
else:
|
| 1038 |
+
if order is None or ep != ep0:
|
| 1039 |
+
order = train_chunks[:]; random.shuffle(order)
|
| 1040 |
+
for ci in range(chunk_i0 if ep == ep0 else 0, len(order)):
|
| 1041 |
+
c = order[ci]
|
| 1042 |
+
ds = ChunkPairs(cfg, c, tok)
|
| 1043 |
+
dl = torch.utils.data.DataLoader(
|
| 1044 |
+
ds, batch_size=cfg.batch_size, shuffle=True, drop_last=True,
|
| 1045 |
+
num_workers=cfg.num_workers, collate_fn=ds.collate,
|
| 1046 |
+
pin_memory=(DEVICE == "cuda"))
|
| 1047 |
+
for ids, am, tg in dl:
|
| 1048 |
+
ids = ids.to(DEVICE, non_blocking=True)
|
| 1049 |
+
am = am.to(DEVICE, non_blocking=True)
|
| 1050 |
+
tgt = F.normalize(tg.to(DEVICE, non_blocking=True).float(), dim=-1)
|
| 1051 |
+
with torch.amp.autocast("cuda", enabled=cfg.amp and DEVICE == "cuda"):
|
| 1052 |
+
emb = student(ids, am)
|
| 1053 |
+
emb = emb.float()
|
| 1054 |
+
l_nce, acc = infonce(emb, tgt, cfg.nce_temperature)
|
| 1055 |
+
l_mse = F.mse_loss(emb, tgt)
|
| 1056 |
+
loss = cfg.nce_weight * l_nce + cfg.mse_weight * l_mse
|
| 1057 |
+
l_cv = torch.zeros((), device=emb.device)
|
| 1058 |
+
if cfg.cv_weight > 0:
|
| 1059 |
+
l_cv = cv_loss(emb, cfg.cv_target)
|
| 1060 |
+
loss = loss + cfg.cv_weight * l_cv
|
| 1061 |
+
scaler.scale(loss).backward()
|
| 1062 |
+
scaler.unscale_(opt)
|
| 1063 |
+
gn = torch.nn.utils.clip_grad_norm_(student.parameters(), cfg.grad_clip)
|
| 1064 |
+
scaler.step(opt); scaler.update()
|
| 1065 |
+
opt.zero_grad(set_to_none=True); sched.step()
|
| 1066 |
+
step += 1
|
| 1067 |
+
|
| 1068 |
+
if step % cfg.log_every == 0:
|
| 1069 |
+
lr = opt.param_groups[0]["lr"]
|
| 1070 |
+
tb.add_scalar("train/loss", loss.item(), step)
|
| 1071 |
+
tb.add_scalar("train/nce", l_nce.item(), step)
|
| 1072 |
+
tb.add_scalar("train/mse", l_mse.item(), step)
|
| 1073 |
+
tb.add_scalar("train/cv", float(l_cv), step)
|
| 1074 |
+
tb.add_scalar("train/batch_acc", acc, step)
|
| 1075 |
+
tb.add_scalar("train/lr", lr, step)
|
| 1076 |
+
tb.add_scalar("train/grad_norm", float(gn), step)
|
| 1077 |
+
tb.add_scalar("train/tokens_per_seq", ids.shape[1], step)
|
| 1078 |
+
print(f" e{ep+1} {step:>7,}/{total:,} loss {loss.item():.4f} "
|
| 1079 |
+
f"nce {l_nce.item():.4f} mse {l_mse.item():.5f} acc {acc:.3f} "
|
| 1080 |
+
f"lr {lr:.2e} L{ids.shape[1]} {(time.time()-t0)/60:.0f}m")
|
| 1081 |
+
|
| 1082 |
+
if step % cfg.eval_every == 0:
|
| 1083 |
+
m = evaluate(student, val_src)
|
| 1084 |
+
for k, v in m.items():
|
| 1085 |
+
if isinstance(v, (int, float)):
|
| 1086 |
+
tb.add_scalar(f"val/{k}", v, step)
|
| 1087 |
+
for nm, p in student.named_parameters():
|
| 1088 |
+
if p.grad is not None and ("output_proj" in nm or "token_emb" in nm):
|
| 1089 |
+
tb.add_histogram(f"grad/{nm}", p.grad, step)
|
| 1090 |
+
tb.add_histogram(f"weight/{nm}", p, step)
|
| 1091 |
+
print(f" VAL r1 {m['mimicry_r1']:.4f} cos {m['cos_to_target']:.4f} "
|
| 1092 |
+
f"self_cos {m['self_cos']:+.4f} erank {m['erank']:.1f} "
|
| 1093 |
+
f"cv {m['cv']:.4f} | frame r1 "
|
| 1094 |
+
f"{m.get('frame_r1_after_rotation', float('nan')):.4f}")
|
| 1095 |
+
if m["cos_to_target"] > best:
|
| 1096 |
+
best = m["cos_to_target"]
|
| 1097 |
+
save_state(cfg, f"{P['ckpt']}/best_state.pt", student, opt,
|
| 1098 |
+
sched, scaler, step, ep, ci, order, best)
|
| 1099 |
+
torch.save(student.state_dict(), f"{P['ckpt']}/best_model.pt")
|
| 1100 |
+
|
| 1101 |
+
if (time.time() - last_ck) / 60 >= cfg.ckpt_every_min:
|
| 1102 |
+
save_state(cfg, sp, student, opt, sched, scaler, step, ep, ci, order, best)
|
| 1103 |
+
torch.save(student.state_dict(), f"{P['ckpt']}/model_s{step}.pt")
|
| 1104 |
+
ck = sorted([f for f in os.listdir(P["ckpt"]) if f.startswith("model_s")],
|
| 1105 |
+
key=lambda f: int(f.split("_s")[1].split(".")[0]))
|
| 1106 |
+
for old in ck[:-cfg.keep_local_ckpts]:
|
| 1107 |
+
os.remove(os.path.join(P["ckpt"], old))
|
| 1108 |
+
tb.flush(); bk.push(msg=f"step {step}")
|
| 1109 |
+
last_ck = time.time()
|
| 1110 |
+
del ds, dl; gc.collect()
|
| 1111 |
+
chunk_i0 = 0
|
| 1112 |
+
|
| 1113 |
+
save_state(cfg, sp, student, opt, sched, scaler, step, cfg.epochs, 0, order, best)
|
| 1114 |
+
torch.save(student.state_dict(), f"{P['ckpt']}/final_model.pt")
|
| 1115 |
+
tok.save_pretrained(f"{P['ckpt']}/tokenizer")
|
| 1116 |
+
m = evaluate(student, val_src)
|
| 1117 |
+
line("FINAL")
|
| 1118 |
+
print(f" mimicry R@1 (student->consensus, NOT capability): {m['mimicry_r1']:.4f}")
|
| 1119 |
+
print(f" cos to target : {m['cos_to_target']:.4f}")
|
| 1120 |
+
print(f" self_cos : {m['self_cos']:+.4f} <- isotropy; teachers .81-.98")
|
| 1121 |
+
print(f" effective rank: {m['erank']:.1f}/{cfg.output_dim}")
|
| 1122 |
+
print(f" CV : {m['cv']:.4f}")
|
| 1123 |
+
print(f" frame-fit R@1 : {m.get('frame_r1_after_rotation', float('nan')):.4f} "
|
| 1124 |
+
f"(should be ~mimicry: reference-member alignment pins the frame)")
|
| 1125 |
+
print(" CAPABILITY is decided by STS-B / SICK vs the five teachers, not here.")
|
| 1126 |
+
json.dump({"config": asdict(cfg), "final": m}, open(f"{P['ckpt']}/metrics.json", "w"),
|
| 1127 |
+
indent=2, default=str)
|
| 1128 |
+
tb.flush(); tb.close(); bk.push(force=True, msg="final")
|
| 1129 |
+
return student
|
| 1130 |
+
|
| 1131 |
+
|
| 1132 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1133 |
+
# RUN
|
| 1134 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1135 |
+
|
| 1136 |
+
def run(cfg: BaseConfig = CFG):
|
| 1137 |
+
print("=" * 78)
|
| 1138 |
+
print(f"{cfg.run_name.upper()} β CONSENSUS DISTILLATION, CC12M SCALE")
|
| 1139 |
+
print("=" * 78)
|
| 1140 |
+
paths(cfg)
|
| 1141 |
+
print(f"device={DEVICE} work_dir={cfg.work_dir}")
|
| 1142 |
+
if DEVICE == "cuda":
|
| 1143 |
+
print(f"gpu={torch.cuda.get_device_name()} "
|
| 1144 |
+
f"vram={torch.cuda.get_device_properties(0).total_memory/1e9:.0f}GB")
|
| 1145 |
+
miss = src0(cfg).get("missing", {})
|
| 1146 |
+
print(f"chunks: {len(usable_chunks(cfg))} train + {len(cfg.holdout_chunks)} holdout "
|
| 1147 |
+
f"| excluded for missing experts: {miss}")
|
| 1148 |
+
if not cfg.require_all_experts:
|
| 1149 |
+
print(" !! require_all_experts=False -> 4-expert consensus on some chunks.")
|
| 1150 |
+
print(" !! The target definition then differs BETWEEN chunks. Discouraged.")
|
| 1151 |
+
bk = Backup(cfg)
|
| 1152 |
+
|
| 1153 |
+
if cfg.run_stage0:
|
| 1154 |
+
cfg.caption_field = stage0_parity(cfg)
|
| 1155 |
+
elif cfg.caption_field is None:
|
| 1156 |
+
raise RuntimeError("caption_field is None and stage 0 is disabled.")
|
| 1157 |
+
|
| 1158 |
+
maps = stage1_fit(cfg, bk) if cfg.run_stage1 else torch.load(
|
| 1159 |
+
f"{paths(cfg)['maps']}/alignment_maps.pt", weights_only=False)
|
| 1160 |
+
chunks = stage2_targets(cfg, maps, bk) if cfg.run_stage2 else sorted(
|
| 1161 |
+
set(usable_chunks(cfg)) | set(cfg.holdout_chunks))
|
| 1162 |
+
if cfg.run_stage3:
|
| 1163 |
+
return stage3_train(cfg, chunks, bk)
|
| 1164 |
+
|
| 1165 |
+
|
| 1166 |
+
if "get_ipython" in globals() or __name__ == "__main__":
|
| 1167 |
+
STUDENT = run(CFG)
|