abdoelsayed commited on
Commit
a8a3a65
·
verified ·
1 Parent(s): e727cd3

v0.1: fix 2_Dense use_residual

Browse files
Files changed (40) hide show
  1. .gitattributes +1 -0
  2. .gitignore +6 -0
  3. 1_Dense/config.json +1 -0
  4. 1_Dense/model.safetensors +3 -0
  5. 2_Dense/config.json +1 -0
  6. 2_Dense/model.safetensors +3 -0
  7. 3_Dense/config.json +1 -0
  8. 3_Dense/model.safetensors +3 -0
  9. README.md +133 -0
  10. config.json +50 -0
  11. config_sentence_transformers.json +21 -0
  12. evaluation/evaluate_bright.py +470 -0
  13. logo_reason_mxai.png +3 -0
  14. model.safetensors +3 -0
  15. modules.json +26 -0
  16. results/BrightRetrieval_aops_evaluation_scores_qlen256.json +8 -0
  17. results/BrightRetrieval_biology_evaluation_scores_qlen256.json +8 -0
  18. results/BrightRetrieval_earth_science_evaluation_scores_qlen256.json +8 -0
  19. results/BrightRetrieval_economics_evaluation_scores_qlen256.json +8 -0
  20. results/BrightRetrieval_leetcode_evaluation_scores_qlen256.json +8 -0
  21. results/BrightRetrieval_pony_evaluation_scores_qlen32.json +8 -0
  22. results/BrightRetrieval_psychology_evaluation_scores_qlen256.json +8 -0
  23. results/BrightRetrieval_robotics_evaluation_scores_qlen256.json +8 -0
  24. results/BrightRetrieval_stackoverflow_evaluation_scores_qlen256.json +8 -0
  25. results/BrightRetrieval_sustainable_living_evaluation_scores_qlen256.json +8 -0
  26. results/BrightRetrieval_theoremqa_questions_evaluation_scores_qlen256.json +8 -0
  27. results/BrightRetrieval_theoremqa_theorems_evaluation_scores_qlen256.json +8 -0
  28. results/run_meta.json +16 -0
  29. results/summary.json +18 -0
  30. sentence_bert_config.json +4 -0
  31. special_tokens_map.json +31 -0
  32. tokenizer.json +0 -0
  33. tokenizer_config.json +968 -0
  34. training/launch_train_multinode.py +126 -0
  35. training/materialize_bge_reasoner.py +149 -0
  36. training/materialize_reasonir.py +108 -0
  37. training/merge_datasets.py +168 -0
  38. training/train_fixed_stage1.sbatch +133 -0
  39. training/train_fixed_stage2.sbatch +128 -0
  40. training/widen_colbert_projection.py +262 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ logo_reason_mxai.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ training/
