Sentence Similarity
Safetensors
sentence-transformers
English
PyLate
modernbert
ColBERT
multi-vector
feature-extraction
late-interaction
reasoning-retrieval
edge
Generated from Trainer
loss:CachedContrastive
text-embeddings-inference
Instructions to use DataScience-UIBK/Reason-mxbai-colbert-v0.1-32m with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use DataScience-UIBK/Reason-mxbai-colbert-v0.1-32m with sentence-transformers:
from pylate import models queries = [ "Which planet is known as the Red Planet?", "What is the largest planet in our solar system?", ] documents = [ ["Mars is the Red Planet.", "Venus is Earth's twin."], ["Jupiter is the largest planet.", "Saturn has rings."], ] model = models.ColBERT(model_name_or_path="DataScience-UIBK/Reason-mxbai-colbert-v0.1-32m") queries_emb = model.encode(queries, is_query=True) docs_emb = model.encode(documents, is_query=False) - Notebooks
- Google Colab
- Kaggle
v0.1: fix 2_Dense use_residual
Browse files- .gitattributes +1 -0
- .gitignore +6 -0
- 1_Dense/config.json +1 -0
- 1_Dense/model.safetensors +3 -0
- 2_Dense/config.json +1 -0
- 2_Dense/model.safetensors +3 -0
- 3_Dense/config.json +1 -0
- 3_Dense/model.safetensors +3 -0
- README.md +133 -0
- config.json +50 -0
- config_sentence_transformers.json +21 -0
- evaluation/evaluate_bright.py +470 -0
- logo_reason_mxai.png +3 -0
- model.safetensors +3 -0
- modules.json +26 -0
- results/BrightRetrieval_aops_evaluation_scores_qlen256.json +8 -0
- results/BrightRetrieval_biology_evaluation_scores_qlen256.json +8 -0
- results/BrightRetrieval_earth_science_evaluation_scores_qlen256.json +8 -0
- results/BrightRetrieval_economics_evaluation_scores_qlen256.json +8 -0
- results/BrightRetrieval_leetcode_evaluation_scores_qlen256.json +8 -0
- results/BrightRetrieval_pony_evaluation_scores_qlen32.json +8 -0
- results/BrightRetrieval_psychology_evaluation_scores_qlen256.json +8 -0
- results/BrightRetrieval_robotics_evaluation_scores_qlen256.json +8 -0
- results/BrightRetrieval_stackoverflow_evaluation_scores_qlen256.json +8 -0
- results/BrightRetrieval_sustainable_living_evaluation_scores_qlen256.json +8 -0
- results/BrightRetrieval_theoremqa_questions_evaluation_scores_qlen256.json +8 -0
- results/BrightRetrieval_theoremqa_theorems_evaluation_scores_qlen256.json +8 -0
- results/run_meta.json +16 -0
- results/summary.json +18 -0
- sentence_bert_config.json +4 -0
- special_tokens_map.json +31 -0
- tokenizer.json +0 -0
- tokenizer_config.json +968 -0
- training/launch_train_multinode.py +126 -0
- training/materialize_bge_reasoner.py +149 -0
- training/materialize_reasonir.py +108 -0
- training/merge_datasets.py +168 -0
- training/train_fixed_stage1.sbatch +133 -0
- training/train_fixed_stage2.sbatch +128 -0
- 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
|
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()
|