2
+ .DS_Store
3
+ .idea/
4
+ .vscode/
5
+ __pycache__/
6
+ *.pyc
1_Dense/config.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"in_features": 384, "out_features": 768, "bias": false, "activation_function": "torch.nn.modules.linear.Identity", "use_residual": false}
1_Dense/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fd2aa41a2974566861ff9690be4f4c2b9a917ee0f97e338911bd66fa06dafddd
3
+ size 589912
2_Dense/config.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"in_features": 768, "out_features": 768, "bias": false, "activation_function": "torch.nn.modules.linear.Identity", "use_residual": true}
2_Dense/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec4fecf007916948b029915b76dbf37efd5241a21a497dfb4f2abe622410b55a
3
+ size 1179736
3_Dense/config.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"in_features": 768, "out_features": 128, "bias": false, "activation_function": "torch.nn.modules.linear.Identity", "use_residual": false}
3_Dense/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c99e270c45b0db9b5ffbf5e0de10ba729680aa778807b2329189b682d686203d
3
+ size 196696
README.md ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - en
4
+ tags:
5
+ - ColBERT
6
+ - PyLate
7
+ - sentence-transformers
8
+ - sentence-similarity
9
+ - feature-extraction
10
+ - late-interaction
11
+ - reasoning-retrieval
12
+ - edge
13
+ - generated_from_trainer
14
+ - loss:CachedContrastive
15
+ base_model: mixedbread-ai/mxbai-edge-colbert-v0-32m
16
+ datasets:
17
+ - hanhainebula/bge-reasoner-data
18
+ - reasonir/reasonir-data
19
+ pipeline_tag: sentence-similarity
20
+ library_name: PyLate
21
+ license: cc-by-nc-4.0
22
+ ---
23
+
24
+ <img src="./logo_reason_mxai.png" width="500" height="auto">
25
+
26
+ # Reason-mxbai-colbert-v0.1-32m
27
+
28
+ **v0.1** of the Reason-mxbai-colbert series — same edge-scale late-interaction retriever as v0, **retrained with the correct projection-head architecture** (`use_residual: true` in the 2_Dense layer). Mean BRIGHT nDCG@10 improves from **19.00 → 19.61** (+0.61), with the largest gains on the natural-language and formal-math splits that v0 struggled with.
29
+
30
+ ## What changed vs v0
31
+
32
+ The v0 release inherited `use_residual: false` in the 2_Dense (768→768) layer from the upstream `mixedbread-ai/mxbai-edge-colbert-v0-32m` config. That turned out to be a shipped-config bug — the base weights were trained with a residual connection on 2_Dense, but the config flag said otherwise. PyLate respects the flag, so every downstream fine-tune was working with a silently-broken architecture.
33
+
34
+ v0.1 fixes this by:
35
+ 1. Copying the base model and flipping `2_Dense/config.json: "use_residual": true`.
36
+ 2. Re-widening the 3_Dense projection from 64 → 128 dims on top of the corrected base (small-random init for the new channels).
37
+ 3. Retraining the full curriculum (Stage 1 VL warmup → Stage 2 BGE-reasoner + HQ-hn polish) on the corrected architecture.
38
+
39
+ Under the corrected architecture, stage-2 training loss drops from **38.73 → 31.71** (−7.02) — real learning. The v0 equivalent run under the broken architecture was stuck at loss ~45-51 and never moved the new-dim weights.
40
+
41
+ ## BRIGHT results
42
+
43
+ Evaluated with MTEB `BrightRetrieval` (brute-force MaxSim, `query_length=256`, `document_length=2048`).
44
+
45
+ | Split | v0 | **v0.1** | Δ v0 | Reason-MCB (150M, paper) |
46
+ |---|---:|---:|---:|---:|
47
+ | aops | 5.05 | 4.89 | −0.16 | 9.17 |
48
+ | biology | 32.71 | **33.16** | +0.45 | 33.25 |
49
+ | earth_science | 43.88 | **44.28** | +0.40 | 41.02 |
50
+ | economics | 18.70 | **20.25** | **+1.54** | 24.93 |
51
+ | leetcode | 17.67 | 17.40 | −0.28 | 31.07 |
52
+ | pony | 20.73 | **22.77** | **+2.03** | 8.51 |
53
+ | psychology | 22.62 | **24.91** | **+2.29** | 30.73 |
54
+ | robotics | 18.43 | **18.65** | +0.23 | 21.12 |
55
+ | stackoverflow | 16.78 | 16.66 | −0.12 | 20.62 |
56
+ | sustainable_living | 20.77 | 20.11 | −0.66 | 20.31 |
57
+ | theoremqa_questions | 8.38 | **9.04** | **+0.66** | 19.51 |
58
+ | theoremqa_theorems | 2.25 | **3.19** | **+0.94** | 11.24 |
59
+ | **Full mean** | **19.00** | **19.61** | **+0.61** | **22.62** |
60
+
61
+ **8 wins / 4 losses.** Highlights:
62
+ - **Natural-language splits** (psychology +2.29, economics +1.54, pony +2.03) — the correct architecture lets the model learn meaningfully richer representations.
63
+ - **Formal-math splits** (theoremqa_theorems +0.94, theoremqa_questions +0.66) — previously stuck near the floor, now moving.
64
+ - **Small code-split regressions** (leetcode −0.28, stackoverflow −0.12) are within noise.
65
+
66
+ ## Model Details
67
+
68
+ - **Model Type:** PyLate ColBERT (late-interaction, multi-vector)
69
+ - **Base model:** [mixedbread-ai/mxbai-edge-colbert-v0-32m](https://huggingface.co/mixedbread-ai/mxbai-edge-colbert-v0-32m) (with `2_Dense/use_residual: true` patched to match trained weights)
70
+ - **Parameters:** ~32M backbone + widened 128-dim projection
71
+ - **Document Length (training):** 2048 tokens
72
+ - **Query Length (training):** 256 tokens
73
+ - **Output Dimensionality:** 128 per token (widened from base's 64)
74
+ - **Similarity Function:** MaxSim
75
+ - **Training Data:** `hanhainebula/bge-reasoner-data` + `reasonir/reasonir-data` (VL split for warmup, HQ with hard negatives for polish)
76
+ - **Language:** en
77
+ - **License:** CC-BY-NC-4.0
78
+
79
+ ## Model Architecture
80
+
81
+ ```
82
+ ColBERT(
83
+ (0): Transformer({'max_seq_length': 127, 'do_lower_case': True}) with ModernBertModel
84
+ hidden_size=384, num_hidden_layers=10, num_attention_heads=6,
85
+ position_embedding_type='sans_pos', max_position_embeddings=7999
86
+ (1): Dense(384 → 768, bias=False, use_residual=False)
87
+ (2): Dense(768 → 768, bias=False, use_residual=True) # ← THE FIX (was False in v0)
88
+ (3): Dense(768 → 128, bias=False, use_residual=False) # widened from 64 → 128 on correct base
89
+ )
90
+ ```
91
+
92
+ ## Usage
93
+
94
+ ```python
95
+ from pylate import indexes, models, retrieve
96
+
97
+ model = models.ColBERT(model_name_or_path="<hf_user>/Reason-mxbai-colbert-v0.1-32m")
98
+
99
+ # Retrieval (see https://lightonai.github.io/pylate/ for full API)
100
+ index = indexes.Voyager(index_folder="pylate-index", index_name="index", override=True)
101
+ docs = ["document 1 text", "document 2 text"]
102
+ doc_embs = model.encode(docs, is_query=False, batch_size=32, show_progress_bar=True)
103
+ index.add_documents(documents_ids=["1","2"], documents_embeddings=doc_embs)
104
+
105
+ retriever = retrieve.ColBERT(index=index)
106
+ q_embs = model.encode(
107
+ ["Given a Psychology post, retrieve relevant passages that help answer the post.\nQuery: why do I procrastinate?"],
108
+ is_query=True,
109
+ )
110
+ scores = retriever.retrieve(queries_embeddings=q_embs, k=10)
111
+ ```
112
+
113
+ ## Training
114
+
115
+ See `training/` (not uploaded to the HF repo — see `training/README.md` for the full reproduction recipe).
116
+
117
+ Pipeline (2 nodes × 4 H100):
118
+
119
+ 1. **Widen** `mixedbread-ai/mxbai-edge-colbert-v0-32m` (patched `2_Dense/use_residual: true`) from 64 → 128-dim projection, small-random init.
120
+ 2. **Stage 1 (VL warmup):** 1 epoch on `reasonir-vl`, lr=1e-5, global batch 2048, ~30 min.
121
+ 3. **Stage 2 (BGE + HQ-hn polish):** 1 epoch on merged `bge-reasoner + reasonir-hq-hn` (~2.7M triples), lr=5e-6, ~2h.
122
+
123
+ Key training-infra fixes already baked into this release:
124
+ - `max_grad_norm=10000` (default `1.0` over-clips cached-contrastive's summed gradients; original v0 training silently froze).
125
+ - `use_residual: true` in 2_Dense (matches base weights).
126
+
127
+ ## Acknowledgements
128
+
129
+ Thanks to Antoine Chaffin (LightOn / Reason-ModernColBERT author) for flagging the upstream 2_Dense config bug in `mxbai-edge-colbert-v0-32m` — without that heads-up, v0.1 wouldn't exist.
130
+
131
+ ## License
132
+
133
+ CC-BY-NC-4.0 (inherited from ReasonIR / BGE-reasoner data).
config.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_name_or_path": "/data/fs201059/aa17626/modernTransformer/repro_reason_moderncolbert/output_edge32m_d128_fixed/stage-vl/final",
3
+ "architectures": [
4
+ "ModernBertModel"
5
+ ],
6
+ "attention_bias": false,
7
+ "attention_dropout": 0.0,
8
+ "bos_token_id": 50281,
9
+ "causal_mask": false,
10
+ "classifier_activation": "gelu",
11
+ "classifier_bias": false,
12
+ "classifier_dropout": 0.0,
13
+ "classifier_pooling": "mean",
14
+ "cls_token_id": 50281,
15
+ "decoder_bias": true,
16
+ "deterministic_flash_attn": false,
17
+ "dtype": "float32",
18
+ "embedding_dropout": 0.0,
19
+ "eos_token_id": 50282,
20
+ "global_attn_every_n_layers": 3,
21
+ "global_rope_theta": 160000.0,
22
+ "gradient_checkpointing": false,
23
+ "hidden_activation": "gelu",
24
+ "hidden_size": 384,
25
+ "initializer_cutoff_factor": 2.0,
26
+ "initializer_range": 0.02,
27
+ "intermediate_size": 576,
28
+ "is_causal": false,
29
+ "layer_norm_eps": 1e-05,
30
+ "local_attention": 128,
31
+ "local_rope_theta": 160000.0,
32
+ "max_position_embeddings": 7999,
33
+ "mlp_bias": false,
34
+ "mlp_dropout": 0.0,
35
+ "model_type": "modernbert",
36
+ "norm_bias": false,
37
+ "norm_eps": 1e-05,
38
+ "num_attention_heads": 6,
39
+ "num_hidden_layers": 10,
40
+ "pad_token_id": 50283,
41
+ "position_embedding_type": "sans_pos",
42
+ "reference_compile": false,
43
+ "repad_logits_with_grad": false,
44
+ "sep_token_id": 50282,
45
+ "sparse_pred_ignore_index": -100,
46
+ "sparse_prediction": false,
47
+ "torch_dtype": "bfloat16",
48
+ "transformers_version": "4.48.2",
49
+ "vocab_size": 50370
50
+ }
config_sentence_transformers.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "ColBERT",
3
+ "__version__": {
4
+ "sentence_transformers": "4.0.2",
5
+ "transformers": "4.48.2",
6
+ "pytorch": "2.5.1"
7
+ },
8
+ "prompts": {
9
+ "query": "",
10
+ "document": ""
11
+ },
12
+ "default_prompt_name": null,
13
+ "similarity_fn_name": "MaxSim",
14
+ "query_prefix": "[Q] ",
15
+ "document_prefix": "[D] ",
16
+ "query_length": 256,
17
+ "document_length": 2048,
18
+ "attend_to_expansion_tokens": false,
19
+ "skiplist_words": [],
20
+ "do_query_expansion": false
21
+ }
evaluation/evaluate_bright.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Official BRIGHT evaluation for LaRLI / ColBERT models.
2
+
3
+ Uses the same protocol as the Reason-ModernColBERT authors:
4
+ - MTEB BrightRetrieval task for data loading
5
+ - Brute-force MaxSim scoring (exact, no approximation)
6
+ - Filters excluded_ids from scores
7
+ - query_length=256 (except pony=32)
8
+
9
+ Output layout
10
+ -------------
11
+ By default each run writes into a unique directory:
12
+ BRIGHT_scores_<label>[__<run_name>]__<YYYYMMDD-HHMMSS>/
13
+ where <label> is derived from the model path (HF ids become "org__name";
14
+ local paths use the last meaningful components so ".../final" directories
15
+ don't collide). This avoids the old behaviour of silently reloading cached
16
+ per-split scores from a stale dir.
17
+
18
+ Key flags:
19
+ - --run_name LABEL : extra label appended to the auto dir name
20
+ - --no_timestamp : drop the timestamp suffix (old stable-path behaviour)
21
+ - --overwrite : recompute a split even if its JSON already exists
22
+ - --output_dir PATH : override the auto dir entirely
23
+ - --output_root DIR : parent dir for the auto dir (default: CWD)
24
+
25
+ Usage:
26
+ # Default: auto dir with timestamp (will never overwrite a prior run)
27
+ python LaRLI/evaluate_bright.py \
28
+ --model_path $PWD/repro_reason_moderncolbert/output_edge17m_v2a/pos4_D/stage-reason-embed/final \
29
+ --model_version baseline \
30
+ --run_name v2a
31
+
32
+ # Pin a stable directory (re-runs reuse per-split cache unless --overwrite)
33
+ python LaRLI/evaluate_bright.py --model_path lightonai/Reason-ModernColBERT \
34
+ --model_version baseline --run_name ref --no_timestamp
35
+
36
+ # PLAID index mode
37
+ python LaRLI/evaluate_bright.py --model_path lightonai/Reason-ModernColBERT \
38
+ --model_version baseline --use_plaid
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import argparse
44
+ import datetime as _dt
45
+ import gc
46
+ import os
47
+ import re
48
+ import sys
49
+
50
+ import numpy as np
51
+ import torch
52
+
53
+ _project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
54
+ sys.path.insert(0, _project_root)
55
+ sys.path.insert(0, os.path.join(_project_root, "pylate"))
56
+
57
+ import mteb
58
+ import srsly
59
+
60
+ from pylate import evaluation, models
61
+
62
+
63
+ _UNINFORMATIVE_TAIL = {"final", "checkpoint-last", ""}
64
+ _SANITIZE_RE = re.compile(r"[^A-Za-z0-9._-]+")
65
+
66
+
67
+ def derive_model_label(model_path: str) -> str:
68
+ """Derive a human-readable label from a model path.
69
+
70
+ Goals:
71
+ - Huggingface refs like "org/model" -> "org__model"
72
+ - Local paths like ".../output_edge17m_v2a/pos4_D/stage-reason-embed/final"
73
+ -> "pos4_D__stage-reason-embed__final" so "final" never stands alone.
74
+ - Always filesystem-safe (no slashes, no weird chars).
75
+ """
76
+ p = str(model_path).rstrip("/")
77
+ if not p:
78
+ return "unknown"
79
+
80
+ # HF-style "org/name" (not an absolute local path)
81
+ if not p.startswith(("/", ".")) and p.count("/") == 1:
82
+ label = p.replace("/", "__")
83
+ return _SANITIZE_RE.sub("-", label)
84
+
85
+ parts = [seg for seg in p.split("/") if seg]
86
+ # Walk from the tail; collect segments until we've seen at least two
87
+ # *informative* ones (i.e. not "final" / "checkpoint-*"). This keeps the
88
+ # run-identifying parent in the label so
89
+ # .../output_edge17m_v2a/pos4_D/stage-reason-embed/final
90
+ # becomes 'pos4_D__stage-reason-embed__final', not just '…__final'.
91
+ collected: list[str] = []
92
+ informative_seen = 0
93
+ for seg in reversed(parts):
94
+ collected.append(seg)
95
+ is_uninformative = seg in _UNINFORMATIVE_TAIL or seg.startswith("checkpoint-")
96
+ if not is_uninformative:
97
+ informative_seen += 1
98
+ if informative_seen >= 2:
99
+ break
100
+ label = "__".join(reversed(collected)) if collected else parts[-1]
101
+ return _SANITIZE_RE.sub("-", label)
102
+
103
+
104
+ def build_default_output_dir(model_path: str, run_name: str | None,
105
+ with_timestamp: bool, root: str) -> str:
106
+ label = derive_model_label(model_path)
107
+ if run_name:
108
+ label = f"{label}__{_SANITIZE_RE.sub('-', run_name)}"
109
+ if with_timestamp:
110
+ stamp = _dt.datetime.now().strftime("%Y%m%d-%H%M%S")
111
+ label = f"{label}__{stamp}"
112
+ return os.path.join(root, f"BRIGHT_scores_{label}")
113
+
114
+
115
+ def load_model(model_path: str, model_version: str, query_length: int, document_length: int | None = None):
116
+ """Load a ColBERT or LaRLI model."""
117
+ if model_version == "baseline":
118
+ kwargs = {"model_name_or_path": model_path, "query_length": query_length}
119
+ if document_length is not None:
120
+ kwargs["document_length"] = document_length
121
+ return models.ColBERT(**kwargs)
122
+ elif model_version == "v7":
123
+ from LaRLI.models import LaRLIColBERTv7
124
+ return LaRLIColBERTv7(
125
+ model_name_or_path=model_path,
126
+ num_heads=4,
127
+ query_length=query_length,
128
+ )
129
+ elif model_version == "v6":
130
+ from LaRLI.models import LaRLIColBERTv6
131
+ return LaRLIColBERTv6(
132
+ model_name_or_path=model_path,
133
+ num_reasoning_tokens=8,
134
+ query_length=query_length,
135
+ )
136
+ elif model_version == "v5":
137
+ from LaRLI.models import LaRLIColBERTv5
138
+ return LaRLIColBERTv5(
139
+ model_name_or_path=model_path,
140
+ num_reasoning_tokens=32,
141
+ query_length=query_length,
142
+ )
143
+ elif model_version == "v4":
144
+ from LaRLI.models import LaRLIColBERTv4
145
+ return LaRLIColBERTv4(
146
+ model_name_or_path=model_path,
147
+ num_reasoning_tokens=32,
148
+ query_length=query_length,
149
+ )
150
+ elif model_version == "v3":
151
+ from LaRLI.models import LaRLIColBERTv3
152
+ return LaRLIColBERTv3(
153
+ model_name_or_path=model_path,
154
+ num_reasoning_tokens=32,
155
+ query_length=query_length,
156
+ )
157
+ else:
158
+ raise ValueError(f"Unknown model version: {model_version}")
159
+
160
+
161
+ def bruteforce_retrieve(
162
+ query_embeddings: list,
163
+ doc_embeddings: list,
164
+ doc_ids: list[str],
165
+ k: int = 100,
166
+ doc_chunk_size: int = 512,
167
+ ) -> list[list[dict]]:
168
+ """Brute-force MaxSim retrieval. One query at a time, docs in chunks."""
169
+ all_scores = []
170
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
171
+ n_docs = len(doc_embeddings)
172
+ dim = doc_embeddings[0].shape[1]
173
+
174
+ # Pre-pad documents and build tensor on CPU
175
+ max_doc_len = max(d.shape[0] for d in doc_embeddings)
176
+ doc_tensor = torch.zeros(n_docs, max_doc_len, dim, dtype=torch.float16)
177
+ doc_mask = torch.zeros(n_docs, max_doc_len, dtype=torch.bool)
178
+ for i, d in enumerate(doc_embeddings):
179
+ doc_len = d.shape[0]
180
+ doc_tensor[i, :doc_len] = torch.as_tensor(d, dtype=torch.float16)
181
+ doc_mask[i, :doc_len] = True
182
+
183
+ for qi in range(len(query_embeddings)):
184
+ if qi % 10 == 0:
185
+ print(f" Scoring query {qi+1}/{len(query_embeddings)}...", end="\r")
186
+
187
+ q = torch.as_tensor(query_embeddings[qi], dtype=torch.float16).to(device) # [Q, D]
188
+ query_scores = torch.zeros(n_docs, dtype=torch.float32)
189
+
190
+ for d_start in range(0, n_docs, doc_chunk_size):
191
+ d_end = min(d_start + doc_chunk_size, n_docs)
192
+ d_gpu = doc_tensor[d_start:d_end].to(device) # [C, L, D]
193
+ m_gpu = doc_mask[d_start:d_end].to(device) # [C, L]
194
+
195
+ # [Q, D] x [C, L, D]^T -> [Q, C, L] via matmul
196
+ # q: [Q, D], d_gpu: [C, L, D] -> d_gpu.permute(0,2,1): [C, D, L]
197
+ # sim[c] = q @ d_gpu[c].T -> [Q, L] for each doc c
198
+ sim = torch.einsum("qd,cld->qcl", q, d_gpu) # [Q, C, L]
199
+ sim.masked_fill_(~m_gpu.unsqueeze(0), -1e4) # mask padding
200
+ max_sim = sim.max(dim=-1).values # [Q, C]
201
+ chunk_scores = max_sim.sum(dim=0) # [C]
202
+ query_scores[d_start:d_end] = chunk_scores.float().cpu()
203
+
204
+ del d_gpu, m_gpu, sim, max_sim, chunk_scores
205
+
206
+ # Top-k
207
+ topk_vals, topk_idxs = query_scores.topk(min(k, n_docs))
208
+ query_results = [
209
+ {"id": doc_ids[idx], "score": val}
210
+ for val, idx in zip(topk_vals.tolist(), topk_idxs.tolist())
211
+ ]
212
+ all_scores.append(query_results)
213
+
214
+ print()
215
+ return all_scores
216
+
217
+
218
+ def main():
219
+ parser = argparse.ArgumentParser(description="BRIGHT evaluation (official protocol)")
220
+ parser.add_argument("--model_path", type=str, required=True)
221
+ parser.add_argument("--model_version", type=str, default="baseline",
222
+ choices=["baseline", "v3", "v4", "v5", "v6", "v7"])
223
+ parser.add_argument("--query_length", type=int, default=256,
224
+ help="Default query length (256 for all splits except pony=32)")
225
+ parser.add_argument("--pony_query_length", type=int, default=32,
226
+ help="Query length for the pony split")
227
+ parser.add_argument("--document_length", type=int, default=None,
228
+ help="Cap document token length during encoding. Default: use model config "
229
+ "(usually 8192). Set lower (e.g. 2048) to avoid CPU-RAM OOM on "
230
+ "large-corpus splits when the model has a wide projection dim.")
231
+ parser.add_argument("--doc_batch_size", type=int, default=100)
232
+ parser.add_argument("--query_batch_size", type=int, default=32)
233
+ parser.add_argument("--output_dir", type=str, default=None,
234
+ help="Output directory for scores. If not set, a dir "
235
+ "'BRIGHT_scores_<label>[__<run_name>]__<timestamp>' "
236
+ "is auto-generated under --output_root so each run "
237
+ "is isolated. Pass an explicit path to override.")
238
+ parser.add_argument("--output_root", type=str, default=".",
239
+ help="Parent dir for auto-generated --output_dir. Default: CWD.")
240
+ parser.add_argument("--run_name", type=str, default=None,
241
+ help="Optional extra label appended to the auto dir name "
242
+ "(e.g. 'v2a' or 'polish-only').")
243
+ parser.add_argument("--no_timestamp", action="store_true",
244
+ help="Disable the timestamp suffix in the auto dir name. "
245
+ "With --run_name and --no_timestamp you'll get a "
246
+ "stable path across runs (old behaviour).")
247
+ parser.add_argument("--overwrite", action="store_true",
248
+ help="Re-compute per-split results even if the JSON "
249
+ "already exists in --output_dir (default is to "
250
+ "load-and-skip).")
251
+ parser.add_argument("--splits", nargs="+", default=None,
252
+ help="Specific splits to evaluate (default: all)")
253
+ parser.add_argument("--use_plaid", action="store_true",
254
+ help="Use PLAID index instead of brute-force (needs more GPU memory)")
255
+ parser.add_argument("--nbits", type=int, default=4,
256
+ help="PLAID index nbits (only with --use_plaid)")
257
+ parser.add_argument("--retrieve_k", type=int, default=100,
258
+ help="Number of documents to retrieve per query")
259
+ args = parser.parse_args()
260
+
261
+ # Load BRIGHT task via MTEB
262
+ print("Loading BrightRetrieval task...")
263
+ tasks = mteb.get_tasks(tasks=["BrightRetrieval"])
264
+ tasks[0].load_data()
265
+
266
+ model_name = args.model_path.rstrip("/")
267
+ short_name = derive_model_label(model_name)
268
+
269
+ if args.output_dir is None:
270
+ args.output_dir = build_default_output_dir(
271
+ model_path=args.model_path,
272
+ run_name=args.run_name,
273
+ with_timestamp=not args.no_timestamp,
274
+ root=args.output_root,
275
+ )
276
+
277
+ os.makedirs(args.output_dir, exist_ok=True)
278
+
279
+ # Write a small run-manifest so you can later remember what a dir contains.
280
+ srsly.write_json(
281
+ os.path.join(args.output_dir, "run_meta.json"),
282
+ {
283
+ "model_path": args.model_path,
284
+ "model_version": args.model_version,
285
+ "query_length": args.query_length,
286
+ "pony_query_length": args.pony_query_length,
287
+ "use_plaid": args.use_plaid,
288
+ "nbits": args.nbits if args.use_plaid else None,
289
+ "retrieve_k": args.retrieve_k,
290
+ "overwrite": args.overwrite,
291
+ "run_name": args.run_name,
292
+ "label": short_name,
293
+ "started_at": _dt.datetime.now().isoformat(timespec="seconds"),
294
+ "splits_requested": args.splits,
295
+ },
296
+ )
297
+
298
+ all_splits = list(tasks[0].queries.keys())
299
+ eval_splits = args.splits if args.splits else all_splits
300
+ print(f"Evaluating {len(eval_splits)} splits: {eval_splits}")
301
+ print(f"Mode: {'PLAID index' if args.use_plaid else 'brute-force MaxSim'}")
302
+ print(f"Output dir: {args.output_dir}")
303
+ if args.overwrite:
304
+ print("Overwrite: ON — existing per-split JSONs will be recomputed.")
305
+
306
+ all_results = {}
307
+
308
+ for eval_set in eval_splits:
309
+ # Use query_length=32 for pony, 256 for everything else
310
+ if "pony" in eval_set:
311
+ query_length = args.pony_query_length
312
+ else:
313
+ query_length = args.query_length
314
+
315
+ print(f"\n{'='*60}")
316
+ print(f"Split: {eval_set} (query_length={query_length})")
317
+ print(f"{'='*60}")
318
+
319
+ # Check if results already exist
320
+ out_file = os.path.join(
321
+ args.output_dir,
322
+ f"BrightRetrieval_{eval_set.replace('/', '_')}_evaluation_scores_qlen{query_length}.json",
323
+ )
324
+ if os.path.exists(out_file) and not args.overwrite:
325
+ print(f"Results already exist at {out_file}. Loading "
326
+ f"(pass --overwrite to recompute).")
327
+ existing = srsly.read_json(out_file)
328
+ all_results[eval_set] = existing
329
+ ndcg10 = existing.get("ndcg@10", "N/A")
330
+ print(f" nDCG@10 = {ndcg10}")
331
+ continue
332
+ if os.path.exists(out_file) and args.overwrite:
333
+ print(f"--overwrite set; recomputing {eval_set} (old file at {out_file}).")
334
+
335
+ # Load model with appropriate query_length (and optional document_length cap)
336
+ model = load_model(args.model_path, args.model_version, query_length,
337
+ document_length=args.document_length)
338
+
339
+ corpus = tasks[0].corpus[eval_set]["standard"]
340
+ queries = tasks[0].queries[eval_set]["standard"]
341
+ doc_ids = list(corpus.keys())
342
+ doc_texts = list(corpus.values())
343
+ query_ids = list(queries.keys())
344
+ query_texts = list(queries.values())
345
+
346
+ # Encode documents
347
+ print(f"Encoding {len(doc_texts)} documents...")
348
+ documents_embeddings = model.encode(
349
+ sentences=doc_texts,
350
+ batch_size=args.doc_batch_size,
351
+ is_query=False,
352
+ show_progress_bar=True,
353
+ )
354
+
355
+ # Encode queries
356
+ print(f"Encoding {len(query_texts)} queries...")
357
+ queries_embeddings = model.encode(
358
+ sentences=query_texts,
359
+ is_query=True,
360
+ show_progress_bar=True,
361
+ batch_size=args.query_batch_size,
362
+ )
363
+
364
+ if args.use_plaid:
365
+ from pylate import indexes, retrieve
366
+
367
+ index = indexes.PLAID(
368
+ override=True,
369
+ nbits=args.nbits,
370
+ index_name=f"BrightRetrieval_{eval_set}_{short_name}_{query_length}_{args.nbits}bits",
371
+ )
372
+ retriever = retrieve.ColBERT(index=index)
373
+
374
+ index.add_documents(
375
+ documents_ids=doc_ids,
376
+ documents_embeddings=documents_embeddings,
377
+ )
378
+ scores = retriever.retrieve(
379
+ queries_embeddings=queries_embeddings, k=args.retrieve_k
380
+ )
381
+ else:
382
+ # Brute-force MaxSim (exact, memory-efficient)
383
+ print("Computing brute-force MaxSim scores...")
384
+ scores = bruteforce_retrieve(
385
+ query_embeddings=queries_embeddings,
386
+ doc_embeddings=documents_embeddings,
387
+ doc_ids=doc_ids,
388
+ k=args.retrieve_k,
389
+ )
390
+
391
+ # Filter excluded_ids (official protocol)
392
+ rel_docs = tasks[0].relevant_docs[eval_set]
393
+ has_excluded = "excluded" in rel_docs
394
+ qrels = rel_docs["standard"] if "standard" in rel_docs else rel_docs
395
+
396
+ if has_excluded:
397
+ excluded_map = rel_docs["excluded"]
398
+ filtered_scores = []
399
+ for query_scores, excluded_ids in zip(
400
+ scores, excluded_map.values()
401
+ ):
402
+ if excluded_ids == "N/A" or not excluded_ids:
403
+ filtered_scores.append(query_scores)
404
+ continue
405
+ filtered_query_scores = [
406
+ qs for qs in query_scores if qs["id"] not in excluded_ids
407
+ ]
408
+ filtered_scores.append(filtered_query_scores)
409
+ else:
410
+ filtered_scores = scores
411
+ print(" (no excluded_ids found, skipping filtering)")
412
+
413
+ # Evaluate
414
+ evaluation_scores = evaluation.evaluate(
415
+ scores=filtered_scores,
416
+ qrels=qrels,
417
+ queries=query_ids,
418
+ metrics=["map", "ndcg@1", "ndcg@10", "ndcg@100", "recall@10", "recall@100"],
419
+ )
420
+
421
+ print(evaluation_scores)
422
+ all_results[eval_set] = evaluation_scores
423
+
424
+ # Save per-split results
425
+ os.makedirs(args.output_dir, exist_ok=True)
426
+ srsly.write_json(out_file, evaluation_scores)
427
+
428
+ # Free memory
429
+ del model, documents_embeddings, queries_embeddings
430
+ gc.collect()
431
+ torch.cuda.empty_cache()
432
+
433
+ # Print summary
434
+ print(f"\n{'='*60}")
435
+ print("SUMMARY")
436
+ print(f"{'='*60}")
437
+ ndcg10_values = []
438
+ for split, scores_dict in sorted(all_results.items()):
439
+ ndcg10 = scores_dict.get("ndcg@10", 0)
440
+ ndcg10_values.append(ndcg10)
441
+ print(f" {split:30s} nDCG@10 = {ndcg10:.4f}")
442
+ if ndcg10_values:
443
+ print(f" {'AVERAGE':30s} nDCG@10 = {sum(ndcg10_values)/len(ndcg10_values):.4f}")
444
+
445
+ # Save summary
446
+ os.makedirs(args.output_dir, exist_ok=True)
447
+ srsly.write_json(os.path.join(args.output_dir, "summary.json"), all_results)
448
+ print(f"\nResults saved to {args.output_dir}/")
449
+
450
+
451
+ if __name__ == "__main__":
452
+ main()
453
+
454
+
455
+ # Your distilled model
456
+ """
457
+ python evaluation/evaluate_bright.py \
458
+ --model_path output/r1colbert_distilled/R1-ColBERT-distill-alpha0.3/checkpoint-1500 \
459
+ --model_version baseline
460
+ # Baseline comparison
461
+ python evaluation/evaluate_bright.py \
462
+ --model_path lightonai/Reason-ModernColBERT \
463
+ --model_version baseline
464
+
465
+
466
+ python LaRLI/evaluate_bright.py \
467
+ --model_path /scratch/fs201059/aa17626/modernTransformer/repro_reason_moderncolbert/output/baseline-ColBERT-ReasonIR/final \
468
+ --model_version baseline \
469
+ --splits biology
470
+ """
logo_reason_mxai.png ADDED

Git LFS Details

  • SHA256: c4a1063f9edc750cb51b41415ac1224120a9068a8b00d5dc9d6f5847b5291809
  • Pointer size: 132 Bytes
  • Size of remote file: 5.6 MB
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7d86cdb2c133be712ea760ea2c053d0eed2dc712d99981ecef4d83eb1827d95b
3
+ size 63773752
modules.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "idx": 0,
4
+ "name": "0",
5
+ "path": "",
6
+ "type": "sentence_transformers.models.Transformer"
7
+ },
8
+ {
9
+ "idx": 1,
10
+ "name": "1",
11
+ "path": "1_Dense",
12
+ "type": "pylate.models.Dense.Dense"
13
+ },
14
+ {
15
+ "idx": 2,
16
+ "name": "2",
17
+ "path": "2_Dense",
18
+ "type": "pylate.models.Dense.Dense"
19
+ },
20
+ {
21
+ "idx": 3,
22
+ "name": "3",
23
+ "path": "3_Dense",
24
+ "type": "pylate.models.Dense.Dense"
25
+ }
26
+ ]
results/BrightRetrieval_aops_evaluation_scores_qlen256.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.0328788796,
3
+ "ndcg@1":0.009009009,
4
+ "ndcg@10":0.0488934387,
5
+ "ndcg@100":0.0961218916,
6
+ "recall@10":0.0662269412,
7
+ "recall@100":0.2151973402
8
+ }
results/BrightRetrieval_biology_evaluation_scores_qlen256.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.2627349551,
3
+ "ndcg@1":0.3398058252,
4
+ "ndcg@10":0.3316177299,
5
+ "ndcg@100":0.4150093687,
6
+ "recall@10":0.3784616664,
7
+ "recall@100":0.6726602104
8
+ }
results/BrightRetrieval_earth_science_evaluation_scores_qlen256.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.3771350277,
3
+ "ndcg@1":0.4568965517,
4
+ "ndcg@10":0.4427963898,
5
+ "ndcg@100":0.5147986924,
6
+ "recall@10":0.4756853467,
7
+ "recall@100":0.7088055504
8
+ }
results/BrightRetrieval_economics_evaluation_scores_qlen256.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.1544231851,
3
+ "ndcg@1":0.1941747573,
4
+ "ndcg@10":0.2024816779,
5
+ "ndcg@100":0.2653182706,
6
+ "recall@10":0.1939382382,
7
+ "recall@100":0.48742489
8
+ }
results/BrightRetrieval_leetcode_evaluation_scores_qlen256.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.1243283683,
3
+ "ndcg@1":0.0,
4
+ "ndcg@10":0.1739560791,
5
+ "ndcg@100":0.2252391566,
6
+ "recall@10":0.2919014085,
7
+ "recall@100":0.510915493
8
+ }
results/BrightRetrieval_pony_evaluation_scores_qlen32.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.1189579454,
3
+ "ndcg@1":0.2767857143,
4
+ "ndcg@10":0.2276578569,
5
+ "ndcg@100":0.3506526988,
6
+ "recall@10":0.1008905099,
7
+ "recall@100":0.4784767044
8
+ }
results/BrightRetrieval_psychology_evaluation_scores_qlen256.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.2074368567,
3
+ "ndcg@1":0.198019802,
4
+ "ndcg@10":0.2490934222,
5
+ "ndcg@100":0.3159161008,
6
+ "recall@10":0.2984747676,
7
+ "recall@100":0.5615963777
8
+ }
results/BrightRetrieval_robotics_evaluation_scores_qlen256.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.1487403514,
3
+ "ndcg@1":0.1683168317,
4
+ "ndcg@10":0.1865310541,
5
+ "ndcg@100":0.2415133828,
6
+ "recall@10":0.216579428,
7
+ "recall@100":0.4233349665
8
+ }
results/BrightRetrieval_stackoverflow_evaluation_scores_qlen256.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.1215601626,
3
+ "ndcg@1":0.1623931624,
4
+ "ndcg@10":0.1666014935,
5
+ "ndcg@100":0.2295905877,
6
+ "recall@10":0.175730543,
7
+ "recall@100":0.3979622293
8
+ }
results/BrightRetrieval_sustainable_living_evaluation_scores_qlen256.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.1692097115,
3
+ "ndcg@1":0.1574074074,
4
+ "ndcg@10":0.2010654629,
5
+ "ndcg@100":0.2989909999,
6
+ "recall@10":0.2462456855,
7
+ "recall@100":0.5804351005
8
+ }
results/BrightRetrieval_theoremqa_questions_evaluation_scores_qlen256.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.0720669895,
3
+ "ndcg@1":0.0243902439,
4
+ "ndcg@10":0.0904299064,
5
+ "ndcg@100":0.1150213787,
6
+ "recall@10":0.1296747967,
7
+ "recall@100":0.2348432056
8
+ }
results/BrightRetrieval_theoremqa_theorems_evaluation_scores_qlen256.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "map":0.0268134408,
3
+ "ndcg@1":0.0153846154,
4
+ "ndcg@10":0.0318862999,
5
+ "ndcg@100":0.0559448533,
6
+ "recall@10":0.0512820513,
7
+ "recall@100":0.1697435897
8
+ }
results/run_meta.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_path":"/data/fs201059/aa17626/modernTransformer/repro_reason_moderncolbert/output_edge32m_d128_fixed/stage-bge-hn/final",
3
+ "model_version":"baseline",
4
+ "query_length":256,
5
+ "pony_query_length":32,
6
+ "use_plaid":false,
7
+ "nbits":null,
8
+ "retrieve_k":100,
9
+ "overwrite":false,
10
+ "run_name":"edge32m_d128_fixed",
11
+ "label":"output_edge32m_d128_fixed__stage-bge-hn__final",
12
+ "started_at":"2026-04-24T21:47:24",
13
+ "splits_requested":[
14
+ "stackoverflow"
15
+ ]
16
+ }
results/summary.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "leetcode":{
3
+ "map":0.1243283683,
4
+ "ndcg@1":0.0,
5
+ "ndcg@10":0.1739560791,
6
+ "ndcg@100":0.2252391566,
7
+ "recall@10":0.2919014085,
8
+ "recall@100":0.510915493
9
+ },
10
+ "aops":{
11
+ "map":0.0328788796,
12
+ "ndcg@1":0.009009009,
13
+ "ndcg@10":0.0488934387,
14
+ "ndcg@100":0.0961218916,
15
+ "recall@10":0.0662269412,
16
+ "recall@100":0.2151973402
17
+ }
18
+ }
sentence_bert_config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "max_seq_length": 255,
3
+ "do_lower_case": true
4
+ }
special_tokens_map.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cls_token": {
3
+ "content": "[CLS]",
4
+ "lstrip": false,
5
+ "normalized": false,
6
+ "rstrip": false,
7
+ "single_word": false
8
+ },
9
+ "mask_token": {
10
+ "content": "[MASK]",
11
+ "lstrip": true,
12
+ "normalized": false,
13
+ "rstrip": false,
14
+ "single_word": false
15
+ },
16
+ "pad_token": "[MASK]",
17
+ "sep_token": {
18
+ "content": "[SEP]",
19
+ "lstrip": false,
20
+ "normalized": false,
21
+ "rstrip": false,
22
+ "single_word": false
23
+ },
24
+ "unk_token": {
25
+ "content": "[UNK]",
26
+ "lstrip": false,
27
+ "normalized": false,
28
+ "rstrip": false,
29
+ "single_word": false
30
+ }
31
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,968 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "|||IP_ADDRESS|||",
5
+ "lstrip": false,
6
+ "normalized": true,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": false
10
+ },
11
+ "1": {
12
+ "content": "<|padding|>",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "50254": {
20
+ "content": " ",
21
+ "lstrip": false,
22
+ "normalized": true,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": false
26
+ },
27
+ "50255": {
28
+ "content": " ",
29
+ "lstrip": false,
30
+ "normalized": true,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": false
34
+ },
35
+ "50256": {
36
+ "content": " ",
37
+ "lstrip": false,
38
+ "normalized": true,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": false
42
+ },
43
+ "50257": {
44
+ "content": " ",
45
+ "lstrip": false,
46
+ "normalized": true,
47
+ "rstrip": false,
48
+ "single_word": false,
49
+ "special": false
50
+ },
51
+ "50258": {
52
+ "content": " ",
53
+ "lstrip": false,
54
+ "normalized": true,
55
+ "rstrip": false,
56
+ "single_word": false,
57
+ "special": false
58
+ },
59
+ "50259": {
60
+ "content": " ",
61
+ "lstrip": false,
62
+ "normalized": true,
63
+ "rstrip": false,
64
+ "single_word": false,
65
+ "special": false
66
+ },
67
+ "50260": {
68
+ "content": " ",
69
+ "lstrip": false,
70
+ "normalized": true,
71
+ "rstrip": false,
72
+ "single_word": false,
73
+ "special": false
74
+ },
75
+ "50261": {
76
+ "content": " ",
77
+ "lstrip": false,
78
+ "normalized": true,
79
+ "rstrip": false,
80
+ "single_word": false,
81
+ "special": false
82
+ },
83
+ "50262": {
84
+ "content": " ",
85
+ "lstrip": false,
86
+ "normalized": true,
87
+ "rstrip": false,
88
+ "single_word": false,
89
+ "special": false
90
+ },
91
+ "50263": {
92
+ "content": " ",
93
+ "lstrip": false,
94
+ "normalized": true,
95
+ "rstrip": false,
96
+ "single_word": false,
97
+ "special": false
98
+ },
99
+ "50264": {
100
+ "content": " ",
101
+ "lstrip": false,
102
+ "normalized": true,
103
+ "rstrip": false,
104
+ "single_word": false,
105
+ "special": false
106
+ },
107
+ "50265": {
108
+ "content": " ",
109
+ "lstrip": false,
110
+ "normalized": true,
111
+ "rstrip": false,
112
+ "single_word": false,
113
+ "special": false
114
+ },
115
+ "50266": {
116
+ "content": " ",
117
+ "lstrip": false,
118
+ "normalized": true,
119
+ "rstrip": false,
120
+ "single_word": false,
121
+ "special": false
122
+ },
123
+ "50267": {
124
+ "content": " ",
125
+ "lstrip": false,
126
+ "normalized": true,
127
+ "rstrip": false,
128
+ "single_word": false,
129
+ "special": false
130
+ },
131
+ "50268": {
132
+ "content": " ",
133
+ "lstrip": false,
134
+ "normalized": true,
135
+ "rstrip": false,
136
+ "single_word": false,
137
+ "special": false
138
+ },
139
+ "50269": {
140
+ "content": " ",
141
+ "lstrip": false,
142
+ "normalized": true,
143
+ "rstrip": false,
144
+ "single_word": false,
145
+ "special": false
146
+ },
147
+ "50270": {
148
+ "content": " ",
149
+ "lstrip": false,
150
+ "normalized": true,
151
+ "rstrip": false,
152
+ "single_word": false,
153
+ "special": false
154
+ },
155
+ "50271": {
156
+ "content": " ",
157
+ "lstrip": false,
158
+ "normalized": true,
159
+ "rstrip": false,
160
+ "single_word": false,
161
+ "special": false
162
+ },
163
+ "50272": {
164
+ "content": " ",
165
+ "lstrip": false,
166
+ "normalized": true,
167
+ "rstrip": false,
168
+ "single_word": false,
169
+ "special": false
170
+ },
171
+ "50273": {
172
+ "content": " ",
173
+ "lstrip": false,
174
+ "normalized": true,
175
+ "rstrip": false,
176
+ "single_word": false,
177
+ "special": false
178
+ },
179
+ "50274": {
180
+ "content": " ",
181
+ "lstrip": false,
182
+ "normalized": true,
183
+ "rstrip": false,
184
+ "single_word": false,
185
+ "special": false
186
+ },
187
+ "50275": {
188
+ "content": " ",
189
+ "lstrip": false,
190
+ "normalized": true,
191
+ "rstrip": false,
192
+ "single_word": false,
193
+ "special": false
194
+ },
195
+ "50276": {
196
+ "content": " ",
197
+ "lstrip": false,
198
+ "normalized": true,
199
+ "rstrip": false,
200
+ "single_word": false,
201
+ "special": false
202
+ },
203
+ "50277": {
204
+ "content": "|||EMAIL_ADDRESS|||",
205
+ "lstrip": false,
206
+ "normalized": true,
207
+ "rstrip": false,
208
+ "single_word": false,
209
+ "special": false
210
+ },
211
+ "50278": {
212
+ "content": "|||PHONE_NUMBER|||",
213
+ "lstrip": false,
214
+ "normalized": true,
215
+ "rstrip": false,
216
+ "single_word": false,
217
+ "special": false
218
+ },
219
+ "50279": {
220
+ "content": "<|endoftext|>",
221
+ "lstrip": false,
222
+ "normalized": false,
223
+ "rstrip": false,
224
+ "single_word": false,
225
+ "special": true
226
+ },
227
+ "50280": {
228
+ "content": "[UNK]",
229
+ "lstrip": false,
230
+ "normalized": false,
231
+ "rstrip": false,
232
+ "single_word": false,
233
+ "special": true
234
+ },
235
+ "50281": {
236
+ "content": "[CLS]",
237
+ "lstrip": false,
238
+ "normalized": false,
239
+ "rstrip": false,
240
+ "single_word": false,
241
+ "special": true
242
+ },
243
+ "50282": {
244
+ "content": "[SEP]",
245
+ "lstrip": false,
246
+ "normalized": false,
247
+ "rstrip": false,
248
+ "single_word": false,
249
+ "special": true
250
+ },
251
+ "50283": {
252
+ "content": "[PAD]",
253
+ "lstrip": false,
254
+ "normalized": false,
255
+ "rstrip": false,
256
+ "single_word": false,
257
+ "special": true
258
+ },
259
+ "50284": {
260
+ "content": "[MASK]",
261
+ "lstrip": true,
262
+ "normalized": false,
263
+ "rstrip": false,
264
+ "single_word": false,
265
+ "special": true
266
+ },
267
+ "50285": {
268
+ "content": "[unused0]",
269
+ "lstrip": false,
270
+ "normalized": true,
271
+ "rstrip": false,
272
+ "single_word": false,
273
+ "special": false
274
+ },
275
+ "50286": {
276
+ "content": "[unused1]",
277
+ "lstrip": false,
278
+ "normalized": true,
279
+ "rstrip": false,
280
+ "single_word": false,
281
+ "special": false
282
+ },
283
+ "50287": {
284
+ "content": "[unused2]",
285
+ "lstrip": false,
286
+ "normalized": true,
287
+ "rstrip": false,
288
+ "single_word": false,
289
+ "special": false
290
+ },
291
+ "50288": {
292
+ "content": "[unused3]",
293
+ "lstrip": false,
294
+ "normalized": true,
295
+ "rstrip": false,
296
+ "single_word": false,
297
+ "special": false
298
+ },
299
+ "50289": {
300
+ "content": "[unused4]",
301
+ "lstrip": false,
302
+ "normalized": true,
303
+ "rstrip": false,
304
+ "single_word": false,
305
+ "special": false
306
+ },
307
+ "50290": {
308
+ "content": "[unused5]",
309
+ "lstrip": false,
310
+ "normalized": true,
311
+ "rstrip": false,
312
+ "single_word": false,
313
+ "special": false
314
+ },
315
+ "50291": {
316
+ "content": "[unused6]",
317
+ "lstrip": false,
318
+ "normalized": true,
319
+ "rstrip": false,
320
+ "single_word": false,
321
+ "special": false
322
+ },
323
+ "50292": {
324
+ "content": "[unused7]",
325
+ "lstrip": false,
326
+ "normalized": true,
327
+ "rstrip": false,
328
+ "single_word": false,
329
+ "special": false
330
+ },
331
+ "50293": {
332
+ "content": "[unused8]",
333
+ "lstrip": false,
334
+ "normalized": true,
335
+ "rstrip": false,
336
+ "single_word": false,
337
+ "special": false
338
+ },
339
+ "50294": {
340
+ "content": "[unused9]",
341
+ "lstrip": false,
342
+ "normalized": true,
343
+ "rstrip": false,
344
+ "single_word": false,
345
+ "special": false
346
+ },
347
+ "50295": {
348
+ "content": "[unused10]",
349
+ "lstrip": false,
350
+ "normalized": true,
351
+ "rstrip": false,
352
+ "single_word": false,
353
+ "special": false
354
+ },
355
+ "50296": {
356
+ "content": "[unused11]",
357
+ "lstrip": false,
358
+ "normalized": true,
359
+ "rstrip": false,
360
+ "single_word": false,
361
+ "special": false
362
+ },
363
+ "50297": {
364
+ "content": "[unused12]",
365
+ "lstrip": false,
366
+ "normalized": true,
367
+ "rstrip": false,
368
+ "single_word": false,
369
+ "special": false
370
+ },
371
+ "50298": {
372
+ "content": "[unused13]",
373
+ "lstrip": false,
374
+ "normalized": true,
375
+ "rstrip": false,
376
+ "single_word": false,
377
+ "special": false
378
+ },
379
+ "50299": {
380
+ "content": "[unused14]",
381
+ "lstrip": false,
382
+ "normalized": true,
383
+ "rstrip": false,
384
+ "single_word": false,
385
+ "special": false
386
+ },
387
+ "50300": {
388
+ "content": "[unused15]",
389
+ "lstrip": false,
390
+ "normalized": true,
391
+ "rstrip": false,
392
+ "single_word": false,
393
+ "special": false
394
+ },
395
+ "50301": {
396
+ "content": "[unused16]",
397
+ "lstrip": false,
398
+ "normalized": true,
399
+ "rstrip": false,
400
+ "single_word": false,
401
+ "special": false
402
+ },
403
+ "50302": {
404
+ "content": "[unused17]",
405
+ "lstrip": false,
406
+ "normalized": true,
407
+ "rstrip": false,
408
+ "single_word": false,
409
+ "special": false
410
+ },
411
+ "50303": {
412
+ "content": "[unused18]",
413
+ "lstrip": false,
414
+ "normalized": true,
415
+ "rstrip": false,
416
+ "single_word": false,
417
+ "special": false
418
+ },
419
+ "50304": {
420
+ "content": "[unused19]",
421
+ "lstrip": false,
422
+ "normalized": true,
423
+ "rstrip": false,
424
+ "single_word": false,
425
+ "special": false
426
+ },
427
+ "50305": {
428
+ "content": "[unused20]",
429
+ "lstrip": false,
430
+ "normalized": true,
431
+ "rstrip": false,
432
+ "single_word": false,
433
+ "special": false
434
+ },
435
+ "50306": {
436
+ "content": "[unused21]",
437
+ "lstrip": false,
438
+ "normalized": true,
439
+ "rstrip": false,
440
+ "single_word": false,
441
+ "special": false
442
+ },
443
+ "50307": {
444
+ "content": "[unused22]",
445
+ "lstrip": false,
446
+ "normalized": true,
447
+ "rstrip": false,
448
+ "single_word": false,
449
+ "special": false
450
+ },
451
+ "50308": {
452
+ "content": "[unused23]",
453
+ "lstrip": false,
454
+ "normalized": true,
455
+ "rstrip": false,
456
+ "single_word": false,
457
+ "special": false
458
+ },
459
+ "50309": {
460
+ "content": "[unused24]",
461
+ "lstrip": false,
462
+ "normalized": true,
463
+ "rstrip": false,
464
+ "single_word": false,
465
+ "special": false
466
+ },
467
+ "50310": {
468
+ "content": "[unused25]",
469
+ "lstrip": false,
470
+ "normalized": true,
471
+ "rstrip": false,
472
+ "single_word": false,
473
+ "special": false
474
+ },
475
+ "50311": {
476
+ "content": "[unused26]",
477
+ "lstrip": false,
478
+ "normalized": true,
479
+ "rstrip": false,
480
+ "single_word": false,
481
+ "special": false
482
+ },
483
+ "50312": {
484
+ "content": "[unused27]",
485
+ "lstrip": false,
486
+ "normalized": true,
487
+ "rstrip": false,
488
+ "single_word": false,
489
+ "special": false
490
+ },
491
+ "50313": {
492
+ "content": "[unused28]",
493
+ "lstrip": false,
494
+ "normalized": true,
495
+ "rstrip": false,
496
+ "single_word": false,
497
+ "special": false
498
+ },
499
+ "50314": {
500
+ "content": "[unused29]",
501
+ "lstrip": false,
502
+ "normalized": true,
503
+ "rstrip": false,
504
+ "single_word": false,
505
+ "special": false
506
+ },
507
+ "50315": {
508
+ "content": "[unused30]",
509
+ "lstrip": false,
510
+ "normalized": true,
511
+ "rstrip": false,
512
+ "single_word": false,
513
+ "special": false
514
+ },
515
+ "50316": {
516
+ "content": "[unused31]",
517
+ "lstrip": false,
518
+ "normalized": true,
519
+ "rstrip": false,
520
+ "single_word": false,
521
+ "special": false
522
+ },
523
+ "50317": {
524
+ "content": "[unused32]",
525
+ "lstrip": false,
526
+ "normalized": true,
527
+ "rstrip": false,
528
+ "single_word": false,
529
+ "special": false
530
+ },
531
+ "50318": {
532
+ "content": "[unused33]",
533
+ "lstrip": false,
534
+ "normalized": true,
535
+ "rstrip": false,
536
+ "single_word": false,
537
+ "special": false
538
+ },
539
+ "50319": {
540
+ "content": "[unused34]",
541
+ "lstrip": false,
542
+ "normalized": true,
543
+ "rstrip": false,
544
+ "single_word": false,
545
+ "special": false
546
+ },
547
+ "50320": {
548
+ "content": "[unused35]",
549
+ "lstrip": false,
550
+ "normalized": true,
551
+ "rstrip": false,
552
+ "single_word": false,
553
+ "special": false
554
+ },
555
+ "50321": {
556
+ "content": "[unused36]",
557
+ "lstrip": false,
558
+ "normalized": true,
559
+ "rstrip": false,
560
+ "single_word": false,
561
+ "special": false
562
+ },
563
+ "50322": {
564
+ "content": "[unused37]",
565
+ "lstrip": false,
566
+ "normalized": true,
567
+ "rstrip": false,
568
+ "single_word": false,
569
+ "special": false
570
+ },
571
+ "50323": {
572
+ "content": "[unused38]",
573
+ "lstrip": false,
574
+ "normalized": true,
575
+ "rstrip": false,
576
+ "single_word": false,
577
+ "special": false
578
+ },
579
+ "50324": {
580
+ "content": "[unused39]",
581
+ "lstrip": false,
582
+ "normalized": true,
583
+ "rstrip": false,
584
+ "single_word": false,
585
+ "special": false
586
+ },
587
+ "50325": {
588
+ "content": "[unused40]",
589
+ "lstrip": false,
590
+ "normalized": true,
591
+ "rstrip": false,
592
+ "single_word": false,
593
+ "special": false
594
+ },
595
+ "50326": {
596
+ "content": "[unused41]",
597
+ "lstrip": false,
598
+ "normalized": true,
599
+ "rstrip": false,
600
+ "single_word": false,
601
+ "special": false
602
+ },
603
+ "50327": {
604
+ "content": "[unused42]",
605
+ "lstrip": false,
606
+ "normalized": true,
607
+ "rstrip": false,
608
+ "single_word": false,
609
+ "special": false
610
+ },
611
+ "50328": {
612
+ "content": "[unused43]",
613
+ "lstrip": false,
614
+ "normalized": true,
615
+ "rstrip": false,
616
+ "single_word": false,
617
+ "special": false
618
+ },
619
+ "50329": {
620
+ "content": "[unused44]",
621
+ "lstrip": false,
622
+ "normalized": true,
623
+ "rstrip": false,
624
+ "single_word": false,
625
+ "special": false
626
+ },
627
+ "50330": {
628
+ "content": "[unused45]",
629
+ "lstrip": false,
630
+ "normalized": true,
631
+ "rstrip": false,
632
+ "single_word": false,
633
+ "special": false
634
+ },
635
+ "50331": {
636
+ "content": "[unused46]",
637
+ "lstrip": false,
638
+ "normalized": true,
639
+ "rstrip": false,
640
+ "single_word": false,
641
+ "special": false
642
+ },
643
+ "50332": {
644
+ "content": "[unused47]",
645
+ "lstrip": false,
646
+ "normalized": true,
647
+ "rstrip": false,
648
+ "single_word": false,
649
+ "special": false
650
+ },
651
+ "50333": {
652
+ "content": "[unused48]",
653
+ "lstrip": false,
654
+ "normalized": true,
655
+ "rstrip": false,
656
+ "single_word": false,
657
+ "special": false
658
+ },
659
+ "50334": {
660
+ "content": "[unused49]",
661
+ "lstrip": false,
662
+ "normalized": true,
663
+ "rstrip": false,
664
+ "single_word": false,
665
+ "special": false
666
+ },
667
+ "50335": {
668
+ "content": "[unused50]",
669
+ "lstrip": false,
670
+ "normalized": true,
671
+ "rstrip": false,
672
+ "single_word": false,
673
+ "special": false
674
+ },
675
+ "50336": {
676
+ "content": "[unused51]",
677
+ "lstrip": false,
678
+ "normalized": true,
679
+ "rstrip": false,
680
+ "single_word": false,
681
+ "special": false
682
+ },
683
+ "50337": {
684
+ "content": "[unused52]",
685
+ "lstrip": false,
686
+ "normalized": true,
687
+ "rstrip": false,
688
+ "single_word": false,
689
+ "special": false
690
+ },
691
+ "50338": {
692
+ "content": "[unused53]",
693
+ "lstrip": false,
694
+ "normalized": true,
695
+ "rstrip": false,
696
+ "single_word": false,
697
+ "special": false
698
+ },
699
+ "50339": {
700
+ "content": "[unused54]",
701
+ "lstrip": false,
702
+ "normalized": true,
703
+ "rstrip": false,
704
+ "single_word": false,
705
+ "special": false
706
+ },
707
+ "50340": {
708
+ "content": "[unused55]",
709
+ "lstrip": false,
710
+ "normalized": true,
711
+ "rstrip": false,
712
+ "single_word": false,
713
+ "special": false
714
+ },
715
+ "50341": {
716
+ "content": "[unused56]",
717
+ "lstrip": false,
718
+ "normalized": true,
719
+ "rstrip": false,
720
+ "single_word": false,
721
+ "special": false
722
+ },
723
+ "50342": {
724
+ "content": "[unused57]",
725
+ "lstrip": false,
726
+ "normalized": true,
727
+ "rstrip": false,
728
+ "single_word": false,
729
+ "special": false
730
+ },
731
+ "50343": {
732
+ "content": "[unused58]",
733
+ "lstrip": false,
734
+ "normalized": true,
735
+ "rstrip": false,
736
+ "single_word": false,
737
+ "special": false
738
+ },
739
+ "50344": {
740
+ "content": "[unused59]",
741
+ "lstrip": false,
742
+ "normalized": true,
743
+ "rstrip": false,
744
+ "single_word": false,
745
+ "special": false
746
+ },
747
+ "50345": {
748
+ "content": "[unused60]",
749
+ "lstrip": false,
750
+ "normalized": true,
751
+ "rstrip": false,
752
+ "single_word": false,
753
+ "special": false
754
+ },
755
+ "50346": {
756
+ "content": "[unused61]",
757
+ "lstrip": false,
758
+ "normalized": true,
759
+ "rstrip": false,
760
+ "single_word": false,
761
+ "special": false
762
+ },
763
+ "50347": {
764
+ "content": "[unused62]",
765
+ "lstrip": false,
766
+ "normalized": true,
767
+ "rstrip": false,
768
+ "single_word": false,
769
+ "special": false
770
+ },
771
+ "50348": {
772
+ "content": "[unused63]",
773
+ "lstrip": false,
774
+ "normalized": true,
775
+ "rstrip": false,
776
+ "single_word": false,
777
+ "special": false
778
+ },
779
+ "50349": {
780
+ "content": "[unused64]",
781
+ "lstrip": false,
782
+ "normalized": true,
783
+ "rstrip": false,
784
+ "single_word": false,
785
+ "special": false
786
+ },
787
+ "50350": {
788
+ "content": "[unused65]",
789
+ "lstrip": false,
790
+ "normalized": true,
791
+ "rstrip": false,
792
+ "single_word": false,
793
+ "special": false
794
+ },
795
+ "50351": {
796
+ "content": "[unused66]",
797
+ "lstrip": false,
798
+ "normalized": true,
799
+ "rstrip": false,
800
+ "single_word": false,
801
+ "special": false
802
+ },
803
+ "50352": {
804
+ "content": "[unused67]",
805
+ "lstrip": false,
806
+ "normalized": true,
807
+ "rstrip": false,
808
+ "single_word": false,
809
+ "special": false
810
+ },
811
+ "50353": {
812
+ "content": "[unused68]",
813
+ "lstrip": false,
814
+ "normalized": true,
815
+ "rstrip": false,
816
+ "single_word": false,
817
+ "special": false
818
+ },
819
+ "50354": {
820
+ "content": "[unused69]",
821
+ "lstrip": false,
822
+ "normalized": true,
823
+ "rstrip": false,
824
+ "single_word": false,
825
+ "special": false
826
+ },
827
+ "50355": {
828
+ "content": "[unused70]",
829
+ "lstrip": false,
830
+ "normalized": true,
831
+ "rstrip": false,
832
+ "single_word": false,
833
+ "special": false
834
+ },
835
+ "50356": {
836
+ "content": "[unused71]",
837
+ "lstrip": false,
838
+ "normalized": true,
839
+ "rstrip": false,
840
+ "single_word": false,
841
+ "special": false
842
+ },
843
+ "50357": {
844
+ "content": "[unused72]",
845
+ "lstrip": false,
846
+ "normalized": true,
847
+ "rstrip": false,
848
+ "single_word": false,
849
+ "special": false
850
+ },
851
+ "50358": {
852
+ "content": "[unused73]",
853
+ "lstrip": false,
854
+ "normalized": true,
855
+ "rstrip": false,
856
+ "single_word": false,
857
+ "special": false
858
+ },
859
+ "50359": {
860
+ "content": "[unused74]",
861
+ "lstrip": false,
862
+ "normalized": true,
863
+ "rstrip": false,
864
+ "single_word": false,
865
+ "special": false
866
+ },
867
+ "50360": {
868
+ "content": "[unused75]",
869
+ "lstrip": false,
870
+ "normalized": true,
871
+ "rstrip": false,
872
+ "single_word": false,
873
+ "special": false
874
+ },
875
+ "50361": {
876
+ "content": "[unused76]",
877
+ "lstrip": false,
878
+ "normalized": true,
879
+ "rstrip": false,
880
+ "single_word": false,
881
+ "special": false
882
+ },
883
+ "50362": {
884
+ "content": "[unused77]",
885
+ "lstrip": false,
886
+ "normalized": true,
887
+ "rstrip": false,
888
+ "single_word": false,
889
+ "special": false
890
+ },
891
+ "50363": {
892
+ "content": "[unused78]",
893
+ "lstrip": false,
894
+ "normalized": true,
895
+ "rstrip": false,
896
+ "single_word": false,
897
+ "special": false
898
+ },
899
+ "50364": {
900
+ "content": "[unused79]",
901
+ "lstrip": false,
902
+ "normalized": true,
903
+ "rstrip": false,
904
+ "single_word": false,
905
+ "special": false
906
+ },
907
+ "50365": {
908
+ "content": "[unused80]",
909
+ "lstrip": false,
910
+ "normalized": true,
911
+ "rstrip": false,
912
+ "single_word": false,
913
+ "special": false
914
+ },
915
+ "50366": {
916
+ "content": "[unused81]",
917
+ "lstrip": false,
918
+ "normalized": true,
919
+ "rstrip": false,
920
+ "single_word": false,
921
+ "special": false
922
+ },
923
+ "50367": {
924
+ "content": "[unused82]",
925
+ "lstrip": false,
926
+ "normalized": true,
927
+ "rstrip": false,
928
+ "single_word": false,
929
+ "special": false
930
+ },
931
+ "50368": {
932
+ "content": "[Q] ",
933
+ "lstrip": false,
934
+ "normalized": true,
935
+ "rstrip": false,
936
+ "single_word": false,
937
+ "special": false
938
+ },
939
+ "50369": {
940
+ "content": "[D] ",
941
+ "lstrip": false,
942
+ "normalized": true,
943
+ "rstrip": false,
944
+ "single_word": false,
945
+ "special": false
946
+ }
947
+ },
948
+ "clean_up_tokenization_spaces": true,
949
+ "cls_token": "[CLS]",
950
+ "extra_special_tokens": {},
951
+ "mask_token": "[MASK]",
952
+ "max_length": 255,
953
+ "model_input_names": [
954
+ "input_ids",
955
+ "attention_mask"
956
+ ],
957
+ "model_max_length": 255,
958
+ "pad_to_multiple_of": null,
959
+ "pad_token": "[MASK]",
960
+ "pad_token_type_id": 0,
961
+ "padding_side": "right",
962
+ "sep_token": "[SEP]",
963
+ "stride": 0,
964
+ "tokenizer_class": "PreTrainedTokenizerFast",
965
+ "truncation_side": "right",
966
+ "truncation_strategy": "longest_first",
967
+ "unk_token": "[UNK]"
968
+ }
training/launch_train_multinode.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Launcher that forwards to LaRLI/train_baseline.py, with a multi-node fix.
2
+
3
+ Background
4
+ ----------
5
+ `sentence_transformers.util.get_device_name()` contains a latent bug that
6
+ affects multi-node torchrun runs:
7
+
8
+ if torch.distributed.is_initialized():
9
+ local_rank = torch.distributed.get_rank() # actually the GLOBAL rank
10
+ else:
11
+ local_rank = int(os.environ.get("LOCAL_RANK", 0))
12
+ return f"cuda:{local_rank}"
13
+
14
+ `torch.distributed.get_rank()` returns the **global** rank (0..world_size-1),
15
+ not the local rank within the node. On node 1 of a 2-node 4-GPU run, ranks
16
+ 4-7 end up trying to bind to `cuda:4..cuda:7`, but only `cuda:0..cuda:3` are
17
+ visible per node → `RuntimeError: CUDA error: invalid device ordinal`.
18
+
19
+ This launcher:
20
+ 1. Pins the current process to `cuda:LOCAL_RANK` from torchrun.
21
+ 2. Monkey-patches `sentence_transformers.util.get_device_name` (and the
22
+ local alias in `SentenceTransformer`) to return the correct local rank
23
+ instead of the global one.
24
+ 3. Hands off to `LaRLI.train_baseline.main()` unchanged.
25
+
26
+ Nothing in LaRLI/ or pylate/ or sentence_transformers/ is modified.
27
+
28
+ Usage (from your sbatch, inside srun + bash -c):
29
+ torchrun --nproc_per_node=4 --nnodes=2 --node_rank=$SLURM_PROCID ... \
30
+ widen_projection/launch_train_multinode.py \
31
+ --base_model ... --train_data_path ... (same args as train_baseline.py)
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import os
37
+ import sys
38
+
39
+ # ── 1. Resolve local rank from torchrun env ────────────────────────────
40
+ _LOCAL_RANK = int(os.environ.get("LOCAL_RANK", 0))
41
+ _GLOBAL_RANK = int(os.environ.get("RANK", 0))
42
+ _WORLD = int(os.environ.get("WORLD_SIZE", 1))
43
+ print(f"[launch] RANK={_GLOBAL_RANK} LOCAL_RANK={_LOCAL_RANK} WORLD={_WORLD}", flush=True)
44
+
45
+ import torch # noqa: E402
46
+
47
+ if torch.cuda.is_available():
48
+ print(f"[launch] cuda.device_count={torch.cuda.device_count()} (before pin)", flush=True)
49
+ if _LOCAL_RANK < torch.cuda.device_count():
50
+ torch.cuda.set_device(_LOCAL_RANK)
51
+ print(f"[launch] torch.cuda.set_device({_LOCAL_RANK}); current={torch.cuda.current_device()}", flush=True)
52
+ else:
53
+ print(f"[launch] WARNING: LOCAL_RANK={_LOCAL_RANK} >= device_count={torch.cuda.device_count()}", flush=True)
54
+
55
+ # ── 2. Monkey-patch ST's get_device_name before anything imports ST ───
56
+ import sentence_transformers.util as _st_util # noqa: E402
57
+
58
+
59
+ def _patched_get_device_name() -> str:
60
+ """Return `cuda:LOCAL_RANK` under torchrun, else fall back to ST's own logic."""
61
+ if torch.cuda.is_available():
62
+ dev = f"cuda:{_LOCAL_RANK}"
63
+ print(f"[launch] get_device_name() -> {dev} (rank {_GLOBAL_RANK})", flush=True)
64
+ return dev
65
+ return _ORIG_GET_DEVICE_NAME()
66
+
67
+
68
+ _ORIG_GET_DEVICE_NAME = _st_util.get_device_name
69
+ _st_util.get_device_name = _patched_get_device_name
70
+ print(f"[launch] patched sentence_transformers.util.get_device_name", flush=True)
71
+
72
+ # SentenceTransformer imports `get_device_name` by name at module load time,
73
+ # so we also need to patch the already-bound reference inside that module.
74
+ # IMPORTANT: `sentence_transformers/__init__.py` does
75
+ # `from .SentenceTransformer import SentenceTransformer`
76
+ # which shadows the submodule name with the class. `import
77
+ # sentence_transformers.SentenceTransformer` therefore returns the CLASS, not
78
+ # the module. The real module is in `sys.modules`.
79
+ _ST_MODULE = sys.modules.get("sentence_transformers.SentenceTransformer")
80
+ if _ST_MODULE is None:
81
+ # Force-import the submodule so it lands in sys.modules.
82
+ import importlib
83
+ _ST_MODULE = importlib.import_module("sentence_transformers.SentenceTransformer")
84
+
85
+ if hasattr(_ST_MODULE, "get_device_name"):
86
+ _ST_MODULE.get_device_name = _patched_get_device_name
87
+ print(f"[launch] patched sentence_transformers.SentenceTransformer (module).get_device_name",
88
+ flush=True)
89
+ else:
90
+ print(f"[launch] WARNING: module has no get_device_name; attrs with 'device': "
91
+ f"{[a for a in dir(_ST_MODULE) if 'device' in a.lower()]}", flush=True)
92
+
93
+ # ── 3. Override max_grad_norm if requested via env var ────────────────
94
+ # Widened projection heads start with zero-init weights in the new dims.
95
+ # Those zeros produce pathological gradients at bootstrap (norm ~30-50k),
96
+ # which HF Trainer's default max_grad_norm=1.0 clips away to near-nothing.
97
+ # Set MAX_GRAD_NORM=100 (or higher / "inf") in the sbatch to let the new
98
+ # dims actually learn; the original 64-dim weights are already trained so
99
+ # they won't blow up.
100
+ _MAX_GRAD_NORM_OVERRIDE = os.environ.get("MAX_GRAD_NORM")
101
+ if _MAX_GRAD_NORM_OVERRIDE:
102
+ import sentence_transformers.training_args as _st_args
103
+
104
+ _orig_ta_init = _st_args.SentenceTransformerTrainingArguments.__post_init__
105
+
106
+ def _patched_post_init(self):
107
+ try:
108
+ val = float(_MAX_GRAD_NORM_OVERRIDE)
109
+ except ValueError:
110
+ val = float("inf")
111
+ self.max_grad_norm = val
112
+ print(f"[launch] forced max_grad_norm = {val} (was default 1.0)", flush=True)
113
+ _orig_ta_init(self)
114
+
115
+ _st_args.SentenceTransformerTrainingArguments.__post_init__ = _patched_post_init
116
+
117
+ # ── 4. Make the project importable and hand off ───────────────────────
118
+ _PROJECT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
119
+ for p in (_PROJECT, os.path.join(_PROJECT, "pylate")):
120
+ if p not in sys.path:
121
+ sys.path.insert(0, p)
122
+
123
+ from LaRLI.train_baseline import main # noqa: E402
124
+
125
+ if __name__ == "__main__":
126
+ main()
training/materialize_bge_reasoner.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Materialize hanhainebula/bge-reasoner-data to {query, positive, negative}.
2
+
3
+ Upstream dataset: https://huggingface.co/datasets/hanhainebula/bge-reasoner-data
4
+ Files: 12 per-BRIGHT-domain JSONL files under ``bge-reasoner-data-0904/``.
5
+ Schema per row: ``{prompt, query, pos:list[str], neg:list[str]}``.
6
+
7
+ We build BRIGHT-aligned training triples by:
8
+ - Prepending the ``prompt`` to the query in the format used by ReasonEmbed/D:
9
+ ``f"{prompt}\nQuery: {query}"``
10
+ so the model sees the same instruction-style prefix it would see in the
11
+ vm2825 polish stage.
12
+ - Exploding each row into up to ``max_pos × max_neg`` (query, pos, neg) triples.
13
+ - Concatenating all 12 domains (or a single named domain) and shuffling.
14
+
15
+ Usage
16
+ -----
17
+ python LaRLI/data/materialize_bge_reasoner.py \
18
+ --output-dir data/bge_reasoner_processed \
19
+ --max-pos-per-query 2 --max-neg-per-query 4
20
+
21
+ # single domain (e.g. aops only) — useful to target a weak split:
22
+ python LaRLI/data/materialize_bge_reasoner.py \
23
+ --split aops --output-dir data/bge_reasoner_aops_processed
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import json
30
+ import logging
31
+ import shutil
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ _PROJECT_ROOT = Path(__file__).resolve().parents[2]
36
+ if str(_PROJECT_ROOT) not in sys.path:
37
+ sys.path.insert(0, str(_PROJECT_ROOT))
38
+
39
+ from datasets import Dataset, concatenate_datasets # noqa: E402
40
+ from huggingface_hub import hf_hub_download # noqa: E402
41
+
42
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
43
+ logger = logging.getLogger(__name__)
44
+
45
+ REPO_ID = "hanhainebula/bge-reasoner-data"
46
+ DATA_SUBDIR = "bge-reasoner-data-0904"
47
+ BRIGHT_DOMAINS = [
48
+ "biology", "earth_science", "economics", "psychology",
49
+ "robotics", "stackoverflow", "sustainable_living",
50
+ "leetcode", "pony",
51
+ "aops", "theoremqa_questions", "theoremqa_theorems",
52
+ ]
53
+
54
+
55
+ def _load_domain_jsonl(domain: str, cache_dir: str | None) -> list[dict]:
56
+ path = hf_hub_download(
57
+ repo_id=REPO_ID,
58
+ filename=f"{DATA_SUBDIR}/{domain}.jsonl",
59
+ repo_type="dataset",
60
+ cache_dir=cache_dir,
61
+ )
62
+ with open(path) as f:
63
+ return [json.loads(line) for line in f if line.strip()]
64
+
65
+
66
+ def _format_query(prompt: str, query: str) -> str:
67
+ prompt = (prompt or "").strip()
68
+ query = (query or "").strip()
69
+ if prompt and query:
70
+ return f"{prompt}\nQuery: {query}"
71
+ return prompt or query
72
+
73
+
74
+ def _explode_row(row: dict, max_pos: int, max_neg: int) -> list[dict]:
75
+ q = _format_query(row.get("prompt", ""), row.get("query", ""))
76
+ pos_list = row.get("pos") or []
77
+ neg_list = row.get("neg") or []
78
+ if not q or not pos_list or not neg_list:
79
+ return []
80
+ out: list[dict] = []
81
+ for pos in pos_list[:max_pos]:
82
+ pos = (pos or "").strip()
83
+ if len(pos) < 10:
84
+ continue
85
+ for neg in neg_list[:max_neg]:
86
+ neg = (neg or "").strip()
87
+ if len(neg) < 10:
88
+ continue
89
+ out.append({"query": q, "positive": pos, "negative": neg})
90
+ return out
91
+
92
+
93
+ def _save_atomically(dataset: Dataset, output_path: Path) -> None:
94
+ output_path.parent.mkdir(parents=True, exist_ok=True)
95
+ tmp = output_path.parent / f".{output_path.name}.tmp"
96
+ if tmp.exists():
97
+ shutil.rmtree(tmp)
98
+ tmp.mkdir(parents=True, exist_ok=True)
99
+ dataset.save_to_disk(str(tmp))
100
+ if output_path.exists():
101
+ shutil.rmtree(output_path)
102
+ tmp.replace(output_path)
103
+
104
+
105
+ def parse_args() -> argparse.Namespace:
106
+ p = argparse.ArgumentParser(description="Materialize BGE-Reasoner data to triples.")
107
+ p.add_argument("--split", default="all",
108
+ help=f"'all' or one of {BRIGHT_DOMAINS}. Default: all.")
109
+ p.add_argument("--output-dir", required=True)
110
+ p.add_argument("--max-pos-per-query", type=int, default=2)
111
+ p.add_argument("--max-neg-per-query", type=int, default=4)
112
+ p.add_argument("--cache-dir", default=None,
113
+ help="HF cache dir. Defaults to HF_HOME.")
114
+ return p.parse_args()
115
+
116
+
117
+ def main() -> None:
118
+ args = parse_args()
119
+ out = Path(args.output_dir)
120
+ domains = BRIGHT_DOMAINS if args.split == "all" else [args.split]
121
+ if args.split != "all" and args.split not in BRIGHT_DOMAINS:
122
+ raise ValueError(f"Unknown split {args.split!r}. Must be 'all' or one of {BRIGHT_DOMAINS}.")
123
+
124
+ per_domain: list[Dataset] = []
125
+ grand_raw = grand_trip = 0
126
+ for dom in domains:
127
+ logger.info("Downloading %s/%s ...", DATA_SUBDIR, dom)
128
+ raw = _load_domain_jsonl(dom, args.cache_dir)
129
+ rows: list[dict] = []
130
+ for r in raw:
131
+ rows.extend(_explode_row(r, args.max_pos_per_query, args.max_neg_per_query))
132
+ logger.info(" %s: %d raw rows → %d triples", dom, len(raw), len(rows))
133
+ grand_raw += len(raw)
134
+ grand_trip += len(rows)
135
+ if rows:
136
+ per_domain.append(Dataset.from_list(rows))
137
+
138
+ if not per_domain:
139
+ raise RuntimeError("No rows materialized.")
140
+ combined = concatenate_datasets(per_domain).shuffle(seed=42)
141
+ logger.info("Combined bge-reasoner (%d domains): %d raw rows → %d triples",
142
+ len(domains), grand_raw, grand_trip)
143
+
144
+ _save_atomically(combined, out)
145
+ logger.info("Saved to %s (%d triples)", out, len(combined))
146
+
147
+
148
+ if __name__ == "__main__":
149
+ main()
training/materialize_reasonir.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Materialize ReasonIR HQ and/or VL to disk as flat {query, positive, negative} datasets.
2
+
3
+ Mirrors the existing `data/reasonir_hq_processed/` layout so both splits are loaded
4
+ the same way at train time.
5
+
6
+ Usage
7
+ -----
8
+ python LaRLI/data/materialize_reasonir.py --config hq --output-dir data/reasonir_hq_processed
9
+ python LaRLI/data/materialize_reasonir.py --config vl --output-dir data/reasonir_vl_processed
10
+ python LaRLI/data/materialize_reasonir.py --config both # materializes both
11
+
12
+ The script is idempotent: if the target directory already contains a saved dataset,
13
+ it exits early.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import logging
20
+ import shutil
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ _PROJECT_ROOT = Path(__file__).resolve().parents[2]
25
+ if str(_PROJECT_ROOT) not in sys.path:
26
+ sys.path.insert(0, str(_PROJECT_ROOT))
27
+
28
+ from LaRLI.data.prepare_data import (
29
+ _is_saved_dataset,
30
+ load_reasonir_hq_dataset,
31
+ load_reasonir_vl_dataset,
32
+ )
33
+
34
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ def _save_atomically(dataset, output_path: Path) -> None:
39
+ output_path.parent.mkdir(parents=True, exist_ok=True)
40
+ tmp_path = output_path.parent / f".{output_path.name}.tmp"
41
+ if tmp_path.exists():
42
+ shutil.rmtree(tmp_path)
43
+ tmp_path.mkdir(parents=True, exist_ok=True)
44
+ dataset.save_to_disk(str(tmp_path))
45
+ if output_path.exists():
46
+ shutil.rmtree(output_path)
47
+ tmp_path.replace(output_path)
48
+
49
+
50
+ def materialize(config: str, output_dir: Path, cache_dir: str | None) -> Path:
51
+ if _is_saved_dataset(output_dir):
52
+ logger.info("%s already materialized at %s, skipping.", config, output_dir)
53
+ return output_dir
54
+
55
+ if config == "hq":
56
+ ds = load_reasonir_hq_dataset(cache_dir=cache_dir)
57
+ elif config == "vl":
58
+ ds = load_reasonir_vl_dataset(cache_dir=cache_dir)
59
+ else:
60
+ raise ValueError(f"Unknown config: {config}")
61
+
62
+ ds = ds.select_columns(["query", "positive", "negative"])
63
+ logger.info("Saving %s (%d rows) to %s", config, len(ds), output_dir)
64
+ _save_atomically(ds, output_dir)
65
+ return output_dir
66
+
67
+
68
+ def parse_args() -> argparse.Namespace:
69
+ parser = argparse.ArgumentParser(description="Materialize ReasonIR splits to disk.")
70
+ parser.add_argument("--config", choices=["hq", "vl", "both"], required=True)
71
+ parser.add_argument(
72
+ "--output-dir",
73
+ type=str,
74
+ default=None,
75
+ help="Target directory (required when --config is hq or vl).",
76
+ )
77
+ parser.add_argument(
78
+ "--hq-output-dir",
79
+ type=str,
80
+ default=str(_PROJECT_ROOT / "data" / "reasonir_hq_processed"),
81
+ help="Used only when --config=both.",
82
+ )
83
+ parser.add_argument(
84
+ "--vl-output-dir",
85
+ type=str,
86
+ default=str(_PROJECT_ROOT / "data" / "reasonir_vl_processed"),
87
+ help="Used only when --config=both.",
88
+ )
89
+ parser.add_argument("--cache-dir", type=str, default=None)
90
+ return parser.parse_args()
91
+
92
+
93
+ def main() -> None:
94
+ args = parse_args()
95
+ if args.config in ("hq", "vl"):
96
+ if not args.output_dir:
97
+ raise SystemExit("--output-dir is required when --config is hq or vl")
98
+ out = materialize(args.config, Path(args.output_dir), args.cache_dir)
99
+ print(out)
100
+ else:
101
+ hq_out = materialize("hq", Path(args.hq_output_dir), args.cache_dir)
102
+ vl_out = materialize("vl", Path(args.vl_output_dir), args.cache_dir)
103
+ print(hq_out)
104
+ print(vl_out)
105
+
106
+
107
+ if __name__ == "__main__":
108
+ main()
training/merge_datasets.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Concatenate several saved-to-disk {query, positive, negative} datasets into one.
2
+
3
+ Useful for building a single Stage-B data dir from the three RaDeR splits, or a
4
+ single Stage-D data dir from multiple reason-embed variants.
5
+
6
+ Usage
7
+ -----
8
+ python LaRLI/data/merge_datasets.py \
9
+ --inputs data/rader_numina_all_processed \
10
+ data/rader_math_qas_lex_processed \
11
+ data/rader_math_cot_lex_processed \
12
+ --output-dir data/rader_merged_processed
13
+
14
+ Preserves only the three columns {query, positive, negative}, shuffles the
15
+ result with a fixed seed so the training loop sees interleaved sources.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import logging
22
+ import shutil
23
+ from pathlib import Path
24
+
25
+ from datasets import concatenate_datasets, load_from_disk
26
+
27
+
28
+ def _free_bytes(path: Path) -> int:
29
+ """Available bytes on the filesystem hosting `path`."""
30
+ p = path if path.exists() else path.parent
31
+ while not p.exists():
32
+ p = p.parent
33
+ stats = shutil.disk_usage(str(p))
34
+ return stats.free
35
+
36
+
37
+ def _safe_rmtree(path: Path) -> None:
38
+ """Remove `path` even if partial writes left stray files behind."""
39
+ if not path.exists():
40
+ return
41
+ shutil.rmtree(path)
42
+
43
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ REQUIRED_COLS = ("query", "positive", "negative")
48
+
49
+
50
+ def _load_one(path: Path):
51
+ if not (path / "dataset_info.json").exists():
52
+ raise FileNotFoundError(f"{path} is not a saved HF dataset (missing dataset_info.json)")
53
+ ds = load_from_disk(str(path))
54
+ missing = set(REQUIRED_COLS) - set(ds.column_names)
55
+ if missing:
56
+ raise ValueError(f"{path} missing columns: {missing}")
57
+ return ds.select_columns(list(REQUIRED_COLS))
58
+
59
+
60
+ def main() -> None:
61
+ p = argparse.ArgumentParser(description="Merge saved-to-disk triplet datasets.")
62
+ p.add_argument("--inputs", nargs="+", required=True,
63
+ help="Paths to input dataset directories.")
64
+ p.add_argument("--output-dir", required=True)
65
+ p.add_argument("--seed", type=int, default=42)
66
+ p.add_argument("--num-shards", type=int, default=None,
67
+ help="Force shard count (default: datasets auto). Use 1 for a "
68
+ "single .arrow file — slower but safer on flaky FS.")
69
+ p.add_argument("--num-proc", type=int, default=1,
70
+ help="Parallel shard writers. Default=1 (serial) because HF "
71
+ "save_to_disk has been observed to race on NFS/lustre. "
72
+ "Bump to 4 only on a healthy local FS.")
73
+ p.add_argument("--min-free-gb", type=int, default=20,
74
+ help="Abort early if the filesystem has less than this many "
75
+ "GB free (guards against mid-write disk-full failures).")
76
+ args = p.parse_args()
77
+
78
+ parts = []
79
+ total = 0
80
+ for raw in args.inputs:
81
+ path = Path(raw)
82
+ ds = _load_one(path)
83
+ logger.info(" + %s (%d rows)", path, len(ds))
84
+ parts.append(ds)
85
+ total += len(ds)
86
+
87
+ combined = concatenate_datasets(parts).shuffle(seed=args.seed)
88
+ logger.info("Combined: %d rows across %d inputs", len(combined), len(parts))
89
+ assert len(combined) == total
90
+
91
+ out = Path(args.output_dir)
92
+ out.parent.mkdir(parents=True, exist_ok=True)
93
+
94
+ free_gb = _free_bytes(out.parent) / (1 << 30)
95
+ logger.info("Free space on target FS: %.1f GB", free_gb)
96
+ if free_gb < args.min_free_gb:
97
+ raise SystemExit(
98
+ f"Only {free_gb:.1f} GB free on {out.parent}, below --min-free-gb "
99
+ f"{args.min_free_gb}. Aborting to avoid a mid-write failure."
100
+ )
101
+
102
+ tmp = out.parent / f".{out.name}.tmp"
103
+ # Defensive: blow away ANY leftover from a prior crash before starting.
104
+ _safe_rmtree(tmp)
105
+ tmp.mkdir(parents=True, exist_ok=True)
106
+
107
+ save_kwargs = {}
108
+ if args.num_shards is not None:
109
+ save_kwargs["num_shards"] = args.num_shards
110
+ if args.num_proc and args.num_proc > 1:
111
+ save_kwargs["num_proc"] = args.num_proc
112
+ # If num_proc is 1 (default), DO NOT pass it — HF's default is serial and
113
+ # we avoid the multiprocessing path entirely. Passing num_proc=1 explicitly
114
+ # still triggers the multiproc code path in some datasets versions.
115
+
116
+ try:
117
+ combined.save_to_disk(str(tmp), **save_kwargs)
118
+ except Exception:
119
+ logger.exception("save_to_disk failed; leaving %s for inspection.", tmp)
120
+ raise
121
+
122
+ # Verify every shard claimed in state.json actually exists AND is
123
+ # internally readable. On networked filesystems we've seen shards that
124
+ # have the expected byte length but are structurally malformed — the
125
+ # training process hits `OSError: Expected to be able to read N bytes for
126
+ # message body, got M` only at load time. Fail loudly HERE instead.
127
+ state_file = tmp / "state.json"
128
+ if not state_file.exists():
129
+ raise SystemExit(f"state.json missing from {tmp}; save_to_disk incomplete.")
130
+ import json
131
+ state = json.loads(state_file.read_text())
132
+ expected_shards = state.get("_data_files") or []
133
+ import pyarrow as pa
134
+ import pyarrow.ipc as ipc
135
+ for shard in expected_shards:
136
+ path = tmp / shard["filename"]
137
+ if not path.exists():
138
+ raise SystemExit(
139
+ f"Shard missing from {tmp}: {shard['filename']}. "
140
+ "Re-run the merge — the filesystem dropped shards mid-write."
141
+ )
142
+ try:
143
+ with pa.memory_map(str(path), "r") as src:
144
+ reader = ipc.open_stream(src)
145
+ _ = reader.read_all()
146
+ except Exception as e:
147
+ raise SystemExit(
148
+ f"Shard {path} exists ({path.stat().st_size} bytes) but is "
149
+ f"structurally malformed: {type(e).__name__}: {e}. "
150
+ "Re-run the merge (preferably with fewer --num-shards)."
151
+ )
152
+ logger.info("Verified %d shards in %s (existence + readability).",
153
+ len(expected_shards), tmp)
154
+
155
+ # Atomic-ish swap.
156
+ if out.exists():
157
+ backup = out.parent / f".{out.name}.old"
158
+ _safe_rmtree(backup)
159
+ out.replace(backup)
160
+ tmp.replace(out)
161
+ if (out.parent / f".{out.name}.old").exists():
162
+ _safe_rmtree(out.parent / f".{out.name}.old")
163
+ logger.info("Saved to %s", out)
164
+ print(out)
165
+
166
+
167
+ if __name__ == "__main__":
168
+ main()
training/train_fixed_stage1.sbatch ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ #ASC --vanilla
3
+
4
+ #SBATCH --output=logs/%j.out
5
+ #SBATCH --error=logs/%j.err
6
+ #SBATCH --job-name=edge32m-fixed-s1
7
+ #SBATCH --partition=zen4_0768_h100x4
8
+ #SBATCH --qos=zen4_0768_h100x4
9
+ #SBATCH --time=08:00:00
10
+ #SBATCH --nodes=2
11
+ #SBATCH --ntasks-per-node=1
12
+ #SBATCH --gres=gpu:4
13
+ #SBATCH --gpus-per-task=4
14
+ #SBATCH --cpus-per-task=8
15
+ #SBATCH --threads-per-core=1
16
+ #
17
+ # Stage 1 of the widened-32M curriculum: train the widened base on VL data.
18
+ # Base must already exist at output_edge32m_d128_fixed/base/ (with random-init new
19
+ # dims; rebuild with widen_colbert_projection.py if missing).
20
+ #
21
+ # Submit:
22
+ # mkdir -p logs
23
+ # sbatch widen_projection/train_widened_stage1.sbatch
24
+ # # then after it finishes, stage 2:
25
+ # sbatch widen_projection/train_widened_stage2.sbatch
26
+
27
+ set -euo pipefail
28
+ cd /data/fs201059/aa17626/modernTransformer
29
+
30
+ eval "$('/data/fs201059/aa17626/miniforge3/bin/conda' shell.bash hook)"
31
+ conda activate reason-mcb-repro
32
+
33
+ # Resolve master node hostname to an IP on the cluster ethernet (avoids
34
+ # NCCL picking a link-local/virbr bridge)
35
+ HEAD=$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)
36
+ export MASTER_ADDR=$(srun --nodes=1 --ntasks=1 -w "$HEAD" \
37
+ bash -c "ip -4 -o addr show eno2np0 | awk '{print \$4}' | cut -d/ -f1")
38
+ # Unique rendezvous port per job — avoids collisions with other running jobs
39
+ export MASTER_PORT=$(( 20000 + (SLURM_JOB_ID % 40000) ))
40
+ export GPUS_PER_NODE=4
41
+
42
+ # HF caches on shared FS
43
+ export HF_HOME=/data/fs201059/aa17626/.cache/huggingface
44
+ export HF_HUB_CACHE="$HF_HOME/hub"
45
+ export HF_DATASETS_CACHE="$HF_HOME/datasets"
46
+
47
+ # Project paths + pylate on PYTHONPATH
48
+ export PYTHONNOUSERSITE=1
49
+ export PYTHONPATH="/data/fs201059/aa17626/modernTransformer:/data/fs201059/aa17626/modernTransformer/pylate${PYTHONPATH:+:${PYTHONPATH}}"
50
+
51
+ # NCCL: pin to cluster ethernet for rendezvous + InfiniBand for the collective.
52
+ # Link-local / virbr / docker / tun interfaces on some nodes were tripping up
53
+ # auto-detection (169.254.x.x unroutable between nodes).
54
+ export NCCL_SOCKET_IFNAME=eno2np0 # 10.97.x.x cluster ethernet
55
+ export NCCL_IB_HCA=mlx5 # auto-match all InfiniBand HCAs on the node
56
+ export NCCL_IB_DISABLE=0 # use IB for collectives (fast)
57
+ export NCCL_IB_TIMEOUT=22
58
+ export NCCL_ASYNC_ERROR_HANDLING=1
59
+ export NCCL_DEBUG=WARN
60
+ export NCCL_TIMEOUT=3600
61
+
62
+ # ── Training config ─────────────────────────────────────────────────────
63
+ BASE_MODEL=/data/fs201059/aa17626/modernTransformer/repro_reason_moderncolbert/output_edge32m_d128_fixed/base
64
+ DATA_PATH=/data/fs201059/aa17626/modernTransformer/data/reasonir_vl_processed
65
+ OUTPUT_DIR=/data/fs201059/aa17626/modernTransformer/repro_reason_moderncolbert/output_edge32m_d128_fixed
66
+ RUN_NAME=stage-vl
67
+
68
+ EPOCHS=1
69
+ LR=1e-5 # VL warmup lr (matches v2b's Stage A)
70
+ BATCH_SIZE=32 # per device → 256 × 4 × 2 = 2048 global
71
+ MINI_BATCH_SIZE=32
72
+ GRADIENT_ACCUMULATION_STEPS=4
73
+ # Let zero-init-adjacent dims (new 64-dim rows) learn without clip saturating.
74
+ # Grad norms spike high at bootstrap while the small-random new rows find
75
+ # sensible values; 100 lets them flow.
76
+ export MAX_GRAD_NORM=10000
77
+ QUERY_LENGTH=256 # matches BRIGHT eval qlen; avoids truncation on long VL queries
78
+ DOCUMENT_LENGTH=2048 # covers p99 of all data; 8192 was wasted padding
79
+ ATTN=flash_attention_2
80
+ DTYPE=bfloat16
81
+
82
+ if [ ! -d "$BASE_MODEL" ]; then
83
+ echo "ERROR: widened base missing at $BASE_MODEL"
84
+ echo "Run: python widen_projection/widen_colbert_projection.py \\"
85
+ echo " --src <HF snapshot of mxbai-edge-colbert-v0-32m> \\"
86
+ echo " --dst $BASE_MODEL --new-dim 128 --verify"
87
+ exit 1
88
+ fi
89
+ if [ ! -f "$DATA_PATH/dataset_info.json" ]; then
90
+ echo "ERROR: VL data missing at $DATA_PATH"
91
+ echo "Run: python LaRLI/data/materialize_reasonir.py --config vl --output-dir $DATA_PATH"
92
+ exit 1
93
+ fi
94
+
95
+ echo "============================================================"
96
+ echo "Stage 1 (widened 32M d128): VL warmup"
97
+ echo "nodes=$SLURM_JOB_NUM_NODES gpus/node=$GPUS_PER_NODE global_batch=$((BATCH_SIZE * GPUS_PER_NODE * SLURM_JOB_NUM_NODES))"
98
+ echo " base : $BASE_MODEL"
99
+ echo " data : $DATA_PATH"
100
+ echo " output : $OUTPUT_DIR/$RUN_NAME/final"
101
+ echo " lr/ep : $LR / $EPOCHS"
102
+ echo " qlen : $QUERY_LENGTH doclen: $DOCUMENT_LENGTH max_grad_norm: $MAX_GRAD_NORM"
103
+ echo " master : $MASTER_ADDR:$MASTER_PORT (from $HEAD via eno2np0)"
104
+ echo " nccl : IFNAME=$NCCL_SOCKET_IFNAME IB_HCA=$NCCL_IB_HCA"
105
+ echo "============================================================"
106
+
107
+ srun --ntasks=$SLURM_JOB_NUM_NODES --ntasks-per-node=1 \
108
+ --gpus-per-task=$GPUS_PER_NODE \
109
+ --gpu-bind=none \
110
+ bash -c '
111
+ torchrun \
112
+ --nproc_per_node='"$GPUS_PER_NODE"' \
113
+ --nnodes='"$SLURM_JOB_NUM_NODES"' \
114
+ --node_rank="$SLURM_PROCID" \
115
+ --master_addr="'"$MASTER_ADDR"'" \
116
+ --master_port="'"$MASTER_PORT"'" \
117
+ widen_projection/launch_train_multinode.py \
118
+ --base_model '"$BASE_MODEL"' \
119
+ --train_data_path '"$DATA_PATH"' \
120
+ --epochs '"$EPOCHS"' --lr '"$LR"' \
121
+ --batch_size '"$BATCH_SIZE"' \
122
+ --mini_batch_size '"$MINI_BATCH_SIZE"' \
123
+ --gradient_accumulation_steps '"$GRADIENT_ACCUMULATION_STEPS"' \
124
+ --query_length '"$QUERY_LENGTH"' \
125
+ --document_length '"$DOCUMENT_LENGTH"' \
126
+ --attn_implementation '"$ATTN"' \
127
+ --torch_dtype '"$DTYPE"' \
128
+ --output_dir '"$OUTPUT_DIR"' \
129
+ --run_name '"$RUN_NAME"'
130
+ '
131
+ # Stage 1 output: $OUTPUT_DIR/$RUN_NAME/final
132
+ # Then submit stage 2:
133
+ # sbatch widen_projection/train_widened_stage2.sbatch
training/train_fixed_stage2.sbatch ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ #ASC --vanilla
3
+
4
+ #SBATCH --output=logs/%j.out
5
+ #SBATCH --error=logs/%j.err
6
+ #SBATCH --job-name=edge32m-fixed-s2
7
+ #SBATCH --partition=zen4_0768_h100x4
8
+ #SBATCH --qos=zen4_0768_h100x4
9
+ #SBATCH --time=12:00:00
10
+ #SBATCH --nodes=2
11
+ #SBATCH --ntasks-per-node=1
12
+ #SBATCH --gres=gpu:4
13
+ #SBATCH --gpus-per-task=4
14
+ #SBATCH --cpus-per-task=8
15
+ #SBATCH --threads-per-core=1
16
+ #
17
+ # Stage 2 (widened 32M: VL → BGE+HQ-hn polish).
18
+ # Request: 2 nodes × 4 H100 = 8 GPUs (matches Reason-MCB's 8-GPU training).
19
+ # per_device_batch_size = 256 → global / in-batch negatives = 2048,
20
+ # very close to the paper's 8×258 = 2064.
21
+ #
22
+ # Stage VL must already be at
23
+ # repro_reason_moderncolbert/output_edge32m_d128_fixed/stage-vl/final
24
+ # and the merged dataset at
25
+ # data/bge_hn_merged_processed
26
+ # before submission.
27
+ #
28
+ # Submit with:
29
+ # mkdir -p logs
30
+ # sbatch widen_projection/train_widened_stage2.sbatch
31
+
32
+ set -euo pipefail
33
+ cd /data/fs201059/aa17626/modernTransformer
34
+
35
+ eval "$('/data/fs201059/aa17626/miniforge3/bin/conda' shell.bash hook)"
36
+ conda activate reason-mcb-repro
37
+
38
+ # Resolve master node hostname to an IP on the cluster ethernet (avoids
39
+ # NCCL picking a link-local/virbr bridge).
40
+ HEAD=$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1)
41
+ export MASTER_ADDR=$(srun --nodes=1 --ntasks=1 -w "$HEAD" \
42
+ bash -c "ip -4 -o addr show eno2np0 | awk '{print \$4}' | cut -d/ -f1")
43
+ export MASTER_PORT=$(( 20000 + (SLURM_JOB_ID % 40000) )) # unique per job
44
+ export GPUS_PER_NODE=4
45
+
46
+ # NCCL: pin to cluster ethernet for rendezvous + InfiniBand for collectives.
47
+ export NCCL_SOCKET_IFNAME=eno2np0 # 10.97.x.x cluster ethernet
48
+ export NCCL_IB_HCA=mlx5 # match all InfiniBand HCAs on the node
49
+ export NCCL_IB_DISABLE=0
50
+ export NCCL_IB_TIMEOUT=22
51
+ export NCCL_ASYNC_ERROR_HANDLING=1
52
+ export NCCL_DEBUG=WARN
53
+ export NCCL_TIMEOUT=3600
54
+
55
+ # HF caches on shared FS
56
+ export HF_HOME=/data/fs201059/aa17626/.cache/huggingface
57
+ export HF_HUB_CACHE="$HF_HOME/hub"
58
+ export HF_DATASETS_CACHE="$HF_HOME/datasets"
59
+
60
+ # Project paths + pylate on PYTHONPATH
61
+ export PYTHONNOUSERSITE=1
62
+ export PYTHONPATH="/data/fs201059/aa17626/modernTransformer:/data/fs201059/aa17626/modernTransformer/pylate${PYTHONPATH:+:${PYTHONPATH}}"
63
+
64
+ # ── Training config ─────────────────────────────────────────────────────
65
+ BASE_MODEL=/data/fs201059/aa17626/modernTransformer/repro_reason_moderncolbert/output_edge32m_d128_fixed/stage-vl/final
66
+ DATA_PATH=/data/fs201059/aa17626/modernTransformer/data/bge_hn_merged_processed
67
+ OUTPUT_DIR=/data/fs201059/aa17626/modernTransformer/repro_reason_moderncolbert/output_edge32m_d128_fixed
68
+ RUN_NAME=stage-bge-hn
69
+
70
+ EPOCHS=1
71
+ LR=5e-6
72
+ BATCH_SIZE=8 # per device → 256 × 4 × 2 = 2048 global (≈ paper 2064)
73
+ MINI_BATCH_SIZE=8
74
+ # Let the zero-init widened projection dims actually learn. With default
75
+ # max_grad_norm=1.0, initial grad norms ~35k get clipped to 1/35000 of lr and the
76
+ # model is effectively frozen. 100 lets the new dims find sensible weights.
77
+ export MAX_GRAD_NORM=10000
78
+ GRADIENT_ACCUMULATION_STEPS=2
79
+ QUERY_LENGTH=256 # BRIGHT eval uses 256; training at 128 truncated 20% of our queries (p90=444 tokens)
80
+ DOCUMENT_LENGTH=2048 # covers p99 of every BRIGHT split and training data; 8192 was 4x wasted compute
81
+ ATTN=flash_attention_2
82
+ DTYPE=bfloat16
83
+
84
+ # Sanity
85
+ if [ ! -d "$BASE_MODEL" ]; then
86
+ echo "ERROR: stage-vl final missing at $BASE_MODEL"; exit 1
87
+ fi
88
+ if [ ! -f "$DATA_PATH/dataset_info.json" ]; then
89
+ echo "ERROR: merged dataset missing at $DATA_PATH"; exit 1
90
+ fi
91
+
92
+ echo "============================================================"
93
+ echo "Stage 2: widened 32M + VL → BGE+HQ-hn polish"
94
+ echo "nodes=$SLURM_JOB_NUM_NODES gpus/node=$GPUS_PER_NODE global_batch=$((BATCH_SIZE * GPUS_PER_NODE * SLURM_JOB_NUM_NODES))"
95
+ echo " base : $BASE_MODEL"
96
+ echo " data : $DATA_PATH"
97
+ echo " output : $OUTPUT_DIR/$RUN_NAME/final"
98
+ echo " lr/ep : $LR / $EPOCHS"
99
+ echo " master : $MASTER_ADDR:$MASTER_PORT"
100
+ echo "============================================================"
101
+
102
+ # One srun task per node; each task launches a local torchrun process group
103
+ # that binds to the 4 GPUs on its node. All nodes meet at $MASTER_ADDR.
104
+ srun --ntasks=$SLURM_JOB_NUM_NODES --ntasks-per-node=1 \
105
+ --gpus-per-task=$GPUS_PER_NODE \
106
+ --gpu-bind=none \
107
+ bash -c '
108
+ torchrun \
109
+ --nproc_per_node='"$GPUS_PER_NODE"' \
110
+ --nnodes='"$SLURM_JOB_NUM_NODES"' \
111
+ --node_rank="$SLURM_PROCID" \
112
+ --master_addr="'"$MASTER_ADDR"'" \
113
+ --master_port="'"$MASTER_PORT"'" \
114
+ widen_projection/launch_train_multinode.py \
115
+ --base_model '"$BASE_MODEL"' \
116
+ --train_data_path '"$DATA_PATH"' \
117
+ --epochs '"$EPOCHS"' --lr '"$LR"' \
118
+ --batch_size '"$BATCH_SIZE"' \
119
+ --mini_batch_size '"$MINI_BATCH_SIZE"' \
120
+ --gradient_accumulation_steps '"$GRADIENT_ACCUMULATION_STEPS"' \
121
+ --query_length '"$QUERY_LENGTH"' \
122
+ --document_length '"$DOCUMENT_LENGTH"' \
123
+ --attn_implementation '"$ATTN"' \
124
+ --torch_dtype '"$DTYPE"' \
125
+ --output_dir '"$OUTPUT_DIR"' \
126
+ --run_name '"$RUN_NAME"'
127
+ '
128
+ # Final model: $OUTPUT_DIR/$RUN_NAME/final
training/widen_colbert_projection.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Widen the final projection head of a PyLate ColBERT checkpoint from N→M dims.
2
+
3
+ The edge-ColBERT 32M model ships a 3-layer projection head ending at 64 dims
4
+ (see `3_Dense/config.json`). That 64-dim MaxSim channel is a bottleneck on
5
+ symbol-dense BRIGHT splits (leetcode, aops, theoremqa) where many tokens look
6
+ alike and need more channels to distinguish.
7
+
8
+ This script takes an EXISTING saved ColBERT checkpoint dir and writes a NEW
9
+ checkpoint dir whose final Dense layer has a widened `out_features`, without
10
+ modifying anything else (backbone weights, tokenizer, query/doc prefixes,
11
+ skiplist, etc. are all hard-linked from the source dir).
12
+
13
+ Zero-padding strategy (safe at init):
14
+ old linear.weight shape (old_dim, in_features)
15
+ new linear.weight shape (new_dim, in_features)
16
+ first old_dim rows = old rows
17
+ rows old_dim .. new_dim-1 = 0
18
+ After L2 normalisation (which the encoder applies before MaxSim), a vector
19
+ `[x; 0]` normalises to `[x/||x||; 0]`, so the initial cosine similarities and
20
+ MaxSim scores are IDENTICAL to the source model. Training from this init only
21
+ learns what to put in the extra channels — the original behaviour is
22
+ preserved unless the model finds something better.
23
+
24
+ Usage
25
+ -----
26
+ python widen_projection/widen_colbert_projection.py \
27
+ --src repro_reason_moderncolbert/output_edge32m_v2b/pos1_A/stage-vl/final \
28
+ --dst repro_reason_moderncolbert/output_edge32m_vl_d128/initial \
29
+ --new-dim 128
30
+
31
+ # Sanity-check the widened checkpoint loads and produces identical outputs
32
+ # on the first 64 dims:
33
+ python widen_projection/widen_colbert_projection.py \
34
+ --src repro_reason_moderncolbert/output_edge32m_v2b/pos1_A/stage-vl/final \
35
+ --dst repro_reason_moderncolbert/output_edge32m_vl_d128/initial \
36
+ --new-dim 128 --verify
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import argparse
42
+ import json
43
+ import logging
44
+ import os
45
+ import shutil
46
+ import sys
47
+ from pathlib import Path
48
+
49
+ import torch
50
+ from safetensors.torch import load_file as load_safetensors
51
+ from safetensors.torch import save_file as save_safetensors
52
+
53
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
54
+ logger = logging.getLogger(__name__)
55
+
56
+ # The pylate project root (so we can import models.ColBERT for --verify).
57
+ _PROJECT_ROOT = Path(__file__).resolve().parents[1]
58
+
59
+
60
+ def _hardlink_tree(src: Path, dst: Path, skip: set[str]) -> None:
61
+ """Mirror `src` to `dst` with hardlinks, skipping paths in `skip`.
62
+
63
+ HF Hub snapshot dirs contain symlinks pointing at `../../blobs/<sha>`;
64
+ hardlinking a symlink preserves the relative link text, which breaks in
65
+ the new location. We resolve symlinks to their real path before hardlinking
66
+ so the destination files have the actual content reachable.
67
+ """
68
+ for root, dirs, files in os.walk(src, followlinks=False):
69
+ rel_root = Path(root).relative_to(src)
70
+ if any(str(rel_root).startswith(s) for s in skip):
71
+ continue
72
+ (dst / rel_root).mkdir(parents=True, exist_ok=True)
73
+ for f in files:
74
+ rel_file = rel_root / f
75
+ if str(rel_file) in skip:
76
+ continue
77
+ src_f = Path(root) / f
78
+ dst_f = dst / rel_file
79
+ if dst_f.exists() or dst_f.is_symlink():
80
+ dst_f.unlink()
81
+ # Resolve if symlink so we hardlink the actual blob, not the
82
+ # symlink text (which would break when relative paths change).
83
+ real = src_f.resolve() if src_f.is_symlink() else src_f
84
+ try:
85
+ os.link(real, dst_f)
86
+ except OSError:
87
+ shutil.copy2(real, dst_f)
88
+
89
+
90
+ def _widen_dense(src_dense: Path, dst_dense: Path, new_out: int) -> tuple[int, int]:
91
+ """Rewrite one Dense/ subdirectory with widened `out_features`.
92
+
93
+ Returns (old_out, in_features).
94
+ """
95
+ cfg_path = src_dense / "config.json"
96
+ w_path = src_dense / "model.safetensors"
97
+ if not cfg_path.exists() or not w_path.exists():
98
+ raise FileNotFoundError(f"{src_dense} missing config.json or model.safetensors")
99
+
100
+ with open(cfg_path) as f:
101
+ cfg = json.load(f)
102
+ old_out = int(cfg["out_features"])
103
+ in_features = int(cfg["in_features"])
104
+
105
+ if old_out == new_out:
106
+ logger.info("%s already %d-dim; copying unchanged.", src_dense.name, new_out)
107
+ dst_dense.mkdir(parents=True, exist_ok=True)
108
+ shutil.copy2(cfg_path, dst_dense / "config.json")
109
+ shutil.copy2(w_path, dst_dense / "model.safetensors")
110
+ return old_out, in_features
111
+ if new_out < old_out:
112
+ raise ValueError(
113
+ f"Refusing to NARROW projection {src_dense.name} ({old_out} → {new_out}); "
114
+ "this tool only widens."
115
+ )
116
+
117
+ # Load old weight. Init new rows with SMALL RANDOM values (not zeros!).
118
+ #
119
+ # Why not zeros: after L2-normalisation, a zero-padded row produces 0 in
120
+ # the new dims on BOTH the query and document sides. The MaxSim score
121
+ # becomes identical to the old 64-dim model, and the gradient w.r.t. the
122
+ # new weights is exactly zero (dead-neuron deadlock). They never learn.
123
+ #
124
+ # Small Gaussian init (std matches the old rows' per-row stats) unlocks
125
+ # gradient flow while only changing MaxSim by O(std^2) at init — small
126
+ # enough that it doesn't undo the VL-stage training.
127
+ state = load_safetensors(str(w_path))
128
+ if "linear.weight" not in state:
129
+ raise KeyError(f"{w_path} missing key 'linear.weight' (got {list(state)})")
130
+ w_old = state["linear.weight"]
131
+ if w_old.shape != (old_out, in_features):
132
+ raise ValueError(
133
+ f"Unexpected weight shape {tuple(w_old.shape)}; "
134
+ f"expected ({old_out}, {in_features}) from config.json"
135
+ )
136
+
137
+ # Use the std of the existing rows so the new rows are scaled consistently.
138
+ old_std = float(w_old.float().std().item())
139
+ init_std = max(old_std * 0.1, 1e-3) # 10% of old magnitude; never below 1e-3
140
+ gen = torch.Generator().manual_seed(0)
141
+ pad = torch.randn((new_out - old_out, in_features), generator=gen,
142
+ dtype=torch.float32) * init_std
143
+ pad = pad.to(w_old.dtype)
144
+ w_new = torch.cat([w_old, pad], dim=0).contiguous()
145
+ logger.info(
146
+ "new rows: std=%.3e (10%% of old std=%.3e) max|abs|=%.3e",
147
+ init_std, old_std, pad.abs().max().item(),
148
+ )
149
+
150
+ new_state = {"linear.weight": w_new}
151
+ if "linear.bias" in state:
152
+ b_old = state["linear.bias"]
153
+ b_pad = torch.zeros((new_out - old_out,), dtype=b_old.dtype)
154
+ new_state["linear.bias"] = torch.cat([b_old, b_pad], dim=0).contiguous()
155
+
156
+ dst_dense.mkdir(parents=True, exist_ok=True)
157
+ save_safetensors(new_state, str(dst_dense / "model.safetensors"))
158
+
159
+ cfg_new = dict(cfg)
160
+ cfg_new["out_features"] = new_out
161
+ with open(dst_dense / "config.json", "w") as f:
162
+ json.dump(cfg_new, f, indent=2)
163
+ logger.info(
164
+ "widened %s: out_features %d → %d (weight shape %s → %s)",
165
+ src_dense.name, old_out, new_out, tuple(w_old.shape), tuple(w_new.shape),
166
+ )
167
+ return old_out, in_features
168
+
169
+
170
+ def _find_final_dense(src: Path) -> Path:
171
+ """Find the last Dense subdir referenced in modules.json."""
172
+ mod_path = src / "modules.json"
173
+ if not mod_path.exists():
174
+ raise FileNotFoundError(f"{src} missing modules.json — not a PyLate/ST model?")
175
+ with open(mod_path) as f:
176
+ modules = json.load(f)
177
+ dense_paths = [m["path"] for m in modules if m.get("path")
178
+ and (src / m["path"]).is_dir()
179
+ and m.get("type", "").endswith("Dense")]
180
+ if not dense_paths:
181
+ raise RuntimeError(f"{src}/modules.json has no Dense entries.")
182
+ final = dense_paths[-1]
183
+ return src / final
184
+
185
+
186
+ def verify(src: Path, dst: Path) -> None:
187
+ """Encode a tiny batch with both models and check the first-N dims match."""
188
+ sys.path.insert(0, str(_PROJECT_ROOT))
189
+ sys.path.insert(0, str(_PROJECT_ROOT / "pylate"))
190
+ from pylate import models # noqa: E402
191
+
192
+ sentences = ["hello world", "a quick brown fox"]
193
+ logger.info("loading source model: %s", src)
194
+ m_old = models.ColBERT(str(src))
195
+ logger.info("loading widened model: %s", dst)
196
+ m_new = models.ColBERT(str(dst))
197
+
198
+ emb_old = m_old.encode(sentences, is_query=False, convert_to_numpy=True, show_progress_bar=False)
199
+ emb_new = m_new.encode(sentences, is_query=False, convert_to_numpy=True, show_progress_bar=False)
200
+
201
+ import numpy as np # local import so widening path doesn't need numpy
202
+ ok = True
203
+ for i, (e_o, e_n) in enumerate(zip(emb_old, emb_new)):
204
+ old_dim = e_o.shape[-1]
205
+ # With small-random init the first old_dim coordinates DRIFT SLIGHTLY
206
+ # from the source (L2-normalisation couples them to the new dims via
207
+ # the norm denominator). Check the tail is non-zero (gradient will
208
+ # flow) and that the drift on the old dims is within a tolerance.
209
+ diff = np.abs(e_n[:, :old_dim] - e_o).max()
210
+ tail = np.abs(e_n[:, old_dim:]).max() if e_n.shape[-1] > old_dim else 0.0
211
+ logger.info("sentence %d: max |old - new[:, :%d]| = %.2e "
212
+ "max |new[:, %d:]| = %.2e (should be > 0 → grad flows)",
213
+ i, old_dim, diff, old_dim, tail)
214
+ if abs(float(diff)) > 5e-2 or abs(float(tail)) < 1e-4:
215
+ ok = False
216
+ if ok:
217
+ logger.info("VERIFY OK — widened model reproduces source behaviour on first %d dims.",
218
+ old_dim)
219
+ else:
220
+ logger.warning("VERIFY MISMATCH — differences above float tolerance; check script.")
221
+
222
+
223
+ def main() -> None:
224
+ p = argparse.ArgumentParser(description="Widen ColBERT final projection head.")
225
+ p.add_argument("--src", required=True, type=Path,
226
+ help="Source ColBERT checkpoint directory.")
227
+ p.add_argument("--dst", required=True, type=Path,
228
+ help="Destination directory (created if missing).")
229
+ p.add_argument("--new-dim", required=True, type=int,
230
+ help="New out_features for the final Dense layer.")
231
+ p.add_argument("--verify", action="store_true",
232
+ help="Load both models and check the first old_dim dims match.")
233
+ args = p.parse_args()
234
+
235
+ if not args.src.is_dir():
236
+ raise FileNotFoundError(f"--src {args.src} is not a directory")
237
+ args.dst.mkdir(parents=True, exist_ok=True)
238
+
239
+ final_dense_src = _find_final_dense(args.src)
240
+ final_dense_rel = final_dense_src.relative_to(args.src)
241
+ logger.info("Final Dense in source: %s (will widen)", final_dense_rel)
242
+
243
+ # Hardlink everything except the final Dense dir.
244
+ skip = {str(final_dense_rel)}
245
+ _hardlink_tree(args.src, args.dst, skip=skip)
246
+
247
+ # Widen the final Dense into the destination.
248
+ final_dense_dst = args.dst / final_dense_rel
249
+ if final_dense_dst.exists():
250
+ shutil.rmtree(final_dense_dst)
251
+ _widen_dense(final_dense_src, final_dense_dst, args.new_dim)
252
+
253
+ logger.info("Wrote widened checkpoint to %s", args.dst)
254
+ logger.info("Backbone + tokenizer + other Dense layers are hardlinks to source "
255
+ "(zero extra disk).")
256
+
257
+ if args.verify:
258
+ verify(args.src, args.dst)
259
+
260
+
261
+ if __name__ == "__main__":
262
+ main